diff --git a/data/model/legacy.py b/data/model/legacy.py index b22bd1457..67daaa540 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -1118,6 +1118,7 @@ def change_username(user_id, new_username): # Rename the user user.username = new_username user.save() + return user def change_invoice_email(user, invoice_email): @@ -2623,6 +2624,10 @@ def delete_matching_notifications(target, kind_name, **kwargs): notification.delete_instance() +def get_organizations(): + return User.select().where(User.organization == True, User.robot == False) + + def get_active_users(): return User.select().where(User.organization == False, User.robot == False) diff --git a/endpoints/api/superuser.py b/endpoints/api/superuser.py index 42bf67c08..c1406dcb8 100644 --- a/endpoints/api/superuser.py +++ b/endpoints/api/superuser.py @@ -103,6 +103,12 @@ class SuperUserLogs(ApiResource): abort(403) +def org_view(org): + return { + 'name': org.username, + 'avatar': avatar.get_data_for_org(org), + } + def user_view(user): return { 'username': user.username, @@ -132,6 +138,25 @@ class UsageInformation(ApiResource): +@resource('/v1/superuser/organizations/') +@internal_only +@show_if(features.SUPER_USERS) +class SuperUserOrganizationList(ApiResource): + """ Resource for listing organizations in the system. """ + @require_fresh_login + @verify_not_prod + @nickname('listAllOrganizations') + def get(self): + """ Returns a list of all organizations in the system. """ + if SuperUserPermission().can(): + orgs = model.get_organizations() + return { + 'organizations': [org_view(org) for org in orgs] + } + + abort(403) + + @resource('/v1/superuser/users/') @internal_only @show_if(features.SUPER_USERS) @@ -248,6 +273,10 @@ class SuperUserManagement(ApiResource): 'email': { 'type': 'string', 'description': 'The new e-mail address for the user', + }, + 'enabled': { + 'type': 'boolean', + 'description': 'Whether the user is enabled' } }, }, @@ -309,3 +338,54 @@ class SuperUserManagement(ApiResource): return user_view(user) abort(403) + + +@resource('/v1/superuser/organizations/') +@path_param('name', 'The name of the organizaton being managed') +@internal_only +@show_if(features.SUPER_USERS) +class SuperUserOrganizationManagement(ApiResource): + """ Resource for managing organizations in the system. """ + schemas = { + 'UpdateOrg': { + 'id': 'UpdateOrg', + 'type': 'object', + 'description': 'Description of updates for an organization', + 'properties': { + 'name': { + 'type': 'string', + 'description': 'The new name for the organization', + } + }, + }, + } + + @require_fresh_login + @verify_not_prod + @nickname('deleteOrganization') + def delete(self, name): + """ Deletes the specified organization. """ + if SuperUserPermission().can(): + org = model.get_organization(name) + + model.delete_user(org) + return 'Deleted', 204 + + abort(403) + + @require_fresh_login + @verify_not_prod + @nickname('changeOrganization') + @validate_json_request('UpdateOrg') + def put(self, name): + """ Updates information about the specified user. """ + if SuperUserPermission().can(): + org = model.get_organization(name) + org_data = request.get_json() + + if 'name' in org_data: + org = model.change_username(org.id, org_data['name']) + + return org_view(org) + + abort(403) diff --git a/static/js/pages/superuser.js b/static/js/pages/superuser.js index f0a3c6046..9717781e9 100644 --- a/static/js/pages/superuser.js +++ b/static/js/pages/superuser.js @@ -101,6 +101,21 @@ $scope.logsCounter++; }; + $scope.loadOrganizations = function() { + if ($scope.organizations) { + return; + } + + $scope.loadOrganizationsInternal(); + }; + + $scope.loadOrganizationsInternal = function() { + $scope.organizationsResource = ApiService.listAllOrganizationsAsResource().get(function(resp) { + $scope.organizations = resp['organizations']; + return $scope.organizations; + }); + }; + $scope.loadUsers = function() { if ($scope.users) { return; @@ -183,6 +198,40 @@ }, ApiService.errorDisplay('Could not change user')); }; + $scope.askDeleteOrganization = function(org) { + bootbox.confirm('Are you sure you want to delete this organization? Its data will be deleted with it.', + function(result) { + if (!result) { return; } + + var params = { + 'name': org.name + }; + + ApiService.deleteOrganization(null, params).then(function(resp) { + $scope.loadOrganizationsInternal(); + }, ApiService.errorDisplay('Could not delete organization')); + }); + }; + + $scope.askRenameOrganization = function(org) { + bootbox.prompt('Enter a new name for the organization:', function(newName) { + if (!newName) { return; } + + var params = { + 'name': org.name + }; + + var data = { + 'name': newName + }; + + ApiService.changeOrganization(data, params).then(function(resp) { + $scope.loadOrganizationsInternal(); + org.name = newName; + }, ApiService.errorDisplay('Could not rename organization')); + }); + }; + $scope.changeUserPassword = function(user) { $('#changePasswordModal').modal('hide'); diff --git a/static/partials/super-user.html b/static/partials/super-user.html index 52fc67b3d..88a8da16a 100644 --- a/static/partials/super-user.html +++ b/static/partials/super-user.html @@ -20,6 +20,10 @@ tab-target="#users" tab-init="loadUsers()"> + + + @@ -107,6 +111,45 @@ For more information: See Here. + +
+
+
+
+ +
+ + + + + + + + + + + + + +
Name
+ + + {{ current_org.name }} + + + + Rename Organization + + + Delete Organization + + +
+
+
+
@@ -168,6 +211,7 @@
+ diff --git a/test/test_api_security.py b/test/test_api_security.py index bc65ecdfe..e7b0f40e2 100644 --- a/test/test_api_security.py +++ b/test/test_api_security.py @@ -45,7 +45,8 @@ from endpoints.api.permission import (RepositoryUserPermission, RepositoryTeamPe RepositoryTeamPermissionList, RepositoryUserPermissionList) from endpoints.api.superuser import (SuperUserLogs, SuperUserList, SuperUserManagement, - SuperUserSendRecoveryEmail, UsageInformation) + SuperUserSendRecoveryEmail, UsageInformation, + SuperUserOrganizationManagement, SuperUserOrganizationList) try: @@ -3927,6 +3928,56 @@ class TestUsageInformation(ApiTestCase): self._run_test('GET', 200, 'devtable', None) +class TestSuperUserOrganizationList(ApiTestCase): + def setUp(self): + ApiTestCase.setUp(self) + self._set_url(SuperUserOrganizationList) + + def test_get_anonymous(self): + self._run_test('GET', 401, None, None) + + def test_get_freshuser(self): + self._run_test('GET', 403, 'freshuser', None) + + def test_get_reader(self): + self._run_test('GET', 403, 'reader', None) + + def test_get_devtable(self): + self._run_test('GET', 200, 'devtable', None) + + +class TestSuperUserOrganizationManagement(ApiTestCase): + def setUp(self): + ApiTestCase.setUp(self) + self._set_url(SuperUserOrganizationManagement, name='buynlarge') + + def test_put_anonymous(self): + self._run_test('PUT', 401, None, {}) + + def test_put_freshuser(self): + self._run_test('PUT', 403, 'freshuser', {}) + + def test_put_reader(self): + self._run_test('PUT', 403, 'reader', {}) + + def test_put_devtable(self): + self._run_test('PUT', 200, 'devtable', {}) + + + def test_delete_anonymous(self): + self._run_test('DELETE', 401, None, None) + + def test_delete_freshuser(self): + self._run_test('DELETE', 403, 'freshuser', None) + + def test_delete_reader(self): + self._run_test('DELETE', 403, 'reader', None) + + def test_delete_devtable(self): + self._run_test('DELETE', 204, 'devtable', None) + + + class TestSuperUserList(ApiTestCase): def setUp(self): ApiTestCase.setUp(self)