From 87efcb9e3d92dc2c449162ee93a133f24e6ed618 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Sun, 28 Jun 2015 11:22:34 +0300 Subject: [PATCH 1/2] Delegated superuser API access Add a new scope for SUPERUSER that allows delegated access to the superuser endpoints. CA needs this so they can programmatically create and remove users. --- auth/permissions.py | 13 ++++++++++--- auth/scopes.py | 14 +++++++++++++- endpoints/api/__init__.py | 4 ++++ endpoints/api/superuser.py | 14 ++++++++++++++ 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/auth/permissions.py b/auth/permissions.py index e53dd6fe8..435a29523 100644 --- a/auth/permissions.py +++ b/auth/permissions.py @@ -27,7 +27,7 @@ _SuperUserNeed = partial(namedtuple('superuserneed', ['type']), 'superuser') REPO_ROLES = [None, 'read', 'write', 'admin'] TEAM_ROLES = [None, 'member', 'creator', 'admin'] -USER_ROLES = [None, 'read', 'admin'] +USER_ROLES = [None, 'read', 'admin', 'superuser'] TEAM_REPO_ROLES = { 'admin': 'admin', @@ -54,6 +54,7 @@ SCOPE_MAX_USER_ROLES = defaultdict(lambda: None) SCOPE_MAX_USER_ROLES.update({ scopes.READ_USER: 'read', scopes.DIRECT_LOGIN: 'admin', + scopes.SUPERUSER: 'superuser', }) @@ -113,8 +114,14 @@ class QuayDeferredPermissionUser(Identity): if user_object is None: return super(QuayDeferredPermissionUser, self).can(permission) - # Add the superuser need, if applicable. - if superusers.is_superuser(user_object.username): + # Add the superuser need, if applicable: + # - If the user's role is an admin (direct login) and they are a superuser + # - If the user has granted the superuser scope + superuser_role = self._user_role_for_scopes('superuser') + + if superuser_role == 'admin' and superusers.is_superuser(user_object.username): + self.provides.add(_SuperUserNeed()) + elif superuser_role == 'superuser': 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(): From 7b470237a18b797b6172ccf9f4b931b8ba8e364d Mon Sep 17 00:00:00 2001 From: Jake Moshenko Date: Tue, 30 Jun 2015 10:58:10 -0400 Subject: [PATCH 2/2] The superuser capability does not require the idea of ordinality since it is a binary permission. --- auth/permissions.py | 20 +++++-------------- test/test_permissions.py | 42 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 15 deletions(-) create mode 100644 test/test_permissions.py diff --git a/auth/permissions.py b/auth/permissions.py index 435a29523..91a8176d0 100644 --- a/auth/permissions.py +++ b/auth/permissions.py @@ -27,7 +27,7 @@ _SuperUserNeed = partial(namedtuple('superuserneed', ['type']), 'superuser') REPO_ROLES = [None, 'read', 'write', 'admin'] TEAM_ROLES = [None, 'member', 'creator', 'admin'] -USER_ROLES = [None, 'read', 'admin', 'superuser'] +USER_ROLES = [None, 'read', 'admin'] TEAM_REPO_ROLES = { 'admin': 'admin', @@ -54,10 +54,8 @@ SCOPE_MAX_USER_ROLES = defaultdict(lambda: None) SCOPE_MAX_USER_ROLES.update({ scopes.READ_USER: 'read', scopes.DIRECT_LOGIN: 'admin', - scopes.SUPERUSER: 'superuser', }) - def repository_read_grant(namespace, repository): return _RepositoryNeed(namespace, repository, 'read') @@ -106,22 +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 the user's role is an admin (direct login) and they are a superuser - # - If the user has granted the superuser scope - superuser_role = self._user_role_for_scopes('superuser') - - if superuser_role == 'admin' and superusers.is_superuser(user_object.username): - self.provides.add(_SuperUserNeed()) - elif superuser_role == 'superuser': + 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/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)