Add ability for super users to take ownership of namespaces

Fixes #1395
This commit is contained in:
Joseph Schorr 2016-06-07 18:12:11 -04:00
parent f75949d533
commit 20816804e5
14 changed files with 280 additions and 94 deletions

View file

@ -0,0 +1,26 @@
"""Add take_ownership log entry kind
Revision ID: 0f17d94d11eb
Revises: a3ba52d02dec
Create Date: 2016-06-07 17:22:20.438873
"""
# revision identifiers, used by Alembic.
revision = '0f17d94d11eb'
down_revision = 'a3ba52d02dec'
from alembic import op
def upgrade(tables):
op.bulk_insert(tables.logentrykind,
[
{'name':'take_ownership'},
])
def downgrade(tables):
op.execute(
(tables.logentrykind.delete()
.where(tables.logentrykind.c.name == op.inline_literal('take_ownership')))
)

View file

@ -136,3 +136,11 @@ def get_all_repo_users_transitive_via_teams(namespace_name, repository_name):
def get_organizations(): def get_organizations():
return User.select().where(User.organization == True, User.robot == False) return User.select().where(User.organization == True, User.robot == False)
def add_user_as_admin(user_obj, org_obj):
try:
admin_role = TeamRole.get(name='admin')
admin_team = Team.select().where(Team.role == admin_role, Team.organization == org_obj).get()
team.add_user_to_team(user_obj, admin_team)
except team.UserAlreadyInTeam:
pass

View file

@ -1,4 +1,4 @@
from data.database import Team, TeamMember, TeamRole, User, TeamMemberInvite, Repository from data.database import Team, TeamMember, TeamRole, User, TeamMemberInvite
from data.model import (DataModelException, InvalidTeamException, UserAlreadyInTeam, from data.model import (DataModelException, InvalidTeamException, UserAlreadyInTeam,
InvalidTeamMemberException, user, _basequery) InvalidTeamMemberException, user, _basequery)
from util.validation import validate_username from util.validation import validate_username

View file

@ -422,6 +422,54 @@ class SuperUserManagement(ApiResource):
abort(403) abort(403)
@resource('/v1/superuser/takeownership/<namespace>')
@path_param('namespace', 'The namespace of the user or organization being managed')
@internal_only
@show_if(features.SUPER_USERS)
class SuperUserTakeOwnership(ApiResource):
""" Resource for a superuser to take ownership of a namespace. """
@require_fresh_login
@verify_not_prod
@nickname('takeOwnership')
@require_scope(scopes.SUPERUSER)
def post(self, namespace):
""" Takes ownership of the specified organization or user. """
if SuperUserPermission().can():
# Disallow for superusers.
if superusers.is_superuser(namespace):
abort(400)
entity = model.user.get_user_or_org(namespace)
if entity is None:
abort(404)
authed_user = get_authenticated_user()
was_user = not entity.organization
if entity.organization:
# Add the superuser as an admin to the owners team of the org.
model.organization.add_user_as_admin(authed_user, entity)
else:
# If the entity is a user, convert it to an organization and add the current superuser
# as the admin.
model.organization.convert_user_to_organization(entity, get_authenticated_user())
# Log the change.
log_metadata = {
'entity_id': entity.id,
'namespace': namespace,
'was_user': was_user,
'superuser': authed_user.username,
}
log_action('take_ownership', authed_user.username, log_metadata)
return jsonify({
'namespace': namespace
})
abort(403)
@resource('/v1/superuser/organizations/<name>') @resource('/v1/superuser/organizations/<name>')
@path_param('name', 'The name of the organizaton being managed') @path_param('name', 'The name of the organizaton being managed')
@internal_only @internal_only

View file

@ -343,6 +343,8 @@ def initialize_database():
LogEntryKind.create(name='service_key_extend') LogEntryKind.create(name='service_key_extend')
LogEntryKind.create(name='service_key_rotate') LogEntryKind.create(name='service_key_rotate')
LogEntryKind.create(name='take_ownership')
ImageStorageLocation.create(name='local_eu') ImageStorageLocation.create(name='local_eu')
ImageStorageLocation.create(name='local_us') ImageStorageLocation.create(name='local_us')

View file

@ -49,3 +49,11 @@
.super-user .input-util { .super-user .input-util {
margin-top: 10px; margin-top: 10px;
} }
.super-user .take-ownership-dialog .avatar {
margin-left: 6px;
}
.super-user .take-ownership-dialog .co-alert {
margin-top: 20px;
}

View file

@ -223,6 +223,14 @@ angular.module('quay').directive('logsView', function () {
'service_key_extend': 'Change of expiration of service key {kid} from {old_expiration_date} to {expiration_date}', 'service_key_extend': 'Change of expiration of service key {kid} from {old_expiration_date} to {expiration_date}',
'service_key_rotate': 'Automatic rotation of service key {kid} by {user_agent}', 'service_key_rotate': 'Automatic rotation of service key {kid} by {user_agent}',
'take_ownership': function(metadata) {
if (metadata.was_user) {
return 'Superuser {superuser} took ownership of user namespace {namespace}';
} else {
return 'Superuser {superuser} took ownership of organization {namespace}';
}
},
// Note: These are deprecated. // Note: These are deprecated.
'add_repo_webhook': 'Add webhook in repository {repo}', 'add_repo_webhook': 'Add webhook in repository {repo}',
'delete_repo_webhook': 'Delete webhook in repository {repo}' 'delete_repo_webhook': 'Delete webhook in repository {repo}'
@ -279,6 +287,7 @@ angular.module('quay').directive('logsView', function () {
'service_key_delete': 'Delete Service Key', 'service_key_delete': 'Delete Service Key',
'service_key_extend': 'Extend Service Key Expiration', 'service_key_extend': 'Extend Service Key Expiration',
'service_key_rotate': 'Automatic rotation of Service Key', 'service_key_rotate': 'Automatic rotation of Service Key',
'take_ownership': 'Take Namespace Ownership',
// Note: these are deprecated. // Note: these are deprecated.
'add_repo_webhook': 'Add webhook', 'add_repo_webhook': 'Add webhook',

View file

@ -10,7 +10,7 @@
}) })
}]); }]);
function SuperuserCtrl($scope, $timeout, ApiService, Features, UserService, ContainerService, AngularPollChannel, CoreDialog) { function SuperuserCtrl($scope, $timeout, $location, ApiService, Features, UserService, ContainerService, AngularPollChannel, CoreDialog) {
if (!Features.SUPER_USERS) { if (!Features.SUPER_USERS) {
return; return;
} }
@ -32,6 +32,7 @@
$scope.dashboardActive = false; $scope.dashboardActive = false;
$scope.currentConfig = null; $scope.currentConfig = null;
$scope.serviceKeysActive = false; $scope.serviceKeysActive = false;
$scope.takeOwnershipInfo = null;
$scope.setDashboardActive = function(active) { $scope.setDashboardActive = function(active) {
$scope.dashboardActive = active; $scope.dashboardActive = active;
@ -261,6 +262,25 @@
}); });
}; };
$scope.askTakeOwnership = function(entity, is_org) {
$scope.takeOwnershipInfo = {
'entity': entity,
'is_org': is_org
};
};
$scope.takeOwnership = function(info, callback) {
var errorDisplay = ApiService.errorDisplay('Could not take ownership of namespace', callback);
var params = {
'namespace': info.entity.username || info.entity.name
};
ApiService.takeOwnership(null, params).then(function() {
callback(true);
$location.path('/organization/' + params.namespace);
}, errorDisplay)
};
$scope.askDisableUser = function(user) { $scope.askDisableUser = function(user) {
var message = 'Are you sure you want to disable this user? ' + var message = 'Are you sure you want to disable this user? ' +
'They will be unable to login, pull or push.' 'They will be unable to login, pull or push.'

View file

@ -138,6 +138,9 @@
<span class="cor-option" option-click="askDeleteOrganization(current_org)"> <span class="cor-option" option-click="askDeleteOrganization(current_org)">
<i class="fa fa-times"></i> Delete Organization <i class="fa fa-times"></i> Delete Organization
</span> </span>
<span class="cor-option" option-click="askTakeOwnership(current_org, true)">
<i class="fa fa-bolt"></i> Take Ownership
</span>
</span> </span>
</td> </td>
</tr> </tr>
@ -223,6 +226,10 @@
<span class="cor-option" option-click="askDisableUser(current_user)"> <span class="cor-option" option-click="askDisableUser(current_user)">
<i class="fa" ng-class="current_user.enabled ? 'fa-circle-o' : 'fa-check-circle-o'"></i> <span ng-if="current_user.enabled">Disable</span> <span ng-if="!current_user.enabled">Enable</span> User <i class="fa" ng-class="current_user.enabled ? 'fa-circle-o' : 'fa-check-circle-o'"></i> <span ng-if="current_user.enabled">Disable</span> <span ng-if="!current_user.enabled">Enable</span> User
</span> </span>
<span class="cor-option" option-click="askTakeOwnership(current_user, false)"
ng-if="user.username != current_user.username && !current_user.super_user">
<i class="fa fa-bolt"></i> Take Ownership
</span>
</span> </span>
</td> </td>
</tr> </tr>
@ -233,6 +240,21 @@
</div> <!-- /cor-tab-content --> </div> <!-- /cor-tab-content -->
</div> <!-- /cor-tab-panel --> </div> <!-- /cor-tab-panel -->
<!-- Take ownership dialog -->
<div class="cor-confirm-dialog take-ownership-dialog"
dialog-context="takeOwnershipInfo"
dialog-action="takeOwnership(info, callback)"
dialog-title="Take Ownership"
dialog-action-title="Take Ownership">
Are you sure you want to take ownership of
<span ng-if="takeOwnershipInfo.is_org">organization <span class="avatar" data="takeOwnershipInfo.entity.avatar" size="16"></span> {{ takeOwnershipInfo.entity.name }}?</span>
<span ng-if="!takeOwnershipInfo.is_org">user namespace <span class="avatar" data="takeOwnershipInfo.entity.avatar" size="16"></span> {{ takeOwnershipInfo .entity.username }}?</span>
<div class="co-alert co-alert-warning" ng-if="!takeOwnershipInfo.is_org">
Note: This will convert the user namespace into an organization. <strong>The user will no longer be able to login to this account.</strong>
</div>
</div>
<!-- Modal message dialog --> <!-- Modal message dialog -->
<div class="co-dialog modal fade" id="confirmDeleteUserModal"> <div class="co-dialog modal fade" id="confirmDeleteUserModal">
<div class="modal-dialog"> <div class="modal-dialog">

Binary file not shown.

View file

@ -9,7 +9,7 @@ class assert_action_logged(object):
self.existing_count = 0 self.existing_count = 0
def _get_log_count(self): def _get_log_count(self):
return LogEntry.select(LogEntry.kind == LogEntryKind.get(name=self.log_kind)).count() return LogEntry.select().where(LogEntry.kind == LogEntryKind.get(name=self.log_kind)).count()
def __enter__(self): def __enter__(self):
self.existing_count = self._get_log_count() self.existing_count = self._get_log_count()

View file

@ -49,7 +49,8 @@ from endpoints.api.superuser import (SuperUserLogs, SuperUserList, SuperUserMana
SuperUserSendRecoveryEmail, ChangeLog, SuperUserSendRecoveryEmail, ChangeLog,
SuperUserOrganizationManagement, SuperUserOrganizationList, SuperUserOrganizationManagement, SuperUserOrganizationList,
SuperUserAggregateLogs, SuperUserServiceKeyManagement, SuperUserAggregateLogs, SuperUserServiceKeyManagement,
SuperUserServiceKey, SuperUserServiceKeyApproval) SuperUserServiceKey, SuperUserServiceKeyApproval,
SuperUserTakeOwnership)
from endpoints.api.secscan import RepositoryImageSecurity from endpoints.api.secscan import RepositoryImageSecurity
@ -3912,6 +3913,24 @@ class TestSuperUserSendRecoveryEmail(ApiTestCase):
self._run_test('POST', 404, 'devtable', None) self._run_test('POST', 404, 'devtable', None)
class TestSuperUserTakeOwnership(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)
self._set_url(SuperUserTakeOwnership, namespace='invalidnamespace')
def test_post_anonymous(self):
self._run_test('POST', 401, None, {})
def test_post_freshuser(self):
self._run_test('POST', 403, 'freshuser', {})
def test_post_reader(self):
self._run_test('POST', 403, 'reader', {})
def test_post_devtable(self):
self._run_test('POST', 404, 'devtable', {})
class TestSuperUserServiceKeyApproval(ApiTestCase): class TestSuperUserServiceKeyApproval(ApiTestCase):
def setUp(self): def setUp(self):
ApiTestCase.setUp(self) ApiTestCase.setUp(self)
@ -3930,7 +3949,6 @@ class TestSuperUserServiceKeyApproval(ApiTestCase):
self._run_test('POST', 404, 'devtable', {}) self._run_test('POST', 404, 'devtable', {})
class TestSuperUserServiceKeyManagement(ApiTestCase): class TestSuperUserServiceKeyManagement(ApiTestCase):
def setUp(self): def setUp(self):
ApiTestCase.setUp(self) ApiTestCase.setUp(self)

View file

@ -24,7 +24,8 @@ from app import app, config_provider
from buildtrigger.basehandler import BuildTriggerHandler from buildtrigger.basehandler import BuildTriggerHandler
from initdb import setup_database_for_testing, finished_database_for_testing from initdb import setup_database_for_testing, finished_database_for_testing
from data import database, model from data import database, model
from data.database import RepositoryActionCount, LogEntry, LogEntryKind from data.database import RepositoryActionCount
from test.helpers import assert_action_logged
from endpoints.api.team import TeamMember, TeamMemberList, TeamMemberInvite, OrganizationTeam from endpoints.api.team import TeamMember, TeamMemberList, TeamMemberInvite, OrganizationTeam
from endpoints.api.tag import RepositoryTagImages, RepositoryTag, RevertTag, ListRepositoryTags from endpoints.api.tag import RepositoryTagImages, RepositoryTag, RevertTag, ListRepositoryTags
@ -59,7 +60,7 @@ from endpoints.api.permission import (RepositoryUserPermission, RepositoryTeamPe
RepositoryTeamPermissionList, RepositoryUserPermissionList) RepositoryTeamPermissionList, RepositoryUserPermissionList)
from endpoints.api.superuser import (SuperUserLogs, SuperUserList, SuperUserManagement, from endpoints.api.superuser import (SuperUserLogs, SuperUserList, SuperUserManagement,
SuperUserServiceKeyManagement, SuperUserServiceKey, SuperUserServiceKeyManagement, SuperUserServiceKey,
SuperUserServiceKeyApproval) SuperUserServiceKeyApproval, SuperUserTakeOwnership)
from endpoints.api.secscan import RepositoryImageSecurity from endpoints.api.secscan import RepositoryImageSecurity
from endpoints.api.suconfig import (SuperUserRegistryStatus, SuperUserConfig, SuperUserConfigFile, from endpoints.api.suconfig import (SuperUserRegistryStatus, SuperUserConfig, SuperUserConfigFile,
SuperUserCreateInitialSuperUser) SuperUserCreateInitialSuperUser)
@ -3650,13 +3651,62 @@ class TestRepositoryImageSecurity(ApiTestCase):
self.assertEquals(1, response['data']['Layer']['IndexedByVersion']) self.assertEquals(1, response['data']['Layer']['IndexedByVersion'])
class TestSuperUserTakeOwnership(ApiTestCase):
def test_take_ownership_superuser(self):
self.login(ADMIN_ACCESS_USER)
# Should fail to take ownership of a superuser.
self.postResponse(SuperUserTakeOwnership, params=dict(namespace=ADMIN_ACCESS_USER),
expected_code=400)
def test_take_ownership_invalid_namespace(self):
self.login(ADMIN_ACCESS_USER)
self.postResponse(SuperUserTakeOwnership, params=dict(namespace='invalid'),
expected_code=404)
def test_take_ownership_non_superuser(self):
self.login(READ_ACCESS_USER)
self.postResponse(SuperUserTakeOwnership, params=dict(namespace='freshuser'),
expected_code=403)
def test_take_ownership_user(self):
self.login(ADMIN_ACCESS_USER)
with assert_action_logged('take_ownership'):
# Take ownership of the read user.
self.postResponse(SuperUserTakeOwnership, params=dict(namespace=READ_ACCESS_USER))
# Ensure that the read access user is now an org, with the superuser as the owner.
reader = model.user.get_user_or_org(READ_ACCESS_USER)
self.assertTrue(reader.organization)
usernames = [admin.username for admin in model.organization.get_admin_users(reader)]
self.assertIn(ADMIN_ACCESS_USER, usernames)
def test_take_ownership_org(self):
# Create a new org with another user as owner.
public_user = model.user.get_user(PUBLIC_USER)
org = model.organization.create_organization('someorg', 'some@example.com', public_user)
# Ensure that the admin is not yet owner of the org.
usernames = [admin.username for admin in model.organization.get_admin_users(org)]
self.assertNotIn(ADMIN_ACCESS_USER, usernames)
with assert_action_logged('take_ownership'):
# Take ownership.
self.login(ADMIN_ACCESS_USER)
self.postResponse(SuperUserTakeOwnership, params=dict(namespace='someorg'))
# Ensure now in the admin users.
usernames = [admin.username for admin in model.organization.get_admin_users(org)]
self.assertIn(ADMIN_ACCESS_USER, usernames)
class TestSuperUserKeyManagement(ApiTestCase): class TestSuperUserKeyManagement(ApiTestCase):
def test_get_update_keys(self): def test_get_update_keys(self):
self.login(ADMIN_ACCESS_USER) self.login(ADMIN_ACCESS_USER)
kind = LogEntryKind.get(LogEntryKind.name == 'service_key_modify')
existing_modify = model.log.LogEntry.select().where(LogEntry.kind == kind).count()
json = self.getJsonResponse(SuperUserServiceKeyManagement) json = self.getJsonResponse(SuperUserServiceKeyManagement)
key_count = len(json['keys']) key_count = len(json['keys'])
@ -3670,81 +3720,65 @@ class TestSuperUserKeyManagement(ApiTestCase):
self.assertTrue('approval' in key) self.assertTrue('approval' in key)
self.assertTrue('metadata' in key) self.assertTrue('metadata' in key)
# Update the key's name. with assert_action_logged('service_key_modify'):
self.putJsonResponse(SuperUserServiceKey, params=dict(kid=key['kid']), # Update the key's name.
data=dict(name='somenewname')) self.putJsonResponse(SuperUserServiceKey, params=dict(kid=key['kid']),
data=dict(name='somenewname'))
# Ensure the key's name has been changed. # Ensure the key's name has been changed.
json = self.getJsonResponse(SuperUserServiceKey, params=dict(kid=key['kid'])) json = self.getJsonResponse(SuperUserServiceKey, params=dict(kid=key['kid']))
self.assertEquals('somenewname', json['name']) self.assertEquals('somenewname', json['name'])
# Ensure a log was added for the modification. with assert_action_logged('service_key_modify'):
kind = LogEntryKind.get(LogEntryKind.name == 'service_key_modify') # Update the key's metadata.
self.assertEquals(existing_modify + 1, model.log.LogEntry.select().where(LogEntry.kind == kind).count()) self.putJsonResponse(SuperUserServiceKey, params=dict(kid=key['kid']),
data=dict(metadata=dict(foo='bar')))
# Update the key's metadata. # Ensure the key's metadata has been changed.
self.putJsonResponse(SuperUserServiceKey, params=dict(kid=key['kid']), json = self.getJsonResponse(SuperUserServiceKey, params=dict(kid=key['kid']))
data=dict(metadata=dict(foo='bar'))) self.assertEquals('bar', json['metadata']['foo'])
# Ensure the key's metadata has been changed. with assert_action_logged('service_key_extend'):
json = self.getJsonResponse(SuperUserServiceKey, params=dict(kid=key['kid'])) # Change the key's expiration.
self.assertEquals('bar', json['metadata']['foo']) self.putJsonResponse(SuperUserServiceKey, params=dict(kid=key['kid']),
data=dict(expiration=None))
# Ensure a log was added for the modification. # Ensure the key's expiration has been changed.
kind = LogEntryKind.get(LogEntryKind.name == 'service_key_modify') json = self.getJsonResponse(SuperUserServiceKey, params=dict(kid=key['kid']))
self.assertEquals(existing_modify + 2, model.log.LogEntry.select().where(LogEntry.kind == kind).count()) self.assertIsNone(json['expiration_date'])
# Change the key's expiration. with assert_action_logged('service_key_delete'):
self.putJsonResponse(SuperUserServiceKey, params=dict(kid=key['kid']), # Delete the key.
data=dict(expiration=None)) self.deleteResponse(SuperUserServiceKey, params=dict(kid=key['kid']))
# Ensure the key's expiration has been changed. # Ensure the key no longer exists.
json = self.getJsonResponse(SuperUserServiceKey, params=dict(kid=key['kid'])) self.getResponse(SuperUserServiceKey, params=dict(kid=key['kid']), expected_code=404)
self.assertIsNone(json['expiration_date'])
# Ensure a log was added for the modification. json = self.getJsonResponse(SuperUserServiceKeyManagement)
kind = LogEntryKind.get(LogEntryKind.name == 'service_key_extend') self.assertEquals(key_count - 1, len(json['keys']))
self.assertEquals(1, model.log.LogEntry.select().where(LogEntry.kind == kind).count())
# Delete the key.
self.deleteResponse(SuperUserServiceKey, params=dict(kid=key['kid']))
# Ensure the key no longer exists.
self.getResponse(SuperUserServiceKey, params=dict(kid=key['kid']), expected_code=404)
json = self.getJsonResponse(SuperUserServiceKeyManagement)
self.assertEquals(key_count - 1, len(json['keys']))
# Ensure a log was added for the deletion.
kind = LogEntryKind.get(LogEntryKind.name == 'service_key_delete')
self.assertEquals(1, model.log.LogEntry.select().where(LogEntry.kind == kind).count())
def test_approve_key(self): def test_approve_key(self):
self.login(ADMIN_ACCESS_USER) self.login(ADMIN_ACCESS_USER)
kind = LogEntryKind.get(LogEntryKind.name == 'service_key_approve')
existing_log_count = model.log.LogEntry.select().where(LogEntry.kind == kind).count()
# Ensure the key is not yet approved. # Ensure the key is not yet approved.
json = self.getJsonResponse(SuperUserServiceKey, params=dict(kid='kid3')) json = self.getJsonResponse(SuperUserServiceKey, params=dict(kid='kid3'))
self.assertEquals('unapprovedkey', json['name']) self.assertEquals('unapprovedkey', json['name'])
self.assertIsNone(json['approval']) self.assertIsNone(json['approval'])
# Approve the key. # Approve the key.
self.postResponse(SuperUserServiceKeyApproval, params=dict(kid='kid3'), with assert_action_logged('service_key_approve'):
data=dict(notes='testapprove'), expected_code=201) self.postResponse(SuperUserServiceKeyApproval, params=dict(kid='kid3'),
data=dict(notes='testapprove'), expected_code=201)
# Ensure the key is approved. # Ensure the key is approved.
json = self.getJsonResponse(SuperUserServiceKey, params=dict(kid='kid3')) json = self.getJsonResponse(SuperUserServiceKey, params=dict(kid='kid3'))
self.assertEquals('unapprovedkey', json['name']) self.assertEquals('unapprovedkey', json['name'])
self.assertIsNotNone(json['approval']) self.assertIsNotNone(json['approval'])
self.assertEquals('ServiceKeyApprovalType.SUPERUSER', json['approval']['approval_type']) self.assertEquals('ServiceKeyApprovalType.SUPERUSER', json['approval']['approval_type'])
self.assertEquals(ADMIN_ACCESS_USER, json['approval']['approver']['username']) self.assertEquals(ADMIN_ACCESS_USER, json['approval']['approver']['username'])
self.assertEquals('testapprove', json['approval']['notes']) self.assertEquals('testapprove', json['approval']['notes'])
# Ensure the approval was logged.
kind = LogEntryKind.get(LogEntryKind.name == 'service_key_approve')
self.assertEquals(existing_log_count + 1, model.log.LogEntry.select().where(LogEntry.kind == kind).count())
def test_approve_preapproved(self): def test_approve_preapproved(self):
self.login(ADMIN_ACCESS_USER) self.login(ADMIN_ACCESS_USER)
@ -3766,9 +3800,6 @@ class TestSuperUserKeyManagement(ApiTestCase):
def test_create_key(self): def test_create_key(self):
self.login(ADMIN_ACCESS_USER) self.login(ADMIN_ACCESS_USER)
kind = LogEntryKind.get(LogEntryKind.name == 'service_key_create')
existing_log_count = model.log.LogEntry.select().where(LogEntry.kind == kind).count()
new_key = { new_key = {
'service': 'coolservice', 'service': 'coolservice',
'name': 'mynewkey', 'name': 'mynewkey',
@ -3777,36 +3808,30 @@ class TestSuperUserKeyManagement(ApiTestCase):
'expiration': timegm((datetime.datetime.now() + datetime.timedelta(days=1)).utctimetuple()), 'expiration': timegm((datetime.datetime.now() + datetime.timedelta(days=1)).utctimetuple()),
} }
# Create the key. with assert_action_logged('service_key_create'):
json = self.postJsonResponse(SuperUserServiceKeyManagement, data=new_key) # Create the key.
self.assertEquals('mynewkey', json['name']) json = self.postJsonResponse(SuperUserServiceKeyManagement, data=new_key)
self.assertTrue('kid' in json) self.assertEquals('mynewkey', json['name'])
self.assertTrue('public_key' in json) self.assertTrue('kid' in json)
self.assertTrue('private_key' in json) self.assertTrue('public_key' in json)
self.assertTrue('private_key' in json)
# Verify the private key is a valid PEM. # Verify the private key is a valid PEM.
serialization.load_pem_private_key(json['private_key'].encode('utf-8'), None, default_backend()) serialization.load_pem_private_key(json['private_key'].encode('utf-8'), None, default_backend())
# Verify the key. # Verify the key.
kid = json['kid'] kid = json['kid']
json = self.getJsonResponse(SuperUserServiceKey, params=dict(kid=kid)) json = self.getJsonResponse(SuperUserServiceKey, params=dict(kid=kid))
self.assertEquals('mynewkey', json['name']) self.assertEquals('mynewkey', json['name'])
self.assertEquals('coolservice', json['service']) self.assertEquals('coolservice', json['service'])
self.assertEquals('baz', json['metadata']['foo']) self.assertEquals('baz', json['metadata']['foo'])
self.assertEquals(kid, json['kid']) self.assertEquals(kid, json['kid'])
self.assertIsNotNone(json['approval']) self.assertIsNotNone(json['approval'])
self.assertEquals('ServiceKeyApprovalType.SUPERUSER', json['approval']['approval_type']) self.assertEquals('ServiceKeyApprovalType.SUPERUSER', json['approval']['approval_type'])
self.assertEquals(ADMIN_ACCESS_USER, json['approval']['approver']['username']) self.assertEquals(ADMIN_ACCESS_USER, json['approval']['approver']['username'])
self.assertEquals('whazzup!?', json['approval']['notes']) self.assertEquals('whazzup!?', json['approval']['notes'])
# Ensure that there are logs for the creation and auto-approval.
kind = LogEntryKind.get(LogEntryKind.name == 'service_key_create')
self.assertEquals(existing_log_count + 1, model.log.LogEntry.select().where(LogEntry.kind == kind).count())
kind = LogEntryKind.get(LogEntryKind.name == 'service_key_approve')
self.assertEquals(existing_log_count + 1, model.log.LogEntry.select().where(LogEntry.kind == kind).count())
class TestSuperUserManagement(ApiTestCase): class TestSuperUserManagement(ApiTestCase):

View file

@ -1,4 +1,4 @@
from multiprocessing.sharedctypes import Value, Array from multiprocessing.sharedctypes import Array
from util.validation import MAX_LENGTH from util.validation import MAX_LENGTH
class SuperUserManager(object): class SuperUserManager(object):