From a68ec6966eb05e3cee7654d0b9d4720c910e4e10 Mon Sep 17 00:00:00 2001 From: Evan Cordell <cordell.evan@gmail.com> Date: Tue, 18 Jul 2017 14:16:41 -0400 Subject: [PATCH] Add data interface for api-permissions for v2-2 --- endpoints/api/permission.py | 195 ++++++------------- endpoints/api/permission_models_interface.py | 111 +++++++++++ endpoints/api/permission_models_pre_oci.py | 118 +++++++++++ test/test_api_security.py | 14 ++ 4 files changed, 304 insertions(+), 134 deletions(-) create mode 100644 endpoints/api/permission_models_interface.py create mode 100644 endpoints/api/permission_models_pre_oci.py diff --git a/endpoints/api/permission.py b/endpoints/api/permission.py index 83ed3e687..e85c6480e 100644 --- a/endpoints/api/permission.py +++ b/endpoints/api/permission.py @@ -4,55 +4,27 @@ import logging from flask import request -from app import avatar from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource, log_action, request_error, validate_json_request, path_param) from endpoints.exception import NotFound -from data import model - +from permission_models_pre_oci import pre_oci_model as model +from permission_models_interface import DeleteException, SaveException logger = logging.getLogger(__name__) -def role_view(repo_perm_obj): - return { - 'role': repo_perm_obj.role.name, - } - -def wrap_role_view_user(role_json, user): - role_json['name'] = user.username - role_json['is_robot'] = user.robot - if not user.robot: - role_json['avatar'] = avatar.get_data_for_user(user) - return role_json - - -def wrap_role_view_org(role_json, user, org_members): - role_json['is_org_member'] = user.robot or user.username in org_members - return role_json - - -def wrap_role_view_team(role_json, team): - role_json['name'] = team.name - role_json['avatar'] = avatar.get_data_for_team(team) - return role_json - - @resource('/v1/repository/<apirepopath:repository>/permissions/team/') @path_param('repository', 'The full path of the repository. e.g. namespace/name') class RepositoryTeamPermissionList(RepositoryParamResource): """ Resource for repository team permissions. """ @require_repo_admin @nickname('listRepoTeamPermissions') - def get(self, namespace, repository): + def get(self, namespace_name, repository_name): """ List all team permission. """ - repo_perms = model.permission.get_all_repo_teams(namespace, repository) - - def wrapped_role_view(repo_perm): - return wrap_role_view_team(role_view(repo_perm), repo_perm.team) + repo_perms = model.get_repo_permissions_by_team(namespace_name, repository_name) return { - 'permissions': {repo_perm.team.name: wrapped_role_view(repo_perm) + 'permissions': {repo_perm.team_name: repo_perm.to_dict() for repo_perm in repo_perms} } @@ -63,38 +35,10 @@ class RepositoryUserPermissionList(RepositoryParamResource): """ Resource for repository user permissions. """ @require_repo_admin @nickname('listRepoUserPermissions') - def get(self, namespace, repository): + def get(self, namespace_name, repository_name): """ List all user permissions. """ - # Lookup the organization (if any). - org = None - try: - org = model.organization.get_organization(namespace) # Will raise an error if not org - except model.InvalidOrganizationException: - # This repository isn't under an org - pass - - # Load the permissions. - repo_perms = model.user.get_all_repo_users(namespace, repository) - - # Determine how to wrap the role(s). - def wrapped_role_view(repo_perm): - return wrap_role_view_user(role_view(repo_perm), repo_perm.user) - - role_view_func = wrapped_role_view - - if org: - users_filter = {perm.user for perm in repo_perms} - org_members = model.organization.get_organization_member_set(org, users_filter=users_filter) - current_func = role_view_func - - def wrapped_role_org_view(repo_perm): - return wrap_role_view_org(current_func(repo_perm), repo_perm.user, org_members) - - role_view_func = wrapped_role_org_view - - return { - 'permissions': {perm.user.username: role_view_func(perm) for perm in repo_perms} - } + perms = model.get_repo_permissions_by_user(namespace_name, repository_name) + return {'permissions': {p.username: p.to_dict() for p in perms}} @resource('/v1/repository/<apirepopath:repository>/permissions/user/<username>/transitive') @@ -105,19 +49,16 @@ class RepositoryUserTransitivePermission(RepositoryParamResource): or via a team. """ @require_repo_admin @nickname('getUserTransitivePermission') - def get(self, namespace, repository, username): + def get(self, namespace_name, repository_name, username): """ Get the fetch the permission for the specified user. """ - user = model.user.get_user(username) - if not user: + + roles = model.get_repo_roles(username, namespace_name, repository_name) + + if not roles: raise NotFound - - repo = model.repository.get_repository(namespace, repository) - if not repo: - raise NotFound - - permissions = list(model.permission.get_user_repo_permissions(user, repo)) + return { - 'permissions': [role_view(permission) for permission in permissions] + 'permissions': [r.to_dict() for r in roles] } @@ -149,69 +90,48 @@ class RepositoryUserPermission(RepositoryParamResource): @require_repo_admin @nickname('getUserPermissions') - def get(self, namespace, repository, username): - """ Get the Fetch the permission for the specified user. """ - logger.debug('Get repo: %s/%s permissions for user %s', namespace, repository, username) - perm = model.permission.get_user_reponame_permission(username, namespace, repository) - perm_view = wrap_role_view_user(role_view(perm), perm.user) - - try: - org = model.organization.get_organization(namespace) - org_members = model.organization.get_organization_member_set(org, users_filter={perm.user}) - perm_view = wrap_role_view_org(perm_view, perm.user, org_members) - except model.InvalidOrganizationException: - # This repository is not part of an organization - pass - - return perm_view + def get(self, namespace_name, repository_name, username): + """ Get the permission for the specified user. """ + logger.debug('Get repo: %s/%s permissions for user %s', namespace_name, repository_name, username) + perm = model.get_repo_permission_for_user(username, namespace_name, repository_name) + return perm.to_dict() @require_repo_admin @nickname('changeUserPermissions') @validate_json_request('UserPermission') - def put(self, namespace, repository, username): # Also needs to respond to post + def put(self, namespace_name, repository_name, username): # Also needs to respond to post """ Update the perimssions for an existing repository. """ new_permission = request.get_json() logger.debug('Setting permission to: %s for user %s', new_permission['role'], username) try: - perm = model.permission.set_user_repo_permission(username, namespace, repository, - new_permission['role']) - except model.DataModelException as ex: + perm = model.set_repo_permission_for_user(username, namespace_name, repository_name, + new_permission['role']) + resp = perm.to_dict() + except SaveException as ex: raise request_error(exception=ex) - perm_view = wrap_role_view_user(role_view(perm), perm.user) - - try: - org = model.organization.get_organization(namespace) - org_members = model.organization.get_organization_member_set(org, users_filter={perm.user}) - perm_view = wrap_role_view_org(perm_view, perm.user, org_members) - except model.InvalidOrganizationException: - # This repository is not part of an organization - pass - except model.DataModelException as ex: - raise request_error(exception=ex) - - log_action('change_repo_permission', namespace, - {'username': username, 'repo': repository, - 'namespace': namespace, + log_action('change_repo_permission', namespace_name, + {'username': username, 'repo': repository_name, + 'namespace': namespace_name, 'role': new_permission['role']}, - repo=model.repository.get_repository(namespace, repository)) + repo_name=repository_name) - return perm_view, 200 + return resp, 200 @require_repo_admin @nickname('deleteUserPermissions') - def delete(self, namespace, repository, username): + def delete(self, namespace_name, repository_name, username): """ Delete the permission for the user. """ try: - model.permission.delete_user_permission(username, namespace, repository) - except model.DataModelException as ex: + model.delete_repo_permission_for_user(username, namespace_name, repository_name) + except DeleteException as ex: raise request_error(exception=ex) - log_action('delete_repo_permission', namespace, - {'username': username, 'repo': repository, 'namespace': namespace}, - repo=model.repository.get_repository(namespace, repository)) + log_action('delete_repo_permission', namespace_name, + {'username': username, 'repo': repository_name, 'namespace': namespace_name}, + repo_name=repository_name) return '', 204 @@ -244,39 +164,46 @@ class RepositoryTeamPermission(RepositoryParamResource): @require_repo_admin @nickname('getTeamPermissions') - def get(self, namespace, repository, teamname): + def get(self, namespace_name, repository_name, teamname): """ Fetch the permission for the specified team. """ - logger.debug('Get repo: %s/%s permissions for team %s', namespace, repository, teamname) - perm = model.permission.get_team_reponame_permission(teamname, namespace, repository) - return role_view(perm) + logger.debug('Get repo: %s/%s permissions for team %s', namespace_name, repository_name, teamname) + role = model.get_repo_role_for_team(teamname, namespace_name, repository_name) + return role.to_dict() @require_repo_admin @nickname('changeTeamPermissions') @validate_json_request('TeamPermission') - def put(self, namespace, repository, teamname): + def put(self, namespace_name, repository_name, teamname): """ Update the existing team permission. """ new_permission = request.get_json() logger.debug('Setting permission to: %s for team %s', new_permission['role'], teamname) - perm = model.permission.set_team_repo_permission(teamname, namespace, repository, - new_permission['role']) + try: + perm = model.set_repo_permission_for_team(teamname, namespace_name, repository_name, + new_permission['role']) + resp = perm.to_dict() + except SaveException as ex: + raise request_error(exception=ex) + - log_action('change_repo_permission', namespace, - {'team': teamname, 'repo': repository, + log_action('change_repo_permission', namespace_name, + {'team': teamname, 'repo': repository_name, 'role': new_permission['role']}, - repo=model.repository.get_repository(namespace, repository)) - - return wrap_role_view_team(role_view(perm), perm.team), 200 + repo_name=repository_name) + return resp, 200 @require_repo_admin @nickname('deleteTeamPermissions') - def delete(self, namespace, repository, teamname): + def delete(self, namespace_name, repository_name, teamname): """ Delete the permission for the specified team. """ - model.permission.delete_team_permission(teamname, namespace, repository) - - log_action('delete_repo_permission', namespace, - {'team': teamname, 'repo': repository}, - repo=model.repository.get_repository(namespace, repository)) + try: + model.delete_repo_permission_for_team(teamname, namespace_name, repository_name) + except DeleteException as ex: + raise request_error(exception=ex) + + log_action('delete_repo_permission', namespace_name, + {'team': teamname, 'repo': repository_name}, + repo_name=repository_name) return '', 204 diff --git a/endpoints/api/permission_models_interface.py b/endpoints/api/permission_models_interface.py new file mode 100644 index 000000000..40bba3039 --- /dev/null +++ b/endpoints/api/permission_models_interface.py @@ -0,0 +1,111 @@ +import sys +from abc import ABCMeta, abstractmethod +from collections import namedtuple + +from six import add_metaclass + + +class SaveException(Exception): + def __init__(self, other): + self.traceback = sys.exc_info() + super(SaveException, self).__init__(other.message) + +class DeleteException(Exception): + def __init__(self, other): + self.traceback = sys.exc_info() + super(DeleteException, self).__init__(other.message) + + +class Role(namedtuple('Role', ['role_name'])): + def to_dict(self): + return { + 'role': self.role_name, + } + +class UserPermission(namedtuple('UserPermission', [ + 'role_name', + 'username', + 'is_robot', + 'avatar', + 'is_org_member', + 'has_org', + ])): + + def to_dict(self): + perm_dict = { + 'role': self.role_name, + 'username': self.username, + 'is_robot': False, + 'avatar': self.avatar, + } + if self.has_org: + perm_dict['is_org_member'] = self.is_org_member + return perm_dict + + +class RobotPermission(namedtuple('RobotPermission', [ + 'role_name', + 'username', + 'is_robot', + 'is_org_member', +])): + + def to_dict(self, user=None, team=None, org_members=None): + return { + 'role': self.role_name, + 'username': self.username, + 'is_robot': True, + 'is_org_member': self.is_org_member, + } + + +class TeamPermission(namedtuple('TeamPermission', [ + 'role_name', + 'team_name', + 'avatar', +])): + + def to_dict(self): + return { + 'role': self.role_name, + 'name': self.team_name, + 'avatar': self.avatar, + } + +@add_metaclass(ABCMeta) +class PermissionDataInterface(object): + @abstractmethod + def get_repo_permissions_by_user(self, namespace_name, repository_name): + pass + + @abstractmethod + def get_repo_roles(self, username, namespace_name, repository_name): + pass + + @abstractmethod + def get_repo_permission_for_user(self, username, namespace_name, repository_name): + pass + + @abstractmethod + def set_repo_permission_for_user(self, username, namespace_name, repository_name, role_name): + pass + + @abstractmethod + def delete_repo_permission_for_user(self, username, namespace_name, repository_name): + pass + + @abstractmethod + def get_repo_permissions_by_team(self, namespace_name, repository_name): + pass + + @abstractmethod + def get_repo_role_for_team(self, team_name, namespace_name, repository_name): + pass + + @abstractmethod + def set_repo_permission_for_team(self, team_name, namespace_name, repository_name, permission): + pass + + @abstractmethod + def delete_repo_permission_for_team(self, team_name, namespace_name, repository_name): + pass \ No newline at end of file diff --git a/endpoints/api/permission_models_pre_oci.py b/endpoints/api/permission_models_pre_oci.py new file mode 100644 index 000000000..07cee95ef --- /dev/null +++ b/endpoints/api/permission_models_pre_oci.py @@ -0,0 +1,118 @@ +from app import avatar +from data import model +from permission_models_interface import PermissionDataInterface, UserPermission, TeamPermission, Role, SaveException, DeleteException + + +class PreOCIModel(PermissionDataInterface): + """ + PreOCIModel implements the data model for Permission using a database schema + before it was changed to support the OCI specification. + """ + + def get_repo_permissions_by_user(self, namespace_name, repository_name): + org = None + try: + org = model.organization.get_organization(namespace_name) # Will raise an error if not org + except model.InvalidOrganizationException: + # This repository isn't under an org + pass + + # Load the permissions. + repo_perms = model.user.get_all_repo_users(namespace_name, repository_name) + + if org: + users_filter = {perm.user for perm in repo_perms} + org_members = model.organization.get_organization_member_set(org, users_filter=users_filter) + + def is_org_member(user): + if not org: + return False + + return user.robot or user.username in org_members + + + return [self._user_permission(perm, org is not None, is_org_member(perm.user)) for perm in repo_perms] + + + def get_repo_roles(self, username, namespace_name, repository_name): + user = model.user.get_user(username) + if not user: + return None + + repo = model.repository.get_repository(namespace_name, repository_name) + if not repo: + return None + + return [self._role(r) for r in model.permission.get_user_repo_permissions(user, repo)] + + def get_repo_permission_for_user(self, username, namespace_name, repository_name): + perm = model.permission.get_user_reponame_permission(username, namespace_name, repository_name) + org = None + try: + org = model.organization.get_organization(namespace_name) + org_members = model.organization.get_organization_member_set(org, users_filter={perm.user}) + is_org_member = perm.user.robot or perm.user.username in org_members + except model.InvalidOrganizationException: + # This repository is not part of an organization + is_org_member = False + + return self._user_permission(perm, org is not None, is_org_member) + + def set_repo_permission_for_user(self, username, namespace_name, repository_name, role_name): + try: + perm = model.permission.set_user_repo_permission(username, namespace_name, repository_name, role_name) + org = None + try: + org = model.organization.get_organization(namespace_name) + org_members = model.organization.get_organization_member_set(org, users_filter={perm.user}) + is_org_member = perm.user.robot or perm.user.username in org_members + except model.InvalidOrganizationException: + # This repository is not part of an organization + is_org_member = False + return self._user_permission(perm, org is not None, is_org_member) + except model.DataModelException as ex: + raise SaveException(ex) + + def delete_repo_permission_for_user(self, username, namespace_name, repository_name): + try: + model.permission.delete_user_permission(username, namespace_name, repository_name) + except model.DataModelException as ex: + raise DeleteException(ex) + + def get_repo_permissions_by_team(self, namespace_name, repository_name): + repo_perms = model.permission.get_all_repo_teams(namespace_name, repository_name) + return [self._team_permission(perm, perm.team.name) for perm in repo_perms] + + def get_repo_role_for_team(self, team_name, namespace_name, repository_name): + return self._role(model.permission.get_team_reponame_permission(team_name, namespace_name, repository_name)) + + def set_repo_permission_for_team(self, team_name, namespace_name, repository_name, role_name): + try: + return self._team_permission(model.permission.set_team_repo_permission(team_name, namespace_name, repository_name, role_name), team_name) + except model.DataModelException as ex: + raise SaveException(ex) + + def delete_repo_permission_for_team(self, team_name, namespace_name, repository_name): + try: + model.permission.delete_team_permission(team_name, namespace_name, repository_name) + except model.DataModelException as ex: + raise DeleteException(ex) + + + def _role(self, permission_obj): + return Role(role_name=permission_obj.role.name) + + def _user_permission(self, permission_obj, has_org, is_org_member): + return UserPermission(role_name=permission_obj.role.name, + username=permission_obj.user.username, + is_robot=permission_obj.user.robot, + avatar=avatar.get_data_for_user(permission_obj.user), + is_org_member=is_org_member, + has_org=has_org) + + def _team_permission(self, permission_obj, team_name): + return TeamPermission(role_name=permission_obj.role.name, + team_name=permission_obj.team.name, + avatar=avatar.get_data_for_team(permission_obj.team)) + +pre_oci_model = PreOCIModel() \ No newline at end of file diff --git a/test/test_api_security.py b/test/test_api_security.py index 40638dd24..348d2edcc 100644 --- a/test/test_api_security.py +++ b/test/test_api_security.py @@ -789,6 +789,20 @@ class TestRepositoryUserTransitivePermissionA2o9DevtableShared(ApiTestCase): def test_get_devtable(self): self._run_test('GET', 404, 'devtable', None) + + +class TestRepositoryUserTransitivePermissionA2o9DevtableShared(ApiTestCase): + def setUp(self): + ApiTestCase.setUp(self) + self._set_url(RepositoryUserTransitivePermission, username="devtable", repository="devtable/shared") + + def test_get_allowed(self): + self._run_test('GET', 200, 'devtable', None) + + def test_get_allowed_no_repo(self): + self._set_url(RepositoryUserTransitivePermission, username="devtable", repository="devtable/nope") + self._run_test('GET', 404, 'devtable', None) + class TestRepositoryUserTransitivePermissionA2o9BuynlargeOrgrepo(ApiTestCase): def setUp(self):