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:
parent
a756a74b49
commit
ad88637f22
@ -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()
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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">×</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">×</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>
|
||||
|
||||
|
@ -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();
|
||||
};
|
||||
|
@ -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 }}
|
||||
|
@ -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", [
|
||||
|
@ -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',)
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user