# HG changeset patch # User Marcin Kuzminski # Date 2013-01-13 21:55:56 # Node ID 3563c47e52fdd9ed31ce98b9eb336b9b759eca94 # Parent 5d7f6b22d6b4cd0740075e00e9e8fb3df0758a8b Implemented API calls for non-admin users for locking/unlocking repositories diff --git a/docs/api/api.rst b/docs/api/api.rst --- a/docs/api/api.rst +++ b/docs/api/api.rst @@ -155,9 +155,10 @@ OUTPUT:: lock ---- -Set locking state on given repository by given user. +Set locking state on given repository by given user. If userid param is skipped +, then it is set to id of user whos calling this method. This command can be executed only using api_key belonging to user with admin -rights. +rights or regular user that have admin or write access to repository. INPUT:: @@ -166,7 +167,7 @@ INPUT:: method : "lock" args : { "repoid" : "" - "userid" : "", + "userid" : "", "locked" : "" } diff --git a/rhodecode/controllers/api/api.py b/rhodecode/controllers/api/api.py --- a/rhodecode/controllers/api/api.py +++ b/rhodecode/controllers/api/api.py @@ -27,10 +27,12 @@ import traceback import logging +from pylons.controllers.util import abort from rhodecode.controllers.api import JSONRPCController, JSONRPCError -from rhodecode.lib.auth import HasPermissionAllDecorator, \ - HasPermissionAnyDecorator, PasswordGenerator, AuthUser +from rhodecode.lib.auth import PasswordGenerator, AuthUser, \ + HasPermissionAllDecorator, HasPermissionAnyDecorator, \ + HasPermissionAnyApi, HasRepoPermissionAnyApi from rhodecode.lib.utils import map_groups, repo2db_mapper from rhodecode.model.meta import Session from rhodecode.model.scm import ScmModel @@ -43,6 +45,22 @@ from rhodecode.model.db import Repositor log = logging.getLogger(__name__) +class OptionalAttr(object): + """ + Special Optional Option that defines other attribute + """ + def __init__(self, attr_name): + self.attr_name = attr_name + + def __repr__(self): + return '' % self.attr_name + + def __call__(self): + return self +#alias +OAttr = OptionalAttr + + class Optional(object): """ Defines an optional parameter:: @@ -184,10 +202,11 @@ class ApiController(JSONRPCController): 'Error occurred during rescan repositories action' ) - @HasPermissionAllDecorator('hg.admin') - def lock(self, apiuser, repoid, userid, locked): + def lock(self, apiuser, repoid, locked, userid=Optional(OAttr('apiuser'))): """ - Set locking state on particular repository by given user + Set locking state on particular repository by given user, if + this command is runned by non-admin account userid is set to user + who is calling this method :param apiuser: :param repoid: @@ -195,6 +214,20 @@ class ApiController(JSONRPCController): :param locked: """ repo = get_repo_or_error(repoid) + if HasPermissionAnyApi('hg.admin')(user=apiuser): + pass + elif HasRepoPermissionAnyApi('repository.admin', + 'repository.write')(user=apiuser, + repo_name=repo.repo_name): + #make sure normal user does not pass userid, he is not allowed to do that + if not isinstance(userid, Optional): + raise JSONRPCError( + 'Only RhodeCode admin can specify `userid` params' + ) + else: + return abort(403) + if isinstance(userid, Optional): + userid = apiuser.user_id user = get_user_or_error(userid) locked = bool(locked) try: @@ -495,7 +528,7 @@ class ApiController(JSONRPCController): ) ) - @HasPermissionAnyDecorator('hg.admin') + @HasPermissionAllDecorator('hg.admin') def get_repo(self, apiuser, repoid): """" Get repository by name @@ -526,7 +559,7 @@ class ApiController(JSONRPCController): data['members'] = members return data - @HasPermissionAnyDecorator('hg.admin') + @HasPermissionAllDecorator('hg.admin') def get_repos(self, apiuser): """" Get all repositories @@ -539,7 +572,7 @@ class ApiController(JSONRPCController): result.append(repo.get_api_data()) return result - @HasPermissionAnyDecorator('hg.admin') + @HasPermissionAllDecorator('hg.admin') def get_repo_nodes(self, apiuser, repoid, revision, root_path, ret_type='all'): """ @@ -642,7 +675,7 @@ class ApiController(JSONRPCController): log.error(traceback.format_exc()) raise JSONRPCError('failed to create repository `%s`' % repo_name) - @HasPermissionAnyDecorator('hg.admin') + @HasPermissionAllDecorator('hg.admin') def fork_repo(self, apiuser, repoid, fork_name, owner, description=Optional(''), copy_permissions=Optional(False), private=Optional(False), landing_rev=Optional('tip')): @@ -685,7 +718,7 @@ class ApiController(JSONRPCController): fork_name) ) - @HasPermissionAnyDecorator('hg.admin') + @HasPermissionAllDecorator('hg.admin') def delete_repo(self, apiuser, repoid): """ Deletes a given repository @@ -708,7 +741,7 @@ class ApiController(JSONRPCController): 'failed to delete repository `%s`' % repo.repo_name ) - @HasPermissionAnyDecorator('hg.admin') + @HasPermissionAllDecorator('hg.admin') def grant_user_permission(self, apiuser, repoid, userid, perm): """ Grant permission for user on given repository, or update existing one @@ -741,7 +774,7 @@ class ApiController(JSONRPCController): ) ) - @HasPermissionAnyDecorator('hg.admin') + @HasPermissionAllDecorator('hg.admin') def revoke_user_permission(self, apiuser, repoid, userid): """ Revoke permission for user on given repository @@ -772,7 +805,7 @@ class ApiController(JSONRPCController): ) ) - @HasPermissionAnyDecorator('hg.admin') + @HasPermissionAllDecorator('hg.admin') def grant_users_group_permission(self, apiuser, repoid, usersgroupid, perm): """ @@ -811,7 +844,7 @@ class ApiController(JSONRPCController): ) ) - @HasPermissionAnyDecorator('hg.admin') + @HasPermissionAllDecorator('hg.admin') def revoke_users_group_permission(self, apiuser, repoid, usersgroupid): """ Revoke permission for users group on given repository diff --git a/rhodecode/lib/auth.py b/rhodecode/lib/auth.py --- a/rhodecode/lib/auth.py +++ b/rhodecode/lib/auth.py @@ -863,6 +863,109 @@ class HasPermissionAnyMiddleware(object) return False +#============================================================================== +# SPECIAL VERSION TO HANDLE API AUTH +#============================================================================== +class _BaseApiPerm(object): + def __init__(self, *perms): + self.required_perms = set(perms) + + def __call__(self, check_location='unspecified', user=None, repo_name=None): + cls_name = self.__class__.__name__ + check_scope = 'user:%s, repo:%s' % (user, repo_name) + log.debug('checking cls:%s %s %s @ %s', cls_name, + self.required_perms, check_scope, check_location) + if not user: + log.debug('Empty User passed into arguments') + return False + + ## process user + if not isinstance(user, AuthUser): + user = AuthUser(user.user_id) + + if self.check_permissions(user.permissions, repo_name): + log.debug('Permission to %s granted for user: %s @ %s', repo_name, + user, check_location) + return True + + else: + log.debug('Permission to %s denied for user: %s @ %s', repo_name, + user, check_location) + return False + + def check_permissions(self, perm_defs, repo_name): + """ + implement in child class should return True if permissions are ok, + False otherwise + + :param perm_defs: dict with permission definitions + :param repo_name: repo name + """ + raise NotImplementedError() + + +class HasPermissionAllApi(_BaseApiPerm): + def __call__(self, user, check_location=''): + return super(HasPermissionAllApi, self)\ + .__call__(check_location=check_location, user=user) + + def check_permissions(self, perm_defs, repo): + if self.required_perms.issubset(perm_defs.get('global')): + return True + return False + + +class HasPermissionAnyApi(_BaseApiPerm): + def __call__(self, user, check_location=''): + return super(HasPermissionAnyApi, self)\ + .__call__(check_location=check_location, user=user) + + def check_permissions(self, perm_defs, repo): + if self.required_perms.intersection(perm_defs.get('global')): + return True + return False + + +class HasRepoPermissionAllApi(_BaseApiPerm): + def __call__(self, user, repo_name, check_location=''): + return super(HasRepoPermissionAllApi, self)\ + .__call__(check_location=check_location, user=user, + repo_name=repo_name) + + def check_permissions(self, perm_defs, repo_name): + + try: + self._user_perms = set( + [perm_defs['repositories'][repo_name]] + ) + except KeyError: + log.warning(traceback.format_exc()) + return False + if self.required_perms.issubset(self._user_perms): + return True + return False + + +class HasRepoPermissionAnyApi(_BaseApiPerm): + def __call__(self, user, repo_name, check_location=''): + return super(HasRepoPermissionAnyApi, self)\ + .__call__(check_location=check_location, user=user, + repo_name=repo_name) + + def check_permissions(self, perm_defs, repo_name): + + try: + _user_perms = set( + [perm_defs['repositories'][repo_name]] + ) + except KeyError: + log.warning(traceback.format_exc()) + return False + if self.required_perms.intersection(_user_perms): + return True + return False + + def check_ip_access(source_ip, allowed_ips=None): """ Checks if source_ip is a subnet of any of allowed_ips. diff --git a/rhodecode/tests/api/api_base.py b/rhodecode/tests/api/api_base.py --- a/rhodecode/tests/api/api_base.py +++ b/rhodecode/tests/api/api_base.py @@ -247,6 +247,15 @@ class BaseTestApi(object): % (TEST_USER_ADMIN_LOGIN, self.REPO, False)) self._compare_ok(id_, expected, given=response.body) + def test_api_lock_repo_lock_aquire_optional_userid(self): + id_, params = _build_data(self.apikey, 'lock', + repoid=self.REPO, + locked=True) + response = api_call(self, params) + expected = ('User `%s` set lock state for repo `%s` to `%s`' + % (TEST_USER_ADMIN_LOGIN, self.REPO, True)) + self._compare_ok(id_, expected, given=response.body) + @mock.patch.object(Repository, 'lock', crash) def test_api_lock_error(self): id_, params = _build_data(self.apikey, 'lock',