From d9c7e92637d21a3276ab9464b22e6d358c190135 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 1 Oct 2014 13:55:09 -0400 Subject: [PATCH] Add superuser abilities: create user, show logs. Also fix the super users UI to show the user drop down and make all superuser API calls require fresh login --- endpoints/api/superuser.py | 96 ++++++++++++++++++++++++++++++--- static/css/quay.css | 2 +- static/js/app.js | 9 +++- static/js/controllers.js | 42 +++++++++++++++ static/partials/super-user.html | 68 +++++++++++++++++++---- test/test_api_security.py | 37 ++++++++++--- 6 files changed, 225 insertions(+), 29 deletions(-) diff --git a/endpoints/api/superuser.py b/endpoints/api/superuser.py index 5a117289b..c41c6a46c 100644 --- a/endpoints/api/superuser.py +++ b/endpoints/api/superuser.py @@ -1,20 +1,22 @@ +import string import logging import json +from random import SystemRandom from app import app - from flask import request from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error, log_action, internal_only, NotFound, require_user_admin, format_date, InvalidToken, require_scope, format_date, hide_if, show_if, parse_args, - query_param, abort) + query_param, abort, require_fresh_login) from endpoints.api.logs import get_logs from data import model from auth.permissions import SuperUserPermission from auth.auth_context import get_authenticated_user +from util.useremails import send_confirmation_email, send_recovery_email import features @@ -55,6 +57,26 @@ def user_view(user): @show_if(features.SUPER_USERS) class SuperUserList(ApiResource): """ Resource for listing users in the system. """ + schemas = { + 'CreateInstallUser': { + 'id': 'CreateInstallUser', + 'description': 'Data for creating a user', + 'required': ['username', 'email'], + 'properties': { + 'username': { + 'type': 'string', + 'description': 'The username of the user being created' + }, + + 'email': { + 'type': 'string', + 'description': 'The email address of the user being created' + } + } + } + } + + @require_fresh_login @nickname('listAllUsers') def get(self): """ Returns a list of all users in the system. """ @@ -67,6 +89,63 @@ class SuperUserList(ApiResource): abort(403) + @require_fresh_login + @nickname('createInstallUser') + @validate_json_request('CreateInstallUser') + def post(self): + """ Creates a new user. """ + user_information = request.get_json() + if SuperUserPermission().can(): + username = user_information['username'] + email = user_information['email'] + + # Generate a temporary password for the user. + random = SystemRandom() + password = ''.join([random.choice(string.ascii_uppercase + string.digits) for _ in range(32)]) + + # Create the user. + user = model.create_user(username, password, email, auto_verify=not features.MAILING) + + # If mailing is turned on, send the user a verification email. + if features.MAILING: + confirmation = model.create_confirm_email_code(user, new_email=user.email) + send_confirmation_email(user.username, user.email, confirmation.code) + + return { + 'username': username, + 'email': email, + 'password': password + } + + abort(403) + + +@resource('/v1/superusers/users//sendrecovery') +@internal_only +@show_if(features.SUPER_USERS) +@show_if(features.MAILING) +class SuperUserSendRecoveryEmail(ApiResource): + """ Resource for sending a recovery user on behalf of a user. """ + @require_fresh_login + @nickname('sendInstallUserRecoveryEmail') + def post(self, username): + if SuperUserPermission().can(): + user = model.get_user(username) + if not user or user.organization or user.robot: + abort(404) + + if username in app.config['SUPER_USERS']: + abort(403) + + code = model.create_reset_password_email_code(user.email) + send_recovery_email(user.email, code.code) + return { + 'email': user.email + } + + abort(403) + + @resource('/v1/superuser/users/') @internal_only @show_if(features.SUPER_USERS) @@ -90,18 +169,20 @@ class SuperUserManagement(ApiResource): }, } + @require_fresh_login @nickname('getInstallUser') def get(self, username): """ Returns information about the specified user. """ if SuperUserPermission().can(): - user = model.get_user(username) - if not user or user.organization or user.robot: - abort(404) - - return user_view(user) + user = model.get_user(username) + if not user or user.organization or user.robot: + abort(404) + + return user_view(user) abort(403) + @require_fresh_login @nickname('deleteInstallUser') def delete(self, username): """ Deletes the specified user. """ @@ -118,6 +199,7 @@ class SuperUserManagement(ApiResource): abort(403) + @require_fresh_login @nickname('changeInstallUser') @validate_json_request('UpdateUser') def put(self, username): diff --git a/static/css/quay.css b/static/css/quay.css index 7e40e18b1..5ec050200 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -4264,7 +4264,7 @@ pre.command:before { } .user-row.super-user td { - background-color: #d9edf7; + background-color: #eeeeee; } .user-row .user-class { diff --git a/static/js/app.js b/static/js/app.js index 186571e5e..27b086bab 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -3073,7 +3073,8 @@ quayApp.directive('logsView', function () { 'user': '=user', 'makevisible': '=makevisible', 'repository': '=repository', - 'performer': '=performer' + 'performer': '=performer', + 'allLogs': '@allLogs' }, controller: function($scope, $element, $sce, Restangular, ApiService, TriggerService, StringBuilderService, ExternalNotificationData) { @@ -3285,7 +3286,7 @@ quayApp.directive('logsView', function () { var hasValidUser = !!$scope.user; var hasValidOrg = !!$scope.organization; var hasValidRepo = $scope.repository && $scope.repository.namespace; - var isValid = hasValidUser || hasValidOrg || hasValidRepo; + var isValid = hasValidUser || hasValidOrg || hasValidRepo || $scope.allLogs; if (!$scope.makevisible || !isValid) { return; @@ -3308,6 +3309,10 @@ quayApp.directive('logsView', function () { url = getRestUrl('repository', $scope.repository.namespace, $scope.repository.name, 'logs'); } + if ($scope.allLogs) { + url = getRestUrl('superuser', 'logs') + } + url += '?starttime=' + encodeURIComponent(getDateString($scope.logStartDate)); url += '&endtime=' + encodeURIComponent(getDateString($scope.logEndDate)); diff --git a/static/js/controllers.js b/static/js/controllers.js index a3ed8890e..908918631 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -2696,6 +2696,14 @@ function SuperUserAdminCtrl($scope, ApiService, Features, UserService) { // Monitor any user changes and place the current user into the scope. UserService.updateUserIn($scope); + $scope.logsCounter = 0; + $scope.newUser = {}; + $scope.createdUsers = []; + + $scope.loadLogs = function() { + $scope.logsCounter++; + }; + $scope.loadUsers = function() { if ($scope.users) { return; @@ -2707,6 +2715,7 @@ function SuperUserAdminCtrl($scope, ApiService, Features, UserService) { $scope.loadUsersInternal = function() { ApiService.listAllUsers().then(function(resp) { $scope.users = resp['users']; + $scope.showInterface = true; }, function(resp) { $scope.users = []; $scope.usersError = resp['data']['message'] || resp['data']['error_description']; @@ -2718,6 +2727,19 @@ function SuperUserAdminCtrl($scope, ApiService, Features, UserService) { $('#changePasswordModal').modal({}); }; + $scope.createUser = function() { + $scope.creatingUser = true; + var errorHandler = ApiService.errorDisplay('Cannot create user', function() { + $scope.creatingUser = false; + }); + + ApiService.createInstallUser($scope.newUser, null).then(function(resp) { + $scope.creatingUser = false; + $scope.newUser = {}; + $scope.createdUsers.push(resp); + }, errorHandler) + }; + $scope.showDeleteUser = function(user) { if (user.username == UserService.currentUser().username) { bootbox.dialog({ @@ -2765,6 +2787,26 @@ function SuperUserAdminCtrl($scope, ApiService, Features, UserService) { }, ApiService.errorDisplay('Cannot delete user')); }; + $scope.sendRecoveryEmail = function(user) { + var params = { + 'username': user.username + }; + + ApiService.sendInstallUserRecoveryEmail(null, params).then(function(resp) { + bootbox.dialog({ + "message": "A recovery email has been sent to " + resp['email'], + "title": "Recovery email sent", + "buttons": { + "close": { + "label": "Close", + "className": "btn-primary" + } + } + }); + + }, ApiService.errorDisplay('Cannot send recovery email')) + }; + $scope.loadUsers(); } diff --git a/static/partials/super-user.html b/static/partials/super-user.html index bc21f5c94..a12f9f5b2 100644 --- a/static/partials/super-user.html +++ b/static/partials/super-user.html @@ -1,4 +1,4 @@ -
+
This panel provides administrator access to super users of this installation of the registry. Super users can be managed in the configuration for this installation.
@@ -10,18 +10,64 @@
  • Users
  • +
  • + Create User +
  • +
  • + System Logs +
  • + +
    +
    +
    + + +
    + +
    +
    + + +
    + +
    + + +
    + + +
    + +
    + + + + + + + + + + + + +
    UsernameE-mail addressTemporary Password
    {{ created_user.username }}{{ created_user.email }}{{ created_user.password }}
    +
    +
    +
    {{ usersError }} -
    +
    @@ -37,8 +83,7 @@ Username E-mail address - - + {{ current_user.email }} - - Super user - - -