Merge pull request #189 from coreos-inc/ultrauser
Delegated superuser API access
This commit is contained in:
commit
d7631f1b22
5 changed files with 77 additions and 8 deletions
|
@ -56,7 +56,6 @@ SCOPE_MAX_USER_ROLES.update({
|
||||||
scopes.DIRECT_LOGIN: 'admin',
|
scopes.DIRECT_LOGIN: 'admin',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def repository_read_grant(namespace, repository):
|
def repository_read_grant(namespace, repository):
|
||||||
return _RepositoryNeed(namespace, repository, 'read')
|
return _RepositoryNeed(namespace, repository, 'read')
|
||||||
|
|
||||||
|
@ -105,16 +104,14 @@ class QuayDeferredPermissionUser(Identity):
|
||||||
|
|
||||||
def can(self, permission):
|
def can(self, permission):
|
||||||
if not self._permissions_loaded:
|
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)
|
user_object = self._user_object or model.get_user_by_uuid(self.id)
|
||||||
if user_object is None:
|
if user_object is None:
|
||||||
return super(QuayDeferredPermissionUser, self).can(permission)
|
return super(QuayDeferredPermissionUser, self).can(permission)
|
||||||
|
|
||||||
if user_object is None:
|
if ((scopes.SUPERUSER in self._scope_set or scopes.DIRECT_LOGIN in self._scope_set) and
|
||||||
return super(QuayDeferredPermissionUser, self).can(permission)
|
superusers.is_superuser(user_object.username)):
|
||||||
|
logger.debug('Adding superuser to user: %s', user_object.username)
|
||||||
# Add the superuser need, if applicable.
|
|
||||||
if superusers.is_superuser(user_object.username):
|
|
||||||
self.provides.add(_SuperUserNeed())
|
self.provides.add(_SuperUserNeed())
|
||||||
|
|
||||||
# Add the user specific permissions, only for non-oauth permission
|
# Add the user specific permissions, only for non-oauth permission
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
import features
|
||||||
|
|
||||||
Scope = namedtuple('scope', ['scope', 'icon', 'dangerous', 'title', 'description'])
|
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. '
|
description=('This scope should not be available to OAuth applications. '
|
||||||
'Never approve a request for this scope!'))
|
'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,
|
ALL_SCOPES = {scope.scope:scope for scope in (READ_REPO, WRITE_REPO, ADMIN_REPO, CREATE_REPO,
|
||||||
READ_USER, ORG_ADMIN)}
|
READ_USER, ORG_ADMIN)}
|
||||||
|
@ -73,6 +82,9 @@ IMPLIED_SCOPES = {
|
||||||
None: set(),
|
None: set(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if features.SUPER_USERS:
|
||||||
|
ALL_SCOPES[SUPERUSER.scope] = SUPERUSER
|
||||||
|
IMPLIED_SCOPES[SUPERUSER] = {SUPERUSER}
|
||||||
|
|
||||||
def scopes_from_scope_string(scopes):
|
def scopes_from_scope_string(scopes):
|
||||||
if not scopes:
|
if not scopes:
|
||||||
|
|
|
@ -308,6 +308,10 @@ def require_fresh_login(func):
|
||||||
if not user:
|
if not user:
|
||||||
raise Unauthorized()
|
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)
|
logger.debug('Checking fresh login for user %s', user.username)
|
||||||
|
|
||||||
last_login = session.get('login_time', datetime.datetime.min)
|
last_login = session.get('login_time', datetime.datetime.min)
|
||||||
|
|
|
@ -19,6 +19,7 @@ from endpoints.api.logs import get_logs
|
||||||
from data import model
|
from data import model
|
||||||
from auth.permissions import SuperUserPermission
|
from auth.permissions import SuperUserPermission
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
|
from auth import scopes
|
||||||
from util.useremails import send_confirmation_email, send_recovery_email
|
from util.useremails import send_confirmation_email, send_recovery_email
|
||||||
|
|
||||||
import features
|
import features
|
||||||
|
@ -42,6 +43,7 @@ class SuperUserGetLogsForService(ApiResource):
|
||||||
@require_fresh_login
|
@require_fresh_login
|
||||||
@verify_not_prod
|
@verify_not_prod
|
||||||
@nickname('getSystemLogs')
|
@nickname('getSystemLogs')
|
||||||
|
@require_scope(scopes.SUPERUSER)
|
||||||
def get(self, service):
|
def get(self, service):
|
||||||
""" Returns the logs for the specific service. """
|
""" Returns the logs for the specific service. """
|
||||||
if SuperUserPermission().can():
|
if SuperUserPermission().can():
|
||||||
|
@ -70,6 +72,7 @@ class SuperUserSystemLogServices(ApiResource):
|
||||||
@require_fresh_login
|
@require_fresh_login
|
||||||
@verify_not_prod
|
@verify_not_prod
|
||||||
@nickname('listSystemLogServices')
|
@nickname('listSystemLogServices')
|
||||||
|
@require_scope(scopes.SUPERUSER)
|
||||||
def get(self):
|
def get(self):
|
||||||
""" List the system logs for the current system. """
|
""" List the system logs for the current system. """
|
||||||
if SuperUserPermission().can():
|
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('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('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)
|
@query_param('performer', 'Username for which to filter logs.', type=str)
|
||||||
|
@require_scope(scopes.SUPERUSER)
|
||||||
def get(self, args):
|
def get(self, args):
|
||||||
""" List the usage logs for the current system. """
|
""" List the usage logs for the current system. """
|
||||||
if SuperUserPermission().can():
|
if SuperUserPermission().can():
|
||||||
|
@ -129,6 +133,7 @@ class ChangeLog(ApiResource):
|
||||||
@require_fresh_login
|
@require_fresh_login
|
||||||
@verify_not_prod
|
@verify_not_prod
|
||||||
@nickname('getChangeLog')
|
@nickname('getChangeLog')
|
||||||
|
@require_scope(scopes.SUPERUSER)
|
||||||
def get(self):
|
def get(self):
|
||||||
""" Returns the change log for this installation. """
|
""" Returns the change log for this installation. """
|
||||||
if SuperUserPermission().can():
|
if SuperUserPermission().can():
|
||||||
|
@ -149,6 +154,7 @@ class SuperUserOrganizationList(ApiResource):
|
||||||
@require_fresh_login
|
@require_fresh_login
|
||||||
@verify_not_prod
|
@verify_not_prod
|
||||||
@nickname('listAllOrganizations')
|
@nickname('listAllOrganizations')
|
||||||
|
@require_scope(scopes.SUPERUSER)
|
||||||
def get(self):
|
def get(self):
|
||||||
""" Returns a list of all organizations in the system. """
|
""" Returns a list of all organizations in the system. """
|
||||||
if SuperUserPermission().can():
|
if SuperUserPermission().can():
|
||||||
|
@ -187,6 +193,7 @@ class SuperUserList(ApiResource):
|
||||||
@require_fresh_login
|
@require_fresh_login
|
||||||
@verify_not_prod
|
@verify_not_prod
|
||||||
@nickname('listAllUsers')
|
@nickname('listAllUsers')
|
||||||
|
@require_scope(scopes.SUPERUSER)
|
||||||
def get(self):
|
def get(self):
|
||||||
""" Returns a list of all users in the system. """
|
""" Returns a list of all users in the system. """
|
||||||
if SuperUserPermission().can():
|
if SuperUserPermission().can():
|
||||||
|
@ -202,6 +209,7 @@ class SuperUserList(ApiResource):
|
||||||
@verify_not_prod
|
@verify_not_prod
|
||||||
@nickname('createInstallUser')
|
@nickname('createInstallUser')
|
||||||
@validate_json_request('CreateInstallUser')
|
@validate_json_request('CreateInstallUser')
|
||||||
|
@require_scope(scopes.SUPERUSER)
|
||||||
def post(self):
|
def post(self):
|
||||||
""" Creates a new user. """
|
""" Creates a new user. """
|
||||||
user_information = request.get_json()
|
user_information = request.get_json()
|
||||||
|
@ -239,6 +247,7 @@ class SuperUserSendRecoveryEmail(ApiResource):
|
||||||
@require_fresh_login
|
@require_fresh_login
|
||||||
@verify_not_prod
|
@verify_not_prod
|
||||||
@nickname('sendInstallUserRecoveryEmail')
|
@nickname('sendInstallUserRecoveryEmail')
|
||||||
|
@require_scope(scopes.SUPERUSER)
|
||||||
def post(self, username):
|
def post(self, username):
|
||||||
if SuperUserPermission().can():
|
if SuperUserPermission().can():
|
||||||
user = model.get_nonrobot_user(username)
|
user = model.get_nonrobot_user(username)
|
||||||
|
@ -288,6 +297,7 @@ class SuperUserManagement(ApiResource):
|
||||||
@require_fresh_login
|
@require_fresh_login
|
||||||
@verify_not_prod
|
@verify_not_prod
|
||||||
@nickname('getInstallUser')
|
@nickname('getInstallUser')
|
||||||
|
@require_scope(scopes.SUPERUSER)
|
||||||
def get(self, username):
|
def get(self, username):
|
||||||
""" Returns information about the specified user. """
|
""" Returns information about the specified user. """
|
||||||
if SuperUserPermission().can():
|
if SuperUserPermission().can():
|
||||||
|
@ -302,6 +312,7 @@ class SuperUserManagement(ApiResource):
|
||||||
@require_fresh_login
|
@require_fresh_login
|
||||||
@verify_not_prod
|
@verify_not_prod
|
||||||
@nickname('deleteInstallUser')
|
@nickname('deleteInstallUser')
|
||||||
|
@require_scope(scopes.SUPERUSER)
|
||||||
def delete(self, username):
|
def delete(self, username):
|
||||||
""" Deletes the specified user. """
|
""" Deletes the specified user. """
|
||||||
if SuperUserPermission().can():
|
if SuperUserPermission().can():
|
||||||
|
@ -321,6 +332,7 @@ class SuperUserManagement(ApiResource):
|
||||||
@verify_not_prod
|
@verify_not_prod
|
||||||
@nickname('changeInstallUser')
|
@nickname('changeInstallUser')
|
||||||
@validate_json_request('UpdateUser')
|
@validate_json_request('UpdateUser')
|
||||||
|
@require_scope(scopes.SUPERUSER)
|
||||||
def put(self, username):
|
def put(self, username):
|
||||||
""" Updates information about the specified user. """
|
""" Updates information about the specified user. """
|
||||||
if SuperUserPermission().can():
|
if SuperUserPermission().can():
|
||||||
|
@ -371,6 +383,7 @@ class SuperUserOrganizationManagement(ApiResource):
|
||||||
@require_fresh_login
|
@require_fresh_login
|
||||||
@verify_not_prod
|
@verify_not_prod
|
||||||
@nickname('deleteOrganization')
|
@nickname('deleteOrganization')
|
||||||
|
@require_scope(scopes.SUPERUSER)
|
||||||
def delete(self, name):
|
def delete(self, name):
|
||||||
""" Deletes the specified organization. """
|
""" Deletes the specified organization. """
|
||||||
if SuperUserPermission().can():
|
if SuperUserPermission().can():
|
||||||
|
@ -385,6 +398,7 @@ class SuperUserOrganizationManagement(ApiResource):
|
||||||
@verify_not_prod
|
@verify_not_prod
|
||||||
@nickname('changeOrganization')
|
@nickname('changeOrganization')
|
||||||
@validate_json_request('UpdateOrg')
|
@validate_json_request('UpdateOrg')
|
||||||
|
@require_scope(scopes.SUPERUSER)
|
||||||
def put(self, name):
|
def put(self, name):
|
||||||
""" Updates information about the specified user. """
|
""" Updates information about the specified user. """
|
||||||
if SuperUserPermission().can():
|
if SuperUserPermission().can():
|
||||||
|
|
42
test/test_permissions.py
Normal file
42
test/test_permissions.py
Normal file
|
@ -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)
|
Reference in a new issue