diff --git a/auth/permissions.py b/auth/permissions.py index e53dd6fe8..91a8176d0 100644 --- a/auth/permissions.py +++ b/auth/permissions.py @@ -56,7 +56,6 @@ SCOPE_MAX_USER_ROLES.update({ scopes.DIRECT_LOGIN: 'admin', }) - def repository_read_grant(namespace, repository): return _RepositoryNeed(namespace, repository, 'read') @@ -105,16 +104,14 @@ class QuayDeferredPermissionUser(Identity): def can(self, permission): if not self._permissions_loaded: - logger.debug('Loading user permissions after deferring.') + logger.debug('Loading user permissions after deferring for: %s', self.id) user_object = self._user_object or model.get_user_by_uuid(self.id) if user_object is None: return super(QuayDeferredPermissionUser, self).can(permission) - if user_object is None: - return super(QuayDeferredPermissionUser, self).can(permission) - - # Add the superuser need, if applicable. - if superusers.is_superuser(user_object.username): + 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 diff --git a/auth/scopes.py b/auth/scopes.py index 128f99b98..23587d1b3 100644 --- a/auth/scopes.py +++ b/auth/scopes.py @@ -1,5 +1,5 @@ from collections import namedtuple - +import features Scope = namedtuple('scope', ['scope', 'icon', 'dangerous', 'title', 'description']) @@ -59,6 +59,15 @@ DIRECT_LOGIN = Scope(scope='direct_user_login', description=('This scope should not be available to OAuth applications. ' 'Never approve a request for this scope!')) +SUPERUSER = Scope(scope='super:user', + icon='fa-street-view', + dangerous=True, + title='Super User Access', + description=('This application will be able to administer your installation ' + 'including managing users, managing organizations and other ' + 'features found in the superuser panel. You should have ' + 'absolute trust in the requesting application before granting this ' + 'permission.')) ALL_SCOPES = {scope.scope:scope for scope in (READ_REPO, WRITE_REPO, ADMIN_REPO, CREATE_REPO, READ_USER, ORG_ADMIN)} @@ -73,6 +82,9 @@ IMPLIED_SCOPES = { None: set(), } +if features.SUPER_USERS: + ALL_SCOPES[SUPERUSER.scope] = SUPERUSER + IMPLIED_SCOPES[SUPERUSER] = {SUPERUSER} def scopes_from_scope_string(scopes): if not scopes: diff --git a/endpoints/api/__init__.py b/endpoints/api/__init__.py index 9a235d6a0..df122fdb0 100644 --- a/endpoints/api/__init__.py +++ b/endpoints/api/__init__.py @@ -308,6 +308,10 @@ def require_fresh_login(func): if not user: raise Unauthorized() + oauth_token = get_validated_oauth_token() + if oauth_token: + return func(*args, **kwargs) + logger.debug('Checking fresh login for user %s', user.username) last_login = session.get('login_time', datetime.datetime.min) diff --git a/endpoints/api/superuser.py b/endpoints/api/superuser.py index 4f70f0761..2ec28c56a 100644 --- a/endpoints/api/superuser.py +++ b/endpoints/api/superuser.py @@ -19,6 +19,7 @@ from endpoints.api.logs import get_logs from data import model from auth.permissions import SuperUserPermission from auth.auth_context import get_authenticated_user +from auth import scopes from util.useremails import send_confirmation_email, send_recovery_email import features @@ -42,6 +43,7 @@ class SuperUserGetLogsForService(ApiResource): @require_fresh_login @verify_not_prod @nickname('getSystemLogs') + @require_scope(scopes.SUPERUSER) def get(self, service): """ Returns the logs for the specific service. """ if SuperUserPermission().can(): @@ -70,6 +72,7 @@ class SuperUserSystemLogServices(ApiResource): @require_fresh_login @verify_not_prod @nickname('listSystemLogServices') + @require_scope(scopes.SUPERUSER) def get(self): """ List the system logs for the current system. """ if SuperUserPermission().can(): @@ -93,6 +96,7 @@ class SuperUserLogs(ApiResource): @query_param('starttime', 'Earliest time from which to get logs. (%m/%d/%Y %Z)', type=str) @query_param('endtime', 'Latest time to which to get logs. (%m/%d/%Y %Z)', type=str) @query_param('performer', 'Username for which to filter logs.', type=str) + @require_scope(scopes.SUPERUSER) def get(self, args): """ List the usage logs for the current system. """ if SuperUserPermission().can(): @@ -129,6 +133,7 @@ class ChangeLog(ApiResource): @require_fresh_login @verify_not_prod @nickname('getChangeLog') + @require_scope(scopes.SUPERUSER) def get(self): """ Returns the change log for this installation. """ if SuperUserPermission().can(): @@ -149,6 +154,7 @@ class SuperUserOrganizationList(ApiResource): @require_fresh_login @verify_not_prod @nickname('listAllOrganizations') + @require_scope(scopes.SUPERUSER) def get(self): """ Returns a list of all organizations in the system. """ if SuperUserPermission().can(): @@ -187,6 +193,7 @@ class SuperUserList(ApiResource): @require_fresh_login @verify_not_prod @nickname('listAllUsers') + @require_scope(scopes.SUPERUSER) def get(self): """ Returns a list of all users in the system. """ if SuperUserPermission().can(): @@ -202,6 +209,7 @@ class SuperUserList(ApiResource): @verify_not_prod @nickname('createInstallUser') @validate_json_request('CreateInstallUser') + @require_scope(scopes.SUPERUSER) def post(self): """ Creates a new user. """ user_information = request.get_json() @@ -239,6 +247,7 @@ class SuperUserSendRecoveryEmail(ApiResource): @require_fresh_login @verify_not_prod @nickname('sendInstallUserRecoveryEmail') + @require_scope(scopes.SUPERUSER) def post(self, username): if SuperUserPermission().can(): user = model.get_nonrobot_user(username) @@ -288,6 +297,7 @@ class SuperUserManagement(ApiResource): @require_fresh_login @verify_not_prod @nickname('getInstallUser') + @require_scope(scopes.SUPERUSER) def get(self, username): """ Returns information about the specified user. """ if SuperUserPermission().can(): @@ -302,6 +312,7 @@ class SuperUserManagement(ApiResource): @require_fresh_login @verify_not_prod @nickname('deleteInstallUser') + @require_scope(scopes.SUPERUSER) def delete(self, username): """ Deletes the specified user. """ if SuperUserPermission().can(): @@ -321,6 +332,7 @@ class SuperUserManagement(ApiResource): @verify_not_prod @nickname('changeInstallUser') @validate_json_request('UpdateUser') + @require_scope(scopes.SUPERUSER) def put(self, username): """ Updates information about the specified user. """ if SuperUserPermission().can(): @@ -371,6 +383,7 @@ class SuperUserOrganizationManagement(ApiResource): @require_fresh_login @verify_not_prod @nickname('deleteOrganization') + @require_scope(scopes.SUPERUSER) def delete(self, name): """ Deletes the specified organization. """ if SuperUserPermission().can(): @@ -385,6 +398,7 @@ class SuperUserOrganizationManagement(ApiResource): @verify_not_prod @nickname('changeOrganization') @validate_json_request('UpdateOrg') + @require_scope(scopes.SUPERUSER) def put(self, name): """ Updates information about the specified user. """ if SuperUserPermission().can(): diff --git a/test/test_permissions.py b/test/test_permissions.py new file mode 100644 index 000000000..b01c1ac3a --- /dev/null +++ b/test/test_permissions.py @@ -0,0 +1,42 @@ +import unittest + +from app import app + +from data import model +from auth import scopes +from auth.permissions import SuperUserPermission, QuayDeferredPermissionUser +from initdb import setup_database_for_testing, finished_database_for_testing + + +SUPER_USERNAME = 'devtable' +UNSUPER_USERNAME = 'freshuser' + + +class TestSuperUserOps(unittest.TestCase): + def setUp(self): + setup_database_for_testing(self) + self._su = model.get_user(SUPER_USERNAME) + self._normie = model.get_user(UNSUPER_USERNAME) + + def tearDown(self): + finished_database_for_testing(self) + + def test_superuser_matrix(self): + import logging + logging.basicConfig(level=logging.DEBUG) + + test_cases = [ + (self._su, {scopes.SUPERUSER}, True), + (self._su, {scopes.DIRECT_LOGIN}, True), + (self._su, {scopes.READ_USER, scopes.SUPERUSER}, True), + (self._su, {scopes.READ_USER}, False), + (self._normie, {scopes.SUPERUSER}, False), + (self._normie, {scopes.DIRECT_LOGIN}, False), + (self._normie, {scopes.READ_USER, scopes.SUPERUSER}, False), + (self._normie, {scopes.READ_USER}, False), + ] + + for user_obj, scope_set, expected in test_cases: + perm_user = QuayDeferredPermissionUser.for_user(user_obj, scope_set) + has_su = perm_user.can(SuperUserPermission()) + self.assertEquals(has_su, expected)