From 20816804e521a8727b0372d9c2d11746a55ec394 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 7 Jun 2016 18:12:11 -0400 Subject: [PATCH] Add ability for super users to take ownership of namespaces Fixes #1395 --- ...d11eb_add_take_ownership_log_entry_kind.py | 26 +++ data/model/organization.py | 8 + data/model/team.py | 2 +- endpoints/api/superuser.py | 48 +++++ initdb.py | 2 + static/css/pages/superuser.css | 8 + static/js/directives/ui/logs-view.js | 9 + static/js/pages/superuser.js | 22 +- static/partials/super-user.html | 22 ++ test/data/test.db | Bin 1175552 -> 1175552 bytes test/helpers.py | 2 +- test/test_api_security.py | 22 +- test/test_api_usage.py | 201 ++++++++++-------- util/config/superusermanager.py | 2 +- 14 files changed, 280 insertions(+), 94 deletions(-) create mode 100644 data/migrations/versions/0f17d94d11eb_add_take_ownership_log_entry_kind.py diff --git a/data/migrations/versions/0f17d94d11eb_add_take_ownership_log_entry_kind.py b/data/migrations/versions/0f17d94d11eb_add_take_ownership_log_entry_kind.py new file mode 100644 index 000000000..5293c756a --- /dev/null +++ b/data/migrations/versions/0f17d94d11eb_add_take_ownership_log_entry_kind.py @@ -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'))) + ) diff --git a/data/model/organization.py b/data/model/organization.py index a63b020cc..6fc556f50 100644 --- a/data/model/organization.py +++ b/data/model/organization.py @@ -136,3 +136,11 @@ def get_all_repo_users_transitive_via_teams(namespace_name, repository_name): def get_organizations(): 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 diff --git a/data/model/team.py b/data/model/team.py index c7d810b80..c3c071d31 100644 --- a/data/model/team.py +++ b/data/model/team.py @@ -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, InvalidTeamMemberException, user, _basequery) from util.validation import validate_username diff --git a/endpoints/api/superuser.py b/endpoints/api/superuser.py index ebd5eef67..baa5b1a86 100644 --- a/endpoints/api/superuser.py +++ b/endpoints/api/superuser.py @@ -422,6 +422,54 @@ class SuperUserManagement(ApiResource): abort(403) +@resource('/v1/superuser/takeownership/') +@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/') @path_param('name', 'The name of the organizaton being managed') @internal_only diff --git a/initdb.py b/initdb.py index e4d042490..e8006e536 100644 --- a/initdb.py +++ b/initdb.py @@ -343,6 +343,8 @@ def initialize_database(): LogEntryKind.create(name='service_key_extend') LogEntryKind.create(name='service_key_rotate') + LogEntryKind.create(name='take_ownership') + ImageStorageLocation.create(name='local_eu') ImageStorageLocation.create(name='local_us') diff --git a/static/css/pages/superuser.css b/static/css/pages/superuser.css index d1eae17bd..19771a0ff 100644 --- a/static/css/pages/superuser.css +++ b/static/css/pages/superuser.css @@ -49,3 +49,11 @@ .super-user .input-util { margin-top: 10px; } + +.super-user .take-ownership-dialog .avatar { + margin-left: 6px; +} + +.super-user .take-ownership-dialog .co-alert { + margin-top: 20px; +} \ No newline at end of file diff --git a/static/js/directives/ui/logs-view.js b/static/js/directives/ui/logs-view.js index 1e8b09f91..32b0aeb4f 100644 --- a/static/js/directives/ui/logs-view.js +++ b/static/js/directives/ui/logs-view.js @@ -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_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. 'add_repo_webhook': 'Add 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_extend': 'Extend Service Key Expiration', 'service_key_rotate': 'Automatic rotation of Service Key', + 'take_ownership': 'Take Namespace Ownership', // Note: these are deprecated. 'add_repo_webhook': 'Add webhook', diff --git a/static/js/pages/superuser.js b/static/js/pages/superuser.js index d97528398..1132f27a7 100644 --- a/static/js/pages/superuser.js +++ b/static/js/pages/superuser.js @@ -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) { return; } @@ -32,6 +32,7 @@ $scope.dashboardActive = false; $scope.currentConfig = null; $scope.serviceKeysActive = false; + $scope.takeOwnershipInfo = null; $scope.setDashboardActive = function(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) { var message = 'Are you sure you want to disable this user? ' + 'They will be unable to login, pull or push.' diff --git a/static/partials/super-user.html b/static/partials/super-user.html index 09c58dcee..56d83adeb 100644 --- a/static/partials/super-user.html +++ b/static/partials/super-user.html @@ -138,6 +138,9 @@ Delete Organization + + Take Ownership + @@ -223,6 +226,10 @@ Disable Enable User + + Take Ownership + @@ -233,6 +240,21 @@ + +
+ Are you sure you want to take ownership of + organization {{ takeOwnershipInfo.entity.name }}? + user namespace {{ takeOwnershipInfo .entity.username }}? + +
+ Note: This will convert the user namespace into an organization. The user will no longer be able to login to this account. +
+
+