diff --git a/auth/permissions.py b/auth/permissions.py index 94ad38e9a..c5d86963e 100644 --- a/auth/permissions.py +++ b/auth/permissions.py @@ -68,7 +68,10 @@ class QuayDeferredPermissionUser(Identity): def __init__(self, uuid, auth_type, auth_scopes, user=None): super(QuayDeferredPermissionUser, self).__init__(uuid, auth_type) - self._permissions_loaded = False + self._namespace_wide_loaded = set() + self._repositories_loaded = set() + self._personal_loaded = False + self._scope_set = auth_scopes self._user_object = user @@ -103,60 +106,108 @@ class QuayDeferredPermissionUser(Identity): def _user_role_for_scopes(self, role): return self._translate_role_for_scopes(USER_ROLES, SCOPE_MAX_USER_ROLES, role) + def _populate_user_provides(self, user_object): + """ Populates the provides that naturally apply to a user, such as being the admin of + their own namespace. + """ + + # Add the user specific permissions, only for non-oauth permission + user_grant = _UserNeed(user_object.username, self._user_role_for_scopes('admin')) + logger.debug('User permission: {0}'.format(user_grant)) + self.provides.add(user_grant) + + # Every user is the admin of their own 'org' + user_namespace = _OrganizationNeed(user_object.username, self._team_role_for_scopes('admin')) + logger.debug('User namespace permission: {0}'.format(user_namespace)) + self.provides.add(user_namespace) + + # Org repo roles can differ for scopes + user_repos = _OrganizationRepoNeed(user_object.username, self._repo_role_for_scopes('admin')) + logger.debug('User namespace repo permission: {0}'.format(user_repos)) + self.provides.add(user_repos) + + if ((scopes.SUPERUSER in self._scope_set or scopes.DIRECT_LOGIN in self._scope_set) and + superusers.is_superuser(user_object.username)): + logger.debug('Adding superuser to user: %s', user_object.username) + self.provides.add(_SuperUserNeed()) + + def _populate_namespace_wide_provides(self, user_object, namespace_filter): + """ Populates the namespace-wide provides for a particular user under a particular namespace. + This method does *not* add any provides for specific repositories. + """ + + for team in model.permission.get_org_wide_permissions(user_object, org_filter=namespace_filter): + team_org_grant = _OrganizationNeed(team.organization.username, + self._team_role_for_scopes(team.role.name)) + logger.debug('Organization team added permission: {0}'.format(team_org_grant)) + self.provides.add(team_org_grant) + + team_repo_role = TEAM_REPO_ROLES[team.role.name] + org_repo_grant = _OrganizationRepoNeed(team.organization.username, + self._repo_role_for_scopes(team_repo_role)) + logger.debug('Organization team added repo permission: {0}'.format(org_repo_grant)) + self.provides.add(org_repo_grant) + + team_grant = _TeamNeed(team.organization.username, team.name, + self._team_role_for_scopes(team.role.name)) + logger.debug('Team added permission: {0}'.format(team_grant)) + self.provides.add(team_grant) + + def _populate_repository_provides(self, user_object, namespace_filter, repository_name): + """ Populates the repository-specific provides for a particular user and repository. """ + + if namespace_filter and repository_name: + permissions = model.permission.get_user_repository_permissions(user_object, namespace_filter, + repository_name) + else: + permissions = model.permission.get_all_user_repository_permissions(user_object) + + for perm in permissions: + repo_grant = _RepositoryNeed(perm.repository.namespace_user.username, perm.repository.name, + self._repo_role_for_scopes(perm.role.name)) + logger.debug('User added permission: {0}'.format(repo_grant)) + self.provides.add(repo_grant) + def can(self, permission): - if not self._permissions_loaded: - logger.debug('Loading user permissions after deferring for: %s', self.id) - user_object = self._user_object or model.user.get_user_by_uuid(self.id) - if user_object is None: + logger.debug('Loading user permissions after deferring for: %s', self.id) + user_object = self._user_object or model.user.get_user_by_uuid(self.id) + if user_object is None: + return super(QuayDeferredPermissionUser, self).can(permission) + + # Add the user-specific provides. + if not self._personal_loaded: + self._populate_user_provides(user_object) + self._personal_loaded = True + + # If we now have permission, no need to load any more permissions. + if super(QuayDeferredPermissionUser, self).can(permission): + return super(QuayDeferredPermissionUser, self).can(permission) + + # Check for namespace and/or repository permissions. + perm_namespace = getattr(permission, 'namespace', None) + perm_repo_name = getattr(permission, 'repo_name', None) + perm_repository = None + + if perm_namespace and perm_repo_name: + perm_repository = '%s/%s' % (perm_namespace, perm_repo_name) + + if not perm_namespace and not perm_repo_name: + # Nothing more to load, so just check directly. + return super(QuayDeferredPermissionUser, self).can(permission) + + # Lazy-load the repository-specific permissions. + if perm_repository and perm_repository not in self._repositories_loaded: + self._populate_repository_provides(user_object, perm_namespace, perm_repo_name) + self._repositories_loaded.add(perm_repository) + + # If we now have permission, no need to load any more permissions. + if super(QuayDeferredPermissionUser, self).can(permission): return super(QuayDeferredPermissionUser, self).can(permission) - if ((scopes.SUPERUSER in self._scope_set or scopes.DIRECT_LOGIN in self._scope_set) and - superusers.is_superuser(user_object.username)): - logger.debug('Adding superuser to user: %s', user_object.username) - self.provides.add(_SuperUserNeed()) - - # Add the user specific permissions, only for non-oauth permission - user_grant = _UserNeed(user_object.username, self._user_role_for_scopes('admin')) - logger.debug('User permission: {0}'.format(user_grant)) - self.provides.add(user_grant) - - # Every user is the admin of their own 'org' - user_namespace = _OrganizationNeed(user_object.username, self._team_role_for_scopes('admin')) - logger.debug('User namespace permission: {0}'.format(user_namespace)) - self.provides.add(user_namespace) - - # Org repo roles can differ for scopes - user_repos = _OrganizationRepoNeed(user_object.username, self._repo_role_for_scopes('admin')) - logger.debug('User namespace repo permission: {0}'.format(user_repos)) - self.provides.add(user_repos) - - # Add repository permissions - for perm in model.permission.get_all_user_permissions(user_object): - repo_grant = _RepositoryNeed(perm.repository.namespace_user.username, perm.repository.name, - self._repo_role_for_scopes(perm.role.name)) - logger.debug('User added permission: {0}'.format(repo_grant)) - self.provides.add(repo_grant) - - # Add namespace permissions derived - for team in model.permission.get_org_wide_permissions(user_object): - team_org_grant = _OrganizationNeed(team.organization.username, - self._team_role_for_scopes(team.role.name)) - logger.debug('Organization team added permission: {0}'.format(team_org_grant)) - self.provides.add(team_org_grant) - - - team_repo_role = TEAM_REPO_ROLES[team.role.name] - org_repo_grant = _OrganizationRepoNeed(team.organization.username, - self._repo_role_for_scopes(team_repo_role)) - logger.debug('Organization team added repo permission: {0}'.format(org_repo_grant)) - self.provides.add(org_repo_grant) - - team_grant = _TeamNeed(team.organization.username, team.name, - self._team_role_for_scopes(team.role.name)) - logger.debug('Team added permission: {0}'.format(team_grant)) - self.provides.add(team_grant) - - self._permissions_loaded = True + # Lazy-load the namespace-wide-only permissions. + if perm_namespace and perm_namespace not in self._namespace_wide_loaded: + self._populate_namespace_wide_provides(user_object, perm_namespace) + self._namespace_wide_loaded.add(perm_namespace) return super(QuayDeferredPermissionUser, self).can(permission) @@ -167,6 +218,10 @@ class ModifyRepositoryPermission(Permission): write_need = _RepositoryNeed(namespace, name, 'write') org_admin_need = _OrganizationRepoNeed(namespace, 'admin') org_write_need = _OrganizationRepoNeed(namespace, 'write') + + self.namespace = namespace + self.repo_name = name + super(ModifyRepositoryPermission, self).__init__(admin_need, write_need, org_admin_need, org_write_need) @@ -179,6 +234,10 @@ class ReadRepositoryPermission(Permission): org_admin_need = _OrganizationRepoNeed(namespace, 'admin') org_write_need = _OrganizationRepoNeed(namespace, 'write') org_read_need = _OrganizationRepoNeed(namespace, 'read') + + self.namespace = namespace + self.repo_name = name + super(ReadRepositoryPermission, self).__init__(admin_need, write_need, read_need, org_admin_need, org_read_need, org_write_need) @@ -187,6 +246,10 @@ class AdministerRepositoryPermission(Permission): def __init__(self, namespace, name): admin_need = _RepositoryNeed(namespace, name, 'admin') org_admin_need = _OrganizationRepoNeed(namespace, 'admin') + + self.namespace = namespace + self.repo_name = name + super(AdministerRepositoryPermission, self).__init__(admin_need, org_admin_need) @@ -195,6 +258,9 @@ class CreateRepositoryPermission(Permission): def __init__(self, namespace): admin_org = _OrganizationNeed(namespace, 'admin') create_repo_org = _OrganizationNeed(namespace, 'creator') + + self.namespace = namespace + super(CreateRepositoryPermission, self).__init__(admin_org, create_repo_org) @@ -220,6 +286,9 @@ class UserReadPermission(Permission): class AdministerOrganizationPermission(Permission): def __init__(self, org_name): admin_org = _OrganizationNeed(org_name, 'admin') + + self.namespace = org_name + super(AdministerOrganizationPermission, self).__init__(admin_org) @@ -228,6 +297,9 @@ class OrganizationMemberPermission(Permission): admin_org = _OrganizationNeed(org_name, 'admin') repo_creator_org = _OrganizationNeed(org_name, 'creator') org_member = _OrganizationNeed(org_name, 'member') + + self.namespace = org_name + super(OrganizationMemberPermission, self).__init__(admin_org, org_member, repo_creator_org) @@ -238,6 +310,9 @@ class ViewTeamPermission(Permission): team_creator = _TeamNeed(org_name, team_name, 'creator') team_member = _TeamNeed(org_name, team_name, 'member') admin_org = _OrganizationNeed(org_name, 'admin') + + self.namespace = org_name + super(ViewTeamPermission, self).__init__(team_admin, team_creator, team_member, admin_org) diff --git a/data/model/permission.py b/data/model/permission.py index 340ab3835..f95764c0c 100644 --- a/data/model/permission.py +++ b/data/model/permission.py @@ -33,7 +33,7 @@ def list_organization_member_permissions(organization, limit_to_user=None): return query -def get_all_user_permissions(user): +def get_all_user_repository_permissions(user): return _get_user_repo_permissions(user) @@ -41,7 +41,12 @@ def get_user_repo_permissions(user, repo): return _get_user_repo_permissions(user, limit_to_repository_obj=repo) -def _get_user_repo_permissions(user, limit_to_repository_obj=None): +def get_user_repository_permissions(user, namespace, repo_name): + return _get_user_repo_permissions(user, limit_namespace=namespace, limit_repo_name=repo_name) + + +def _get_user_repo_permissions(user, limit_to_repository_obj=None, limit_namespace=None, + limit_repo_name=None): UserThroughTeam = User.alias() base_query = (RepositoryPermission @@ -54,6 +59,9 @@ def _get_user_repo_permissions(user, limit_to_repository_obj=None): if limit_to_repository_obj is not None: base_query = base_query.where(RepositoryPermission.repository == limit_to_repository_obj) + elif limit_namespace and limit_repo_name: + base_query = base_query.where(Repository.name == limit_repo_name, + Namespace.username == limit_namespace) direct = (base_query .clone() @@ -121,12 +129,16 @@ def add_prototype_permission(org, role_name, activating_user, delegate_user=delegate_user, delegate_team=delegate_team) -def get_org_wide_permissions(user): +def get_org_wide_permissions(user, org_filter=None): Org = User.alias() team_with_role = Team.select(Team, Org, TeamRole).join(TeamRole) with_org = team_with_role.switch(Team).join(Org, on=(Team.organization == Org.id)) with_user = with_org.switch(Team).join(TeamMember).join(User) + + if org_filter: + with_user.where(Org.username == org_filter) + return with_user.where(User.id == user, Org.organization == True) diff --git a/test/test_api_usage.py b/test/test_api_usage.py index cd700fc1c..d67f097f5 100644 --- a/test/test_api_usage.py +++ b/test/test_api_usage.py @@ -69,11 +69,11 @@ app.register_blueprint(webhooks, url_prefix='/webhooks') BASE_QUERY_COUNT = 0 # The number of queries we run for logged in users on API calls. -BASE_LOGGEDIN_QUERY_COUNT = BASE_QUERY_COUNT + 2 +BASE_LOGGEDIN_QUERY_COUNT = BASE_QUERY_COUNT + 1 # The number of queries we run for logged in users on API calls that check # access permissions. -BASE_ACCESS_QUERY_COUNT = BASE_LOGGEDIN_QUERY_COUNT + 1 +BASE_PERM_ACCESS_QUERY_COUNT = BASE_LOGGEDIN_QUERY_COUNT + 2 NO_ACCESS_USER = 'freshuser' READ_ACCESS_USER = 'reader' @@ -265,7 +265,7 @@ class TestUserStarredRepositoryList(ApiTestCase): self.login(READ_ACCESS_USER) # Queries: Base + the list query - with assert_query_count(BASE_ACCESS_QUERY_COUNT + 1): + with assert_query_count(BASE_LOGGEDIN_QUERY_COUNT + 1): self.getJsonResponse(StarredRepositoryList, expected_code=200) def test_star_repo_guest(self): @@ -280,7 +280,7 @@ class TestUserStarredRepositoryList(ApiTestCase): self.login(READ_ACCESS_USER) # Queries: Base + the list query - with assert_query_count(BASE_ACCESS_QUERY_COUNT + 1): + with assert_query_count(BASE_LOGGEDIN_QUERY_COUNT + 1): json = self.getJsonResponse(StarredRepositoryList) assert json['repositories'] == [] @@ -667,6 +667,16 @@ class TestConductSearch(ApiTestCase): self.assertEquals(json['results'][0]['name'], 'readers') + def test_explicit_permission(self): + self.login('reader') + + json = self.getJsonResponse(ConductSearch, + params=dict(query='shared')) + + self.assertEquals(1, len(json['results'])) + self.assertEquals(json['results'][0]['kind'], 'repository') + self.assertEquals(json['results'][0]['name'], 'shared') + class TestGetMatchingEntities(ApiTestCase): def test_notinorg(self): @@ -1355,7 +1365,7 @@ class TestListRepos(ApiTestCase): self.login(READ_ACCESS_USER) # Queries: Base + the list query - with assert_query_count(BASE_LOGGEDIN_QUERY_COUNT + 1): + with assert_query_count(BASE_LOGGEDIN_QUERY_COUNT + 2): json = self.getJsonResponse(RepositoryList, params=dict(public=True)) self.assertGreater(len(json['repositories']), 0) @@ -1374,8 +1384,8 @@ class TestListRepos(ApiTestCase): def test_listrepos_allparams(self): self.login(ADMIN_ACCESS_USER) - # Queries: Base + the list query + the popularity and last modified queries - with assert_query_count(BASE_LOGGEDIN_QUERY_COUNT + 3): + # Queries: Base + the list query + the popularity and last modified queries + full perms load + with assert_query_count(BASE_LOGGEDIN_QUERY_COUNT + 4): json = self.getJsonResponse(RepositoryList, params=dict(namespace=ORGANIZATION, public=False, @@ -1835,8 +1845,8 @@ class TestRepoBuilds(ApiTestCase): def test_getrepo_nobuilds(self): self.login(ADMIN_ACCESS_USER) - # Queries: Base + the list query - with assert_query_count(BASE_ACCESS_QUERY_COUNT + 1): + # Queries: Permission + the list query + with assert_query_count(2): json = self.getJsonResponse(RepositoryBuildList, params=dict(repository=ADMIN_ACCESS_USER + '/simple')) @@ -1845,8 +1855,8 @@ class TestRepoBuilds(ApiTestCase): def test_getrepobuilds(self): self.login(ADMIN_ACCESS_USER) - # Queries: Base + the list query - with assert_query_count(BASE_ACCESS_QUERY_COUNT + 1): + # Queries: Permission + the list query + with assert_query_count(2): json = self.getJsonResponse(RepositoryBuildList, params=dict(repository=ADMIN_ACCESS_USER + '/building')) @@ -2572,12 +2582,12 @@ class TestUserRobots(ApiTestCase): params=dict(robot_shortname='coolbot'), expected_code=201) - # Queries: Base + the list query - with assert_query_count(BASE_ACCESS_QUERY_COUNT + 1): + # Queries: Base + the lookup query + with assert_query_count(BASE_LOGGEDIN_QUERY_COUNT + 1): self.getJsonResponse(UserRobotList) - # Queries: Base + the list query - with assert_query_count(BASE_ACCESS_QUERY_COUNT + 1): + # Queries: Base + the lookup query + with assert_query_count(BASE_LOGGEDIN_QUERY_COUNT + 1): self.getJsonResponse(UserRobotList, params=dict(permissions=True))