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

This commit is contained in:
Joseph Schorr 2014-10-01 13:55:09 -04:00
parent 039d53ea6c
commit d9c7e92637
6 changed files with 225 additions and 29 deletions

View file

@ -1,20 +1,22 @@
import string
import logging import logging
import json import json
from random import SystemRandom
from app import app from app import app
from flask import request from flask import request
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error, from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
log_action, internal_only, NotFound, require_user_admin, format_date, log_action, internal_only, NotFound, require_user_admin, format_date,
InvalidToken, require_scope, format_date, hide_if, show_if, parse_args, 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 endpoints.api.logs import get_logs
from data import model from data import model
from auth.permissions import SuperUserPermission from auth.permissions import SuperUserPermission
from auth.auth_context import get_authenticated_user from auth.auth_context import get_authenticated_user
from util.useremails import send_confirmation_email, send_recovery_email
import features import features
@ -55,6 +57,26 @@ def user_view(user):
@show_if(features.SUPER_USERS) @show_if(features.SUPER_USERS)
class SuperUserList(ApiResource): class SuperUserList(ApiResource):
""" Resource for listing users in the system. """ """ 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') @nickname('listAllUsers')
def get(self): def get(self):
""" Returns a list of all users in the system. """ """ Returns a list of all users in the system. """
@ -67,6 +89,63 @@ class SuperUserList(ApiResource):
abort(403) 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/<username>/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/<username>') @resource('/v1/superuser/users/<username>')
@internal_only @internal_only
@show_if(features.SUPER_USERS) @show_if(features.SUPER_USERS)
@ -90,6 +169,7 @@ class SuperUserManagement(ApiResource):
}, },
} }
@require_fresh_login
@nickname('getInstallUser') @nickname('getInstallUser')
def get(self, username): def get(self, username):
""" Returns information about the specified user. """ """ Returns information about the specified user. """
@ -102,6 +182,7 @@ class SuperUserManagement(ApiResource):
abort(403) abort(403)
@require_fresh_login
@nickname('deleteInstallUser') @nickname('deleteInstallUser')
def delete(self, username): def delete(self, username):
""" Deletes the specified user. """ """ Deletes the specified user. """
@ -118,6 +199,7 @@ class SuperUserManagement(ApiResource):
abort(403) abort(403)
@require_fresh_login
@nickname('changeInstallUser') @nickname('changeInstallUser')
@validate_json_request('UpdateUser') @validate_json_request('UpdateUser')
def put(self, username): def put(self, username):

View file

@ -4264,7 +4264,7 @@ pre.command:before {
} }
.user-row.super-user td { .user-row.super-user td {
background-color: #d9edf7; background-color: #eeeeee;
} }
.user-row .user-class { .user-row .user-class {

View file

@ -3073,7 +3073,8 @@ quayApp.directive('logsView', function () {
'user': '=user', 'user': '=user',
'makevisible': '=makevisible', 'makevisible': '=makevisible',
'repository': '=repository', 'repository': '=repository',
'performer': '=performer' 'performer': '=performer',
'allLogs': '@allLogs'
}, },
controller: function($scope, $element, $sce, Restangular, ApiService, TriggerService, controller: function($scope, $element, $sce, Restangular, ApiService, TriggerService,
StringBuilderService, ExternalNotificationData) { StringBuilderService, ExternalNotificationData) {
@ -3285,7 +3286,7 @@ quayApp.directive('logsView', function () {
var hasValidUser = !!$scope.user; var hasValidUser = !!$scope.user;
var hasValidOrg = !!$scope.organization; var hasValidOrg = !!$scope.organization;
var hasValidRepo = $scope.repository && $scope.repository.namespace; var hasValidRepo = $scope.repository && $scope.repository.namespace;
var isValid = hasValidUser || hasValidOrg || hasValidRepo; var isValid = hasValidUser || hasValidOrg || hasValidRepo || $scope.allLogs;
if (!$scope.makevisible || !isValid) { if (!$scope.makevisible || !isValid) {
return; return;
@ -3308,6 +3309,10 @@ quayApp.directive('logsView', function () {
url = getRestUrl('repository', $scope.repository.namespace, $scope.repository.name, 'logs'); url = getRestUrl('repository', $scope.repository.namespace, $scope.repository.name, 'logs');
} }
if ($scope.allLogs) {
url = getRestUrl('superuser', 'logs')
}
url += '?starttime=' + encodeURIComponent(getDateString($scope.logStartDate)); url += '?starttime=' + encodeURIComponent(getDateString($scope.logStartDate));
url += '&endtime=' + encodeURIComponent(getDateString($scope.logEndDate)); url += '&endtime=' + encodeURIComponent(getDateString($scope.logEndDate));

View file

@ -2696,6 +2696,14 @@ function SuperUserAdminCtrl($scope, ApiService, Features, UserService) {
// Monitor any user changes and place the current user into the scope. // Monitor any user changes and place the current user into the scope.
UserService.updateUserIn($scope); UserService.updateUserIn($scope);
$scope.logsCounter = 0;
$scope.newUser = {};
$scope.createdUsers = [];
$scope.loadLogs = function() {
$scope.logsCounter++;
};
$scope.loadUsers = function() { $scope.loadUsers = function() {
if ($scope.users) { if ($scope.users) {
return; return;
@ -2707,6 +2715,7 @@ function SuperUserAdminCtrl($scope, ApiService, Features, UserService) {
$scope.loadUsersInternal = function() { $scope.loadUsersInternal = function() {
ApiService.listAllUsers().then(function(resp) { ApiService.listAllUsers().then(function(resp) {
$scope.users = resp['users']; $scope.users = resp['users'];
$scope.showInterface = true;
}, function(resp) { }, function(resp) {
$scope.users = []; $scope.users = [];
$scope.usersError = resp['data']['message'] || resp['data']['error_description']; $scope.usersError = resp['data']['message'] || resp['data']['error_description'];
@ -2718,6 +2727,19 @@ function SuperUserAdminCtrl($scope, ApiService, Features, UserService) {
$('#changePasswordModal').modal({}); $('#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) { $scope.showDeleteUser = function(user) {
if (user.username == UserService.currentUser().username) { if (user.username == UserService.currentUser().username) {
bootbox.dialog({ bootbox.dialog({
@ -2765,6 +2787,26 @@ function SuperUserAdminCtrl($scope, ApiService, Features, UserService) {
}, ApiService.errorDisplay('Cannot delete user')); }, 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(); $scope.loadUsers();
} }

View file

@ -1,4 +1,4 @@
<div class="container" quay-show="Features.SUPER_USERS"> <div class="container" quay-show="Features.SUPER_USERS && showInterface">
<div class="alert alert-info"> <div class="alert alert-info">
This panel provides administrator access to <strong>super users of this installation of the registry</strong>. Super users can be managed in the configuration for this installation. This panel provides administrator access to <strong>super users of this installation of the registry</strong>. Super users can be managed in the configuration for this installation.
</div> </div>
@ -10,12 +10,58 @@
<li class="active"> <li class="active">
<a href="javascript:void(0)" data-toggle="tab" data-target="#users" ng-click="loadUsers()">Users</a> <a href="javascript:void(0)" data-toggle="tab" data-target="#users" ng-click="loadUsers()">Users</a>
</li> </li>
<li>
<a href="javascript:void(0)" data-toggle="tab" data-target="#create-user">Create User</a>
</li>
<li>
<a href="javascript:void(0)" data-toggle="tab" data-target="#logs" ng-click="loadLogs()">System Logs</a>
</li>
</ul> </ul>
</div> </div>
<!-- Content --> <!-- Content -->
<div class="col-md-10"> <div class="col-md-10">
<div class="tab-content"> <div class="tab-content">
<!-- Logs tab -->
<div id="logs" class="tab-pane">
<div class="logsView" makevisible="logsCounter" all-logs="true"></div>
</div>
<!-- Create user tab -->
<div id="create-user" class="tab-pane">
<span class="quay-spinner" ng-show="creatingUser"></span>
<form name="createUserForm" ng-submit="createUser()" ng-show="!creatingUser">
<div class="form-group">
<label>Username</label>
<input class="form-control" type="text" ng-model="newUser.username" ng-pattern="/^[a-z0-9_]{4,30}$/" required>
</div>
<div class="form-group">
<label>Email address</label>
<input class="form-control" type="email" ng-model="newUser.email" required>
</div>
<button class="btn btn-primary" type="submit" ng-disabled="!createUserForm.$valid">Create User</button>
</form>
<div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee;" ng-show="createdUsers.length">
<table class="table">
<thead>
<th>Username</th>
<th>E-mail address</th>
<th>Temporary Password</th>
</thead>
<tr ng-repeat="created_user in createdUsers"
class="user-row">
<td>{{ created_user.username }}</td>
<td>{{ created_user.email }}</td>
<td>{{ created_user.password }}</td>
</tr>
</table>
</div>
</div>
<!-- Users tab --> <!-- Users tab -->
<div id="users" class="tab-pane active"> <div id="users" class="tab-pane active">
<div class="quay-spinner" ng-show="!users"></div> <div class="quay-spinner" ng-show="!users"></div>
@ -37,8 +83,7 @@
<thead> <thead>
<th>Username</th> <th>Username</th>
<th>E-mail address</th> <th>E-mail address</th>
<th></th> <th style="width: 24px;"></th>
<th></th>
</thead> </thead>
<tr ng-repeat="current_user in (users | filter:search | orderBy:'username' | limitTo:100)" <tr ng-repeat="current_user in (users | filter:search | orderBy:'username' | limitTo:100)"
@ -51,19 +96,20 @@
<td> <td>
<a href="mailto:{{ current_user.email }}">{{ current_user.email }}</a> <a href="mailto:{{ current_user.email }}">{{ current_user.email }}</a>
</td> </td>
<td class="user-class"> <td style="text-align: center;">
<span ng-if="current_user.super_user">Super user</span> <i class="fa fa-ge fa-lg" ng-if="current_user.super_user" data-title="Super User" bs-tooltip></i>
</td> <div class="dropdown" style="text-align: left;" ng-if="user.username != current_user.username && !current_user.super_user">
<td>
<div class="dropdown" ng-if="user.username != current_user.username && !user.super_user">
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown"> <button class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-ellipsis-h"></i> <i class="caret"></i>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu pull-right">
<li> <li>
<a href="javascript:void(0)" ng-click="showChangePassword(current_user)"> <a href="javascript:void(0)" ng-click="showChangePassword(current_user)">
<i class="fa fa-key"></i> Change Password <i class="fa fa-key"></i> Change Password
</a> </a>
<a href="javascript:void(0)" ng-click="sendRecoveryEmail(current_user)" quay-show="Features.MAILING">
<i class="fa fa-envelope"></i> Send Recovery Email
</a>
<a href="javascript:void(0)" ng-click="showDeleteUser(current_user)"> <a href="javascript:void(0)" ng-click="showDeleteUser(current_user)">
<i class="fa fa-times"></i> Delete User <i class="fa fa-times"></i> Delete User
</a> </a>

View file

@ -1,5 +1,6 @@
import unittest import unittest
import json import json
import datetime
from urllib import urlencode from urllib import urlencode
from urlparse import urlparse, urlunparse, parse_qs from urlparse import urlparse, urlunparse, parse_qs
@ -41,7 +42,8 @@ from endpoints.api.repository import RepositoryList, RepositoryVisibility, Repos
from endpoints.api.permission import (RepositoryUserPermission, RepositoryTeamPermission, from endpoints.api.permission import (RepositoryUserPermission, RepositoryTeamPermission,
RepositoryTeamPermissionList, RepositoryUserPermissionList) RepositoryTeamPermissionList, RepositoryUserPermissionList)
from endpoints.api.superuser import SuperUserLogs, SuperUserList, SuperUserManagement from endpoints.api.superuser import (SuperUserLogs, SuperUserList, SuperUserManagement,
SuperUserSendRecoveryEmail)
try: try:
@ -78,6 +80,7 @@ class ApiTestCase(unittest.TestCase):
if auth_username: if auth_username:
loaded = model.get_user(auth_username) loaded = model.get_user(auth_username)
sess['user_id'] = loaded.id sess['user_id'] = loaded.id
sess['login_time'] = datetime.datetime.now()
sess[CSRF_TOKEN_KEY] = CSRF_TOKEN sess[CSRF_TOKEN_KEY] = CSRF_TOKEN
# Restore the teardown functions # Restore the teardown functions
@ -512,13 +515,13 @@ class TestUser(ApiTestCase):
self._run_test('PUT', 401, None, {}) self._run_test('PUT', 401, None, {})
def test_put_freshuser(self): def test_put_freshuser(self):
self._run_test('PUT', 401, 'freshuser', {}) self._run_test('PUT', 200, 'freshuser', {})
def test_put_reader(self): def test_put_reader(self):
self._run_test('PUT', 401, 'reader', {}) self._run_test('PUT', 200, 'reader', {})
def test_put_devtable(self): def test_put_devtable(self):
self._run_test('PUT', 401, 'devtable', {}) self._run_test('PUT', 200, 'devtable', {})
def test_post_anonymous(self): def test_post_anonymous(self):
self._run_test('POST', 400, None, {u'username': 'T946', u'password': '0SG4', u'email': 'MENT'}) self._run_test('POST', 400, None, {u'username': 'T946', u'password': '0SG4', u'email': 'MENT'})
@ -3585,6 +3588,24 @@ class TestSuperUserLogs(ApiTestCase):
self._run_test('GET', 200, 'devtable', None) self._run_test('GET', 200, 'devtable', None)
class TestSuperUserSendRecoveryEmail(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)
self._set_url(SuperUserSendRecoveryEmail, username='someuser')
def test_post_anonymous(self):
self._run_test('POST', 401, None, None)
def test_post_freshuser(self):
self._run_test('POST', 403, 'freshuser', None)
def test_post_reader(self):
self._run_test('POST', 403, 'reader', None)
def test_post_devtable(self):
self._run_test('POST', 404, 'devtable', None)
class TestTeamMemberInvite(ApiTestCase): class TestTeamMemberInvite(ApiTestCase):
def setUp(self): def setUp(self):
ApiTestCase.setUp(self) ApiTestCase.setUp(self)
@ -3621,7 +3642,7 @@ class TestSuperUserList(ApiTestCase):
self._set_url(SuperUserList) self._set_url(SuperUserList)
def test_get_anonymous(self): def test_get_anonymous(self):
self._run_test('GET', 403, None, None) self._run_test('GET', 401, None, None)
def test_get_freshuser(self): def test_get_freshuser(self):
self._run_test('GET', 403, 'freshuser', None) self._run_test('GET', 403, 'freshuser', None)
@ -3639,7 +3660,7 @@ class TestSuperUserManagement(ApiTestCase):
self._set_url(SuperUserManagement, username='freshuser') self._set_url(SuperUserManagement, username='freshuser')
def test_get_anonymous(self): def test_get_anonymous(self):
self._run_test('GET', 403, None, None) self._run_test('GET', 401, None, None)
def test_get_freshuser(self): def test_get_freshuser(self):
self._run_test('GET', 403, 'freshuser', None) self._run_test('GET', 403, 'freshuser', None)
@ -3652,7 +3673,7 @@ class TestSuperUserManagement(ApiTestCase):
def test_put_anonymous(self): def test_put_anonymous(self):
self._run_test('PUT', 403, None, {}) self._run_test('PUT', 401, None, {})
def test_put_freshuser(self): def test_put_freshuser(self):
self._run_test('PUT', 403, 'freshuser', {}) self._run_test('PUT', 403, 'freshuser', {})
@ -3665,7 +3686,7 @@ class TestSuperUserManagement(ApiTestCase):
def test_delete_anonymous(self): def test_delete_anonymous(self):
self._run_test('DELETE', 403, None, None) self._run_test('DELETE', 401, None, None)
def test_delete_freshuser(self): def test_delete_freshuser(self):
self._run_test('DELETE', 403, 'freshuser', None) self._run_test('DELETE', 403, 'freshuser', None)