Adding some niceties around the way users are associated with tokens. (#1012)

* Adding some niceties around the way users are associated with tokens.

- Includes user typeahead
- Tooltips
- User information displayed in table
- Default to current user when no user is passed
This commit is contained in:
kevgliss 2017-12-05 10:57:17 -08:00 committed by GitHub
parent a756a74b49
commit ad88637f22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 118 additions and 63 deletions

View File

@ -5,21 +5,27 @@
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Eric Coan <kungfury@instructure.com>
"""
from flask import g
from marshmallow import fields
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
from lemur.users.schemas import UserNestedOutputSchema, UserInputSchema
def current_user_id():
return {'id': g.current_user.id, 'email': g.current_user.email, 'username': g.current_user.username}
class ApiKeyInputSchema(LemurInputSchema):
name = fields.String(required=False)
user_id = fields.Integer()
user = fields.Nested(UserInputSchema, missing=current_user_id, default=current_user_id)
ttl = fields.Integer()
class ApiKeyRevokeSchema(LemurInputSchema):
id = fields.Integer(required=False)
id = fields.Integer(required=True)
name = fields.String()
user_id = fields.Integer(required=False)
user = fields.Nested(UserInputSchema, required=True)
revoked = fields.Boolean()
ttl = fields.Integer()
issued_at = fields.Integer(required=False)
@ -37,7 +43,7 @@ class ApiKeyOutputSchema(LemurOutputSchema):
class ApiKeyDescribedOutputSchema(LemurOutputSchema):
id = fields.Integer()
name = fields.String()
user_id = fields.Integer()
user = fields.Nested(UserNestedOutputSchema)
ttl = fields.Integer()
issued_at = fields.Integer()
revoked = fields.Boolean()

View File

@ -28,6 +28,7 @@ api = Api(mod)
class ApiKeyList(AuthenticatedResource):
""" Defines the 'api_keys' endpoint """
def __init__(self):
super(ApiKeyList, self).__init__()
@ -123,16 +124,17 @@ class ApiKeyList(AuthenticatedResource):
:statuscode 403: unauthenticated
"""
if not ApiKeyCreatorPermission().can():
if data['user_id'] != g.current_user.id:
return dict(message="You are not authorized to create tokens for: {0}".format(data['user_id'])), 403
if data['user']['id'] != g.current_user.id:
return dict(message="You are not authorized to create tokens for: {0}".format(data['user']['username'])), 403
access_token = service.create(name=data['name'], user_id=data['user_id'], ttl=data['ttl'],
revoked=False, issued_at=int(datetime.utcnow().timestamp()))
access_token = service.create(name=data['name'], user_id=data['user']['id'], ttl=data['ttl'],
revoked=False, issued_at=int(datetime.utcnow().timestamp()))
return dict(jwt=create_token(access_token.user_id, access_token.id, access_token.ttl))
class ApiKeyUserList(AuthenticatedResource):
""" Defines the 'keys' endpoint on the 'users' endpoint. """
def __init__(self):
super(ApiKeyUserList, self).__init__()
@ -231,7 +233,7 @@ class ApiKeyUserList(AuthenticatedResource):
return dict(message="You are not authorized to create tokens for: {0}".format(user_id)), 403
access_token = service.create(name=data['name'], user_id=user_id, ttl=data['ttl'],
revoked=False, issued_at=int(datetime.utcnow().timestamp()))
revoked=False, issued_at=int(datetime.utcnow().timestamp()))
return dict(jwt=create_token(access_token.user_id, access_token.id, access_token.ttl))
@ -272,11 +274,14 @@ class ApiKeys(AuthenticatedResource):
:statuscode 403: unauthenticated
"""
access_key = service.get(aid)
if access_key is None:
return dict(message="This token does not exist!"), 404
if access_key.user_id != g.current_user.id:
if not ApiKeyCreatorPermission().can():
return dict(message="You are not authorized to view this token!"), 403
return dict(jwt=create_token(access_key.user_id, access_key.id, access_key.ttl))
@validate_schema(api_key_revoke_schema, api_key_output_schema)
@ -319,9 +324,11 @@ class ApiKeys(AuthenticatedResource):
access_key = service.get(aid)
if access_key is None:
return dict(message="This token does not exist!"), 404
if access_key.user_id != g.current_user.id:
if not ApiKeyCreatorPermission().can():
return dict(message="You are not authorized to update this token!"), 403
service.update(access_key, name=data['name'], revoked=data['revoked'], ttl=data['ttl'])
return dict(jwt=create_token(access_key.user_id, access_key.id, access_key.ttl))
@ -358,9 +365,11 @@ class ApiKeys(AuthenticatedResource):
access_key = service.get(aid)
if access_key is None:
return dict(message="This token does not exist!"), 404
if access_key.user_id != g.current_user.id:
if not ApiKeyCreatorPermission().can():
return dict(message="You are not authorized to delete this token!"), 403
service.delete(access_key)
return {'result': True}
@ -404,11 +413,15 @@ class UserApiKeys(AuthenticatedResource):
if uid != g.current_user.id:
if not ApiKeyCreatorPermission().can():
return dict(message="You are not authorized to view this token!"), 403
access_key = service.get(aid)
if access_key is None:
return dict(message="This token does not exist!"), 404
if access_key.user_id != uid:
return dict(message="You are not authorized to view this token!"), 403
return dict(jwt=create_token(access_key.user_id, access_key.id, access_key.ttl))
@validate_schema(api_key_revoke_schema, api_key_output_schema)
@ -451,11 +464,14 @@ class UserApiKeys(AuthenticatedResource):
if uid != g.current_user.id:
if not ApiKeyCreatorPermission().can():
return dict(message="You are not authorized to view this token!"), 403
access_key = service.get(aid)
if access_key is None:
return dict(message="This token does not exist!"), 404
if access_key.user_id != uid:
return dict(message="You are not authorized to update this token!"), 403
service.update(access_key, name=data['name'], revoked=data['revoked'], ttl=data['ttl'])
return dict(jwt=create_token(access_key.user_id, access_key.id, access_key.ttl))
@ -492,11 +508,14 @@ class UserApiKeys(AuthenticatedResource):
if uid != g.current_user.id:
if not ApiKeyCreatorPermission().can():
return dict(message="You are not authorized to view this token!"), 403
access_key = service.get(aid)
if access_key is None:
return dict(message="This token does not exist!"), 404
if access_key.user_id != uid:
return dict(message="You are not authorized to delete this token!"), 403
service.delete(access_key)
return {'result': True}
@ -545,9 +564,11 @@ class ApiKeysDescribed(AuthenticatedResource):
access_key = service.get(aid)
if access_key is None:
return dict(message="This token does not exist!"), 404
if access_key.user_id != g.current_user.id:
if not ApiKeyCreatorPermission().can():
return dict(message="You are not authorized to view this token!"), 403
return access_key

View File

@ -1,50 +1,18 @@
'use strict';
angular.module('lemur')
.controller('ApiKeysCreateController', function ($scope, $uibModalInstance, PluginService, ApiKeyService, LemurRestangular, toaster) {
.controller('ApiKeysCreateController', function ($scope, $uibModalInstance, PluginService, ApiKeyService, UserService, LemurRestangular, toaster) {
$scope.apiKey = LemurRestangular.restangularizeElement(null, {}, 'keys');
$scope.origin = window.location.origin;
$scope.save = function (apiKey) {
ApiKeyService.create(apiKey).then(
function (responseBody) {
toaster.pop({
type: 'success',
title: 'Success!',
body: 'Successfully Created JWT!'
});
$scope.jwt = responseBody.jwt;
}, function (response) {
toaster.pop({
type: 'error',
title: apiKey.name || 'Unnamed Api Key',
body: 'lemur-bad-request',
bodyOutputType: 'directive',
directiveData: response.data,
timeout: 100000
});
});
};
$scope.cancel = function () {
$uibModalInstance.dismiss('cancel');
};
$scope.close = function() {
$uibModalInstance.close();
};
})
.controller('ApiKeysEditController', function ($scope, $uibModalInstance, ApiKeyService, LemurRestangular, toaster, editId) {
LemurRestangular.one('keys', editId).customGET('described').then(function(apiKey) {
$scope.apiKey = apiKey;
});
$scope.save = function (apiKey) {
ApiKeyService.update(apiKey).then(
function (responseBody) {
toaster.pop({
type: 'success',
title: 'Success',
body: 'Successfully updated JWT!'
body: 'Successfully Created API Token!'
});
$scope.jwt = responseBody.jwt;
}, function (response) {
@ -66,4 +34,45 @@ angular.module('lemur')
$scope.close = function() {
$uibModalInstance.close();
};
$scope.userService = UserService;
})
.controller('ApiKeysEditController', function ($scope, $uibModalInstance, ApiKeyService, UserService, LemurRestangular, toaster, editId) {
LemurRestangular.one('keys', editId).customGET('described').then(function(apiKey) {
$scope.apiKey = apiKey;
$scope.selectedUser = apiKey.user;
});
$scope.origin = window.location.origin;
$scope.save = function (apiKey) {
ApiKeyService.update(apiKey).then(
function (responseBody) {
toaster.pop({
type: 'success',
title: 'Success',
body: 'Successfully updated API Token!'
});
$scope.jwt = responseBody.jwt;
}, function (response) {
toaster.pop({
type: 'error',
title: apiKey.name || 'Unnamed API Key',
body: 'lemur-bad-request',
bodyOutputType: 'directive',
directiveData: response.data,
timeout: 100000
});
});
};
$scope.cancel = function () {
$uibModalInstance.dismiss('cancel');
};
$scope.close = function() {
$uibModalInstance.close();
};
$scope.userService = UserService;
});

View File

@ -1,10 +1,13 @@
<div class="modal-header" ng-show="!jwt">
<button type="button" class="close" ng-click="cancel()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h3><span ng-show="!apiKey.fromServer">Create</span><span ng-show="apiKey.fromServer">Edit</span></h3>
<h3>
<span ng-show="!apiKey.fromServer">Create Token</span>
<span ng-show="apiKey.fromServer">Edit Token</span>
</h3>
</div>
<div class="modal-header" ng-show="jwt">
<button type="button" class="close" ng-click="close()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h3><span>Token</span></h3>
<h3><span>Token Pickup</span></h3>
</div>
<div class="modal-body" ng-show="!jwt">
<form name="createForm" class="form-horizontal" role="form" novalidate>
@ -14,17 +17,20 @@
Name
</label>
<div class="col-sm-10">
<input name="name" ng-model="apiKey.name" placeholder="A Cool API Key" class="form-control" required/>
<input name="name" ng-model="apiKey.name" placeholder="ExampleService" class="form-control" required/>
<p ng-show="createForm.label.$invalid && !createForm.label.$pristine" class="help-block">You must enter an api key name</p>
</div>
</div>
<div class="form-group"
ng-class="{'has-error': createForm.user_id.$invalid, 'has-success': !createForm.user_id.$invalid&&createForm.user_id.$dirty}">
<div class="form-group">
<label class="control-label col-sm-2">
User ID
User
</label>
<div class="col-sm-10">
<input name="user_id" ng-model="apiKey.userId" placeholder="42" class="form-control" type="number" required/>
<input type="text" ng-model="apiKey.user" placeholder="My username..."
uib-typeahead="user.username for user in userService.findUserByName($viewValue)" typeahead-loading="loadingUsers"
class="form-control input-md" typeahead-on-select="apiKey.attachUser($item)"
uib-tooltip="This user will be tied to the generated key. All key permissions will mirror the users. Current user is the default."
uib-tooltip-trigger="focus" uib-tooltip-placement="top" typeahead-wait-ms="500">
<p ng-show="createForm.label.$invalid && !createForm.label.$pristine" class="help-block">You must enter an API Key User ID.</p>
</div>
</div>
@ -34,15 +40,19 @@
TTL
</label>
<div class="col-sm-10">
<input name="ttl" ng-model="apiKey.ttl" placeholder="-1" class="form-control" type="number" required/>
<input name="ttl" ng-model="apiKey.ttl" placeholder="-1" class="form-control" type="number"
uib-tooltip="Number of days for the token to last. -1 meaning the token will not expire."
uib-tooltip-trigger="focus" uib-tooltip-placement="top" required/>
<p ng-show="createForm.label.$invalid && !createForm.label.$pristine" class="help-block">You must enter an API Key TTL.</p>
</div>
</div>
</form>
</div>
<div ng-show="jwt">
<h3>Successfully exported!</h3>
<h4>Your Token is: <pre><code>{{ jwt }}</code></pre></h4>
<div class="modal-body" ng-show="jwt">
<h4>Pass the following token on every Lemur API request:</h4>
<pre><code>{{ jwt }}</code></pre>
<h4>Example usuage:</h4>
<pre><code>curl -i {{ origin }}/certificates -H "Authorization: Bearer {{ jwt }}</code></pre>
</div>
<div class="modal-footer" ng-show="!jwt">
<button ng-click="save(apiKey)" type="submit" ng-disabled="createForm.$invalid" class="btn btn-primary">Save</button>
@ -51,4 +61,3 @@
<div class="modal-footer" ng-show="jwt">
<button ng-click="close()" class="btn btn-primary">Close</button>
</div>

View File

@ -2,10 +2,18 @@
angular.module('lemur')
.service('ApiKeyApi', function (LemurRestangular) {
LemurRestangular.extendModel('keys', function (obj) {
return angular.extend(obj, {
attachUser: function (user) {
this.user = user;
}
});
});
return LemurRestangular.all('keys');
})
.service('ApiKeyService', function ($location, ApiKeyApi) {
var ApiKeyService = this;
ApiKeyService.update = function(apiKey) {
return apiKey.put();
};

View File

@ -19,8 +19,8 @@
<td data-title="'Name'" sortable="'name'" align="center">
{{ apiKey.name || 'Unnamed Api Key' }}
</td>
<td data-title="'User ID'" sortable="'user_id'" align="center">
{{ apiKey.userId }}
<td data-title="'User'" align="center">
{{ apiKey.user.email }}
</td>
<td data-title="'TTL'" align="center">
{{ apiKey.ttl == -1 ? 'Forever' : apiKey.ttl }}

View File

@ -34,7 +34,7 @@ def test_api_key_list_post_invalid(client, token, status):
('', 0, 401)
])
def test_api_key_list_post_valid_self(client, user_id, token, status):
assert client.post(api.url_for(ApiKeyList), data=json.dumps({'name': 'a test token', 'userId': user_id, 'ttl': -1}), headers=token).status_code == status
assert client.post(api.url_for(ApiKeyList), data=json.dumps({'name': 'a test token', 'user': {'id': user_id, 'username': 'example', 'email': 'example@test.net'}, 'ttl': -1}), headers=token).status_code == status
@pytest.mark.parametrize("token,status", [
@ -44,7 +44,7 @@ def test_api_key_list_post_valid_self(client, user_id, token, status):
('', 401)
])
def test_api_key_list_post_valid_no_permission(client, token, status):
assert client.post(api.url_for(ApiKeyList), data=json.dumps({'name': 'a test token', 'userId': 2, 'ttl': -1}), headers=token).status_code == status
assert client.post(api.url_for(ApiKeyList), data=json.dumps({'name': 'a test token', 'user': {'id': 2, 'username': 'example', 'email': 'example@test.net'}, 'ttl': -1}), headers=token).status_code == status
@pytest.mark.parametrize("token,status", [
@ -94,7 +94,7 @@ def test_user_api_key_list_post_invalid(client, token, status):
('', 0, 401)
])
def test_user_api_key_list_post_valid_self(client, user_id, token, status):
assert client.post(api.url_for(ApiKeyUserList, user_id=1), data=json.dumps({'name': 'a test token', 'userId': user_id, 'ttl': -1}), headers=token).status_code == status
assert client.post(api.url_for(ApiKeyUserList, user_id=1), data=json.dumps({'name': 'a test token', 'user': {'id': user_id}, 'ttl': -1}), headers=token).status_code == status
@pytest.mark.parametrize("token,status", [
@ -104,7 +104,7 @@ def test_user_api_key_list_post_valid_self(client, user_id, token, status):
('', 401)
])
def test_user_api_key_list_post_valid_no_permission(client, token, status):
assert client.post(api.url_for(ApiKeyUserList, user_id=2), data=json.dumps({'name': 'a test token', 'userId': 2, 'ttl': -1}), headers=token).status_code == status
assert client.post(api.url_for(ApiKeyUserList, user_id=2), data=json.dumps({'name': 'a test token', 'user': {'id': 2}, 'ttl': -1}), headers=token).status_code == status
@pytest.mark.parametrize("token,status", [

View File

@ -44,6 +44,7 @@ class User(db.Model):
roles = relationship('Role', secondary=roles_users, passive_deletes=True, backref=db.backref('user'), lazy='dynamic')
certificates = relationship('Certificate', backref=db.backref('user'), lazy='dynamic')
authorities = relationship('Authority', backref=db.backref('user'), lazy='dynamic')
keys = relationship('ApiKey', backref=db.backref('user'), lazy='dynamic')
logs = relationship('Log', backref=db.backref('user'), lazy='dynamic')
sensitive_fields = ('password',)

View File

@ -12,6 +12,7 @@ from lemur.schemas import AssociatedRoleSchema, AssociatedCertificateSchema, Ass
class UserInputSchema(LemurInputSchema):
id = fields.Integer()
username = fields.String(required=True)
email = fields.Email(required=True)
password = fields.String() # TODO add complexity requirements