diff --git a/data/model/legacy.py b/data/model/legacy.py index a41f4c36e..4276775e1 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -1201,6 +1201,12 @@ def update_email(user, new_email, auto_verify=False): def get_all_user_permissions(user): + return _get_user_repo_permissions(user) + +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): UserThroughTeam = User.alias() base_query = (RepositoryPermission @@ -1211,6 +1217,9 @@ def get_all_user_permissions(user): .join(Namespace, on=(Repository.namespace_user == Namespace.id)) .switch(RepositoryPermission)) + if limit_to_repository_obj is not None: + base_query = base_query.where(RepositoryPermission.repository == limit_to_repository_obj) + direct = (base_query .clone() .join(User) diff --git a/endpoints/api/permission.py b/endpoints/api/permission.py index 6e160d688..f8c3f5012 100644 --- a/endpoints/api/permission.py +++ b/endpoints/api/permission.py @@ -6,7 +6,8 @@ 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) + log_action, request_error, validate_json_request, path_param, + NotFound) from data import model @@ -96,6 +97,30 @@ class RepositoryUserPermissionList(RepositoryParamResource): } +@resource('/v1/repository//permissions/user//transitive') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') +@path_param('username', 'The username of the user to which the permissions apply') +class RepositoryUserTransitivePermission(RepositoryParamResource): + """ Resource for retrieving whether a user has access to a repository, either directly + or via a team. """ + @require_repo_admin + @nickname('getUserTransitivePermission') + def get(self, namespace, repository, username): + """ Get the fetch the permission for the specified user. """ + user = model.get_user(username) + if not user: + raise NotFound + + repo = model.get_repository(namespace, repository) + if not repo: + raise NotFound + + permissions = list(model.get_user_repo_permissions(user, repo)) + return { + 'permissions': [role_view(permission) for permission in permissions] + } + + @resource('/v1/repository//permissions/user/') @path_param('repository', 'The full path of the repository. e.g. namespace/name') @path_param('username', 'The username of the user to which the permission applies') diff --git a/static/directives/fetch-tag-dialog.html b/static/directives/fetch-tag-dialog.html index d808f0e6c..0e9c24930 100644 --- a/static/directives/fetch-tag-dialog.html +++ b/static/directives/fetch-tag-dialog.html @@ -56,6 +56,13 @@
+ +
+ Warning: Robot account {{ currentRobot.name }} does not have + read permission on this repository, so the command below will fail with an authorization error. +
+
Command:
{{ getCommand(currentFormat, currentRobot) }}
diff --git a/static/js/directives/ui/fetch-tag-dialog.js b/static/js/directives/ui/fetch-tag-dialog.js index 57377bf66..7f902f53a 100644 --- a/static/js/directives/ui/fetch-tag-dialog.js +++ b/static/js/directives/ui/fetch-tag-dialog.js @@ -18,6 +18,7 @@ angular.module('quay').directive('fetchTagDialog', function () { $scope.currentEntity = null; $scope.currentRobot = null; $scope.formats = []; + $scope.currentRobotHasPermission = null; UserService.updateUserIn($scope, updateFormats); @@ -58,6 +59,7 @@ angular.module('quay').directive('fetchTagDialog', function () { } $scope.currentRobot = null; + $scope.currentRobotHasPermission = null; var parts = entity.name.split('+'); var namespace = parts[0]; @@ -71,6 +73,15 @@ angular.module('quay').directive('fetchTagDialog', function () { ApiService.getRobot(orgname, null, params).then(function(resp) { $scope.currentRobot = resp; }, ApiService.errorDisplay('Cannot download robot token')); + + var permParams = { + 'repository': $scope.repository.namespace + '/' + $scope.repository.name, + 'username': entity.name + }; + + ApiService.getUserTransitivePermission(null, permParams).then(function(resp) { + $scope.currentRobotHasPermission = resp['permissions'].length > 0; + }); }); $scope.getCommand = function(format, robot) { @@ -106,6 +117,7 @@ angular.module('quay').directive('fetchTagDialog', function () { $scope.currentFormat = null; $scope.currentEntity = null; $scope.currentRobot = null; + $scope.currentRobotHasPermission = null; $scope.clearCounter++; diff --git a/test/test_api_security.py b/test/test_api_security.py index 5a9adb92b..030073b7f 100644 --- a/test/test_api_security.py +++ b/test/test_api_security.py @@ -43,7 +43,8 @@ from endpoints.api.organization import (OrganizationList, OrganizationMember, OrganizationApplicationResetClientSecret) from endpoints.api.repository import RepositoryList, RepositoryVisibility, Repository from endpoints.api.permission import (RepositoryUserPermission, RepositoryTeamPermission, - RepositoryTeamPermissionList, RepositoryUserPermissionList) + RepositoryTeamPermissionList, RepositoryUserPermissionList, + RepositoryUserTransitivePermission) from endpoints.api.superuser import (SuperUserLogs, SuperUserList, SuperUserManagement, SuperUserSendRecoveryEmail, ChangeLog, SuperUserOrganizationManagement, SuperUserOrganizationList) @@ -728,6 +729,58 @@ class TestTeamMemberListBuynlargeOwners(ApiTestCase): self._run_test('GET', 200, 'devtable', None) +class TestRepositoryUserTransitivePermissionA2o9PublicPublicrepo(ApiTestCase): + def setUp(self): + ApiTestCase.setUp(self) + self._set_url(RepositoryUserTransitivePermission, username="A2O9", repository="public/publicrepo") + + def test_get_anonymous(self): + self._run_test('GET', 401, None, None) + + def test_get_freshuser(self): + self._run_test('GET', 403, 'freshuser', None) + + def test_get_reader(self): + self._run_test('GET', 403, 'reader', None) + + def test_get_devtable(self): + self._run_test('GET', 403, 'devtable', None) + +class TestRepositoryUserTransitivePermissionA2o9DevtableShared(ApiTestCase): + def setUp(self): + ApiTestCase.setUp(self) + self._set_url(RepositoryUserTransitivePermission, username="A2O9", repository="devtable/shared") + + def test_get_anonymous(self): + self._run_test('GET', 401, None, None) + + def test_get_freshuser(self): + self._run_test('GET', 403, 'freshuser', None) + + def test_get_reader(self): + self._run_test('GET', 403, 'reader', None) + + def test_get_devtable(self): + self._run_test('GET', 404, 'devtable', None) + +class TestRepositoryUserTransitivePermissionA2o9BuynlargeOrgrepo(ApiTestCase): + def setUp(self): + ApiTestCase.setUp(self) + self._set_url(RepositoryUserTransitivePermission, username="A2O9", repository="buynlarge/orgrepo") + + def test_get_anonymous(self): + self._run_test('GET', 401, None, None) + + def test_get_freshuser(self): + self._run_test('GET', 403, 'freshuser', None) + + def test_get_reader(self): + self._run_test('GET', 403, 'reader', None) + + def test_get_devtable(self): + self._run_test('GET', 404, 'devtable', None) + + class TestRepositoryUserPermissionA2o9PublicPublicrepo(ApiTestCase): def setUp(self): ApiTestCase.setUp(self)