add per user api keys to the backend (#995)

Adds in per user api keys to the backend of lemur.
the basics are:
  - API Keys are really just JWTs with custom second length TTLs.
  - API Keys are provided in the exact same ways JWTs are now.
  - API Keys can be revoked/unrevoked at any time by their creator
    as well as have their TTL Change at anytime.
  - Users can create/view/list their own API Keys at will, and
    an admin role has permission to modify all api keys in the
    instance.

Adds in support for lemur api keys to the frontend of lemur.
doing this required a few changes to the backend as well, but it is
now all working (maybe not the best way though, review will determine
that).

  - fixes inconsistency in moduleauthor name I inputted during the
    first commit.
  - Allows the revoke schema to optionally allow a full api_key object.
  - Adds `/users/:user_id/api_keys/:api_key` and `/users/:user_id/api_keys`
    endpoints.
  - normalizes use of `userId` vs `userId`
  - makes `put` call respond with a JWT so the frontend can show
    the token on updating.
  - adds in the API Key views for clicking "API Keys" on the main nav.
  - adds in the API Key views for clicking into a users edit page.
  - adds tests for the API Key backend views I added.
This commit is contained in:
Eric
2017-12-04 09:50:31 -07:00
committed by kevgliss
parent eb810f1bf0
commit c402f1ff87
35 changed files with 1578 additions and 20 deletions

View File

@ -0,0 +1,69 @@
'use strict';
angular.module('lemur')
.controller('ApiKeysCreateController', function ($scope, $uibModalInstance, PluginService, ApiKeyService, LemurRestangular, toaster) {
$scope.apiKey = LemurRestangular.restangularizeElement(null, {}, 'keys');
$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!'
});
$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();
};
});

View File

@ -0,0 +1,54 @@
<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>
</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>
</div>
<div class="modal-body" ng-show="!jwt">
<form name="createForm" class="form-horizontal" role="form" novalidate>
<div class="form-group"
ng-class="{'has-error': createForm.name.$invalid, 'has-success': !createForm.name.$invalid&&createForm.name.$dirty}">
<label class="control-label col-sm-2">
Name
</label>
<div class="col-sm-10">
<input name="name" ng-model="apiKey.name" placeholder="A Cool API Key" 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}">
<label class="control-label col-sm-2">
User ID
</label>
<div class="col-sm-10">
<input name="user_id" ng-model="apiKey.userId" placeholder="42" class="form-control" type="number" required/>
<p ng-show="createForm.label.$invalid && !createForm.label.$pristine" class="help-block">You must enter an API Key User ID.</p>
</div>
</div>
<div class="form-group"
ng-class="{'has-error': createForm.ttl.$invalid, 'has-success': !createForm.ttl.$invalid&&createForm.ttl.$dirty}">
<label class="control-label col-sm-2">
TTL
</label>
<div class="col-sm-10">
<input name="ttl" ng-model="apiKey.ttl" placeholder="-1" class="form-control" type="number" 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>
<div class="modal-footer" ng-show="!jwt">
<button ng-click="save(apiKey)" type="submit" ng-disabled="createForm.$invalid" class="btn btn-primary">Save</button>
<button ng-click="cancel()" class="btn btn-danger">Cancel</button>
</div>
<div class="modal-footer" ng-show="jwt">
<button ng-click="close()" class="btn btn-primary">Close</button>
</div>

View File

@ -0,0 +1,20 @@
'use strict';
angular.module('lemur')
.service('ApiKeyApi', function (LemurRestangular) {
return LemurRestangular.all('keys');
})
.service('ApiKeyService', function ($location, ApiKeyApi) {
var ApiKeyService = this;
ApiKeyService.update = function(apiKey) {
return apiKey.put();
};
ApiKeyService.create = function (apiKey) {
return ApiKeyApi.post(apiKey);
};
ApiKeyService.delete = function (apiKey) {
return apiKey.remove();
};
});

View File

@ -0,0 +1,104 @@
'use strict';
angular.module('lemur')
.config(function config($stateProvider) {
$stateProvider.state('keys', {
url: '/keys',
templateUrl: '/angular/api_keys/view/view.tpl.html',
controller: 'ApiKeysViewController'
});
})
.controller('ApiKeysViewController', function ($scope, $uibModal, ApiKeyApi, ApiKeyService, ngTableParams, toaster) {
$scope.filter = {};
$scope.apiKeysTable = new ngTableParams({
page: 1, // show first page
count: 10, // count per page
sorting: {
id: 'desc' // initial sorting
},
filter: $scope.filter
}, {
total: 0, // length of data
getData: function ($defer, params) {
ApiKeyApi.getList(params.url()).then(function (data) {
params.total(data.total);
$defer.resolve(data);
});
}
});
$scope.updateRevoked = function (apiKey) {
ApiKeyService.update(apiKey).then(
function () {
toaster.pop({
type: 'success',
title: 'Updated JWT!',
body: apiKey.jwt
});
},
function (response) {
toaster.pop({
type: 'error',
title: apiKey.name || 'Unnamed API Key',
body: 'Unable to update! ' + response.data.message,
timeout: 100000
});
});
};
$scope.create = function () {
var uibModalInstance = $uibModal.open({
animation: true,
controller: 'ApiKeysCreateController',
templateUrl: '/angular/api_keys/api_key/api_key.tpl.html',
size: 'lg',
backdrop: 'static'
});
uibModalInstance.result.then(function () {
$scope.apiKeysTable.reload();
});
};
$scope.remove = function (apiKey) {
apiKey.remove().then(
function () {
toaster.pop({
type: 'success',
title: 'Removed!',
body: 'Deleted that API Key!'
});
$scope.apiKeysTable.reload();
},
function (response) {
toaster.pop({
type: 'error',
title: 'Opps',
body: 'I see what you did there: ' + response.data.message
});
}
);
};
$scope.edit = function (apiKeyId) {
var uibModalInstance = $uibModal.open({
animation: true,
templateUrl: '/angular/api_keys/api_key/api_key.tpl.html',
controller: 'ApiKeysEditController',
size: 'lg',
backdrop: 'static',
resolve: {
editId: function () {
return apiKeyId;
}
}
});
uibModalInstance.result.then(function () {
$scope.apiKeysTable.reload();
});
};
});

View File

@ -0,0 +1,50 @@
<div class="row">
<div class="col-md-12">
<h2 class="featurette-heading">API Keys
<span class="text-muted"><small>For accidentally uploading to github</small></span></h2>
<div class="panel panel-default">
<div class="panel-heading">
<div class="btn-group pull-right">
<button ng-click="create()" class="btn btn-primary">Create</button>
</div>
<div class="clearfix"></div>
</div>
<div class="table-responsive">
<table ng-table="apiKeysTable" class="table table-striped" template-pagination="angular/pager.html">
<tbody>
<tr ng-repeat="apiKey in $data track by $index">
<td data-title="'ID'" align="center">
{{ apiKey.id }}
</td>
<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>
<td data-title="'TTL'" align="center">
{{ apiKey.ttl == -1 ? 'Forever' : apiKey.ttl }}
</td>
<td data-title="'Revoked'" align="center">
<form>
<switch ng-change="updateRevoked(apiKey)" id="status" name="status"
ng-model="apiKey.revoked" class="green small"></switch>
</form>
</td>
<td data-title="''">
<div class="btn-group-vertical pull-right">
<button uib-tooltip="Edit Key" ng-click="edit(apiKey.id)" class="btn btn-sm btn-info">
Edit
</button>
<button uib-tooltip="Delete Key" ng-click="remove(apiKey)" type="button" class="btn btn-sm btn-danger pull-left">
Remove
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>

View File

@ -3,7 +3,7 @@
*/
'use strict';
angular.module('lemur')
.service('UserApi', function (LemurRestangular) {
.service('UserApi', function (LemurRestangular, ApiKeyService) {
LemurRestangular.extendModel('users', function (obj) {
return angular.extend(obj, {
attachRole: function (role) {
@ -15,6 +15,11 @@ angular.module('lemur')
},
removeRole: function (index) {
this.roles.splice(index, 1);
},
removeApiKey: function (index) {
var removedApiKeys = this.apiKeys.splice(index, 1);
var removedApiKey = removedApiKeys[0];
return ApiKeyService.delete(removedApiKey);
}
});
});
@ -41,6 +46,12 @@ angular.module('lemur')
});
};
UserService.getApiKeys = function (user) {
user.getList('keys').then(function (apiKeys) {
user.apiKeys = apiKeys;
});
};
UserService.loadMoreRoles = function (user, page) {
user.getList('roles', {page: page}).then(function (roles) {
_.each(roles, function (role) {
@ -49,6 +60,14 @@ angular.module('lemur')
});
};
UserService.loadMoreApiKeys = function (user, page) {
user.getList('keys', {page: page}).then(function (apiKeys) {
_.each(apiKeys, function (apiKey) {
user.apiKeys.push(apiKey);
});
});
};
UserService.create = function (user) {
return UserApi.post(user);
};

View File

@ -2,14 +2,17 @@
angular.module('lemur')
.controller('UsersEditController', function ($scope, $uibModalInstance, UserApi, UserService, RoleService, toaster, editId) {
.controller('UsersEditController', function ($scope, $uibModalInstance, UserApi, UserService, RoleService, ApiKeyService, toaster, editId) {
UserApi.get(editId).then(function (user) {
$scope.user = user;
UserService.getApiKeys(user);
});
$scope.roleService = RoleService;
$scope.apiKeyService = ApiKeyService;
$scope.rolePage = 1;
$scope.apiKeyPage = 1;
$scope.save = function (user) {
UserService.update(user).then(
@ -36,15 +39,40 @@ angular.module('lemur')
$uibModalInstance.dismiss('cancel');
};
$scope.removeApiKey = function (idx) {
UserApi.removeApiKey(idx).then(function () {
toaster.pop({
type: 'success',
title: 'Removed API Key!',
body: 'Successfully removed the api key!'
});
}, function(err) {
toaster.pop({
type: 'error',
title: 'Failed to remove API Key!',
body: 'lemur-bad-request',
bodyOutputType: 'directive',
directiveData: err,
timeout: 100000
});
});
};
$scope.loadMoreRoles = function () {
$scope.rolePage += 1;
UserService.loadMoreRoles($scope.user, $scope.rolePage);
};
$scope.loadMoreApiKeys = function () {
$scope.apiKeyPage += 1;
UserService.loadMoreApiKeys($scope.user, $scope.apiKeyPage);
};
})
.controller('UsersCreateController', function ($scope, $uibModalInstance, UserService, LemurRestangular, RoleService, toaster) {
.controller('UsersCreateController', function ($scope, $uibModalInstance, UserService, LemurRestangular, RoleService, ApiKeyService, toaster) {
$scope.user = LemurRestangular.restangularizeElement(null, {}, 'users');
$scope.roleService = RoleService;
$scope.apiKeyService = ApiKeyService;
$scope.save = function (user) {
UserService.create(user).then(

View File

@ -85,6 +85,27 @@
</table>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">
API Keys
</label>
<div class="col-sm-10">
<table ng-show="user.apiKeys" class="table">
<tr ng-repeat="apiKey in user.apiKeys track by $index">
<td><a class="btn btn-sm btn-info" href="#/keys/{{ apiKey.id }}/edit">{{ apiKey.name || "Unnamed Api Key" }}</a></td>
<td>
<button type="button" ng-click="user.removeApiKey($index)" class="btn btn-danger btn-sm pull-right">Remove
</button>
</td>
</tr>
<tr>
<td></td>
<td></td>
<td><a class="pull-right" ng-click="loadMoreApiKeys()"><strong>More</strong></a></td>
</tr>
</table>
</div>
</div>
</form>
<div class="modal-footer">
<button ng-click="save(user)" type="submit" ng-disabled="createForm.$invalid" class="btn btn-primary">Save</button>

View File

@ -63,6 +63,7 @@
<li><a ui-sref="users">Users</a></li>
<li><a ui-sref="domains">Domains</a></li>
<li><a ui-sref="logs">Logs</a></li>
<li><a ui-sref="keys">Api Keys</a></li>
</ul>
</li>
</ul>