changeset 3161:3563c47e52fd beta

Implemented API calls for non-admin users for locking/unlocking repositories
author Marcin Kuzminski <marcin@python-works.com>
date Sun, 13 Jan 2013 22:55:56 +0100
parents 5d7f6b22d6b4
children a0a8f38e8fb8
files docs/api/api.rst rhodecode/controllers/api/api.py rhodecode/lib/auth.py rhodecode/tests/api/api_base.py
diffstat 4 files changed, 163 insertions(+), 17 deletions(-) [+]
line wrap: on
line diff
--- a/docs/api/api.rst	Sun Jan 13 20:03:15 2013 +0100
+++ b/docs/api/api.rst	Sun Jan 13 22:55:56 2013 +0100
@@ -155,9 +155,10 @@
 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 @@
     method :  "lock"
     args :    {
                 "repoid" : "<reponame or repo_id>"
-                "userid" : "<user_id or username>",
+                "userid" : "<user_id or username = Optional(=apiuser)>",
                 "locked" : "<bool true|false>"
               }
 
--- a/rhodecode/controllers/api/api.py	Sun Jan 13 20:03:15 2013 +0100
+++ b/rhodecode/controllers/api/api.py	Sun Jan 13 22:55:56 2013 +0100
@@ -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 @@
 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 '<OptionalAttr:%s>' % self.attr_name
+
+    def __call__(self):
+        return self
+#alias
+OAttr = OptionalAttr
+
+
 class Optional(object):
     """
     Defines an optional parameter::
@@ -184,10 +202,11 @@
                 '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 @@
         :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 @@
                     )
             )
 
-    @HasPermissionAnyDecorator('hg.admin')
+    @HasPermissionAllDecorator('hg.admin')
     def get_repo(self, apiuser, repoid):
         """"
         Get repository by name
@@ -526,7 +559,7 @@
         data['members'] = members
         return data
 
-    @HasPermissionAnyDecorator('hg.admin')
+    @HasPermissionAllDecorator('hg.admin')
     def get_repos(self, apiuser):
         """"
         Get all repositories
@@ -539,7 +572,7 @@
             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 @@
             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 @@
                                                             fork_name)
             )
 
-    @HasPermissionAnyDecorator('hg.admin')
+    @HasPermissionAllDecorator('hg.admin')
     def delete_repo(self, apiuser, repoid):
         """
         Deletes a given repository
@@ -708,7 +741,7 @@
                 '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 @@
                 )
             )
 
-    @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 @@
                 )
             )
 
-    @HasPermissionAnyDecorator('hg.admin')
+    @HasPermissionAllDecorator('hg.admin')
     def grant_users_group_permission(self, apiuser, repoid, usersgroupid,
                                      perm):
         """
@@ -811,7 +844,7 @@
                 )
             )
 
-    @HasPermissionAnyDecorator('hg.admin')
+    @HasPermissionAllDecorator('hg.admin')
     def revoke_users_group_permission(self, apiuser, repoid, usersgroupid):
         """
         Revoke permission for users group on given repository
--- a/rhodecode/lib/auth.py	Sun Jan 13 20:03:15 2013 +0100
+++ b/rhodecode/lib/auth.py	Sun Jan 13 22:55:56 2013 +0100
@@ -863,6 +863,109 @@
         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.
--- a/rhodecode/tests/api/api_base.py	Sun Jan 13 20:03:15 2013 +0100
+++ b/rhodecode/tests/api/api_base.py	Sun Jan 13 22:55:56 2013 +0100
@@ -247,6 +247,15 @@
                    % (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',