Add ability for super users to take ownership of namespaces
Fixes #1395
This commit is contained in:
parent
f75949d533
commit
20816804e5
14 changed files with 280 additions and 94 deletions
|
@ -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')))
|
||||||
|
)
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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')
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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',
|
||||||
|
|
|
@ -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.'
|
||||||
|
|
|
@ -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.
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Reference in a new issue