Add a banner to the Quay UI when an app specific token is about to expire
This commit is contained in:
parent
5b4f5f9859
commit
888b564a9b
9 changed files with 60 additions and 9 deletions
|
@ -74,5 +74,4 @@ def test_valid_app_specific_token(app):
|
|||
app_specific_token = model.appspecifictoken.create_token(user, 'some token')
|
||||
token = _token(APP_SPECIFIC_TOKEN_USERNAME, app_specific_token.token_code)
|
||||
result = validate_basic_auth(token)
|
||||
print result.tuple()
|
||||
assert result == ValidateResult(AuthKind.basic, appspecifictoken=app_specific_token)
|
||||
|
|
|
@ -17,7 +17,7 @@ def test_valid_robot(app):
|
|||
assert result == ValidateResult(AuthKind.credentials, robot=robot)
|
||||
|
||||
def test_valid_robot_for_disabled_user(app):
|
||||
user = model.user.get_user('devtable')
|
||||
user = model.user.get_user('devtable')
|
||||
user.enabled = False
|
||||
user.save()
|
||||
|
||||
|
@ -50,7 +50,7 @@ def test_invalid_user(app):
|
|||
def test_valid_app_specific_token(app):
|
||||
user = model.user.get_user('devtable')
|
||||
app_specific_token = model.appspecifictoken.create_token(user, 'some token')
|
||||
|
||||
|
||||
result, kind = validate_credentials(APP_SPECIFIC_TOKEN_USERNAME, app_specific_token.token_code)
|
||||
assert kind == CredentialKind.app_specific_token
|
||||
assert result == ValidateResult(AuthKind.credentials, appspecifictoken=app_specific_token)
|
||||
|
|
|
@ -11,13 +11,17 @@ from util.timedeltastring import convert_to_timedelta
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _default_expiration():
|
||||
expiration_str = config.app_config.get('APP_SPECIFIC_TOKEN_EXPIRATION')
|
||||
return datetime.now() + convert_to_timedelta(expiration_str) if expiration_str else expiration_str
|
||||
|
||||
|
||||
_default_expiration_opt = 'deo'
|
||||
# Define a "unique" value so that callers can specifiy an expiration of None and *not* have it
|
||||
# use the default.
|
||||
_default_expiration_opt = '__deo'
|
||||
|
||||
def create_token(user, title, expiration=_default_expiration_opt):
|
||||
""" Creates and returns an app specific token for the given user. If no expiration is specified
|
||||
(including `None`), then the default from config is used. """
|
||||
|
|
|
@ -1,19 +1,25 @@
|
|||
""" Manages app specific tokens for the current user. """
|
||||
|
||||
import logging
|
||||
import math
|
||||
|
||||
from datetime import timedelta
|
||||
from flask import request
|
||||
|
||||
import features
|
||||
|
||||
from app import app
|
||||
from auth.auth_context import get_authenticated_user
|
||||
from data import model
|
||||
from endpoints.api import (ApiResource, nickname, resource, validate_json_request,
|
||||
log_action, require_user_admin, require_fresh_login,
|
||||
path_param, NotFound, format_date, show_if)
|
||||
path_param, NotFound, format_date, show_if, query_param, parse_args,
|
||||
truthy_bool)
|
||||
from util.timedeltastring import convert_to_timedelta
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def token_view(token, include_code=False):
|
||||
data = {
|
||||
'uuid': token.uuid,
|
||||
|
@ -30,6 +36,11 @@ def token_view(token, include_code=False):
|
|||
|
||||
return data
|
||||
|
||||
|
||||
# The default window to use when looking up tokens that will be expiring.
|
||||
_DEFAULT_TOKEN_EXPIRATION_WINDOW = '4w'
|
||||
|
||||
|
||||
@resource('/v1/user/apptoken')
|
||||
@show_if(features.APP_SPECIFIC_TOKENS)
|
||||
class AppTokens(ApiResource):
|
||||
|
@ -51,11 +62,23 @@ class AppTokens(ApiResource):
|
|||
|
||||
@require_user_admin
|
||||
@nickname('listAppTokens')
|
||||
def get(self):
|
||||
@parse_args()
|
||||
@query_param('expiring', 'If true, only returns those tokens expiring soon', type=truthy_bool)
|
||||
def get(self, parsed_args):
|
||||
""" Lists the app specific tokens for the user. """
|
||||
tokens = model.appspecifictoken.list_tokens(get_authenticated_user())
|
||||
expiring = parsed_args['expiring']
|
||||
if expiring:
|
||||
expiration = app.config.get('APP_SPECIFIC_TOKEN_EXPIRATION')
|
||||
token_expiration = convert_to_timedelta(expiration or _DEFAULT_TOKEN_EXPIRATION_WINDOW)
|
||||
seconds = math.ceil(token_expiration.total_seconds() * 0.1) or 1
|
||||
soon = timedelta(seconds=seconds)
|
||||
tokens = model.appspecifictoken.get_expiring_tokens(get_authenticated_user(), soon)
|
||||
else:
|
||||
tokens = model.appspecifictoken.list_tokens(get_authenticated_user())
|
||||
|
||||
return {
|
||||
'tokens': [token_view(token, include_code=False) for token in tokens],
|
||||
'only_expiring': expiring,
|
||||
}
|
||||
|
||||
@require_user_admin
|
||||
|
|
|
@ -17,6 +17,10 @@ def test_app_specific_tokens(app, client):
|
|||
assert token_uuid in set([token['uuid'] for token in resp['tokens']])
|
||||
assert not set([token['token_code'] for token in resp['tokens'] if 'token_code' in token])
|
||||
|
||||
# List the tokens expiring soon and ensure the one added is not present.
|
||||
resp = conduct_api_call(cl, AppTokens, 'GET', {'expiring': True}, None, 200).json
|
||||
assert token_uuid not in set([token['uuid'] for token in resp['tokens']])
|
||||
|
||||
# Get the token and ensure we have its code.
|
||||
resp = conduct_api_call(cl, AppToken, 'GET', {'token_uuid': token_uuid}, None, 200).json
|
||||
assert resp['token']['uuid'] == token_uuid
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
<div class="announcement inline quay-message-bar-element" ng-show="messages.length">
|
||||
<div ng-repeat="token in NotificationService.expiringAppTokens">
|
||||
<div class="quay-service-status-description warning">
|
||||
Your external application token <strong style="display: inline-block; padding: 4px;">{{ token.title }}</strong>
|
||||
will be expiring <strong style="display: inline-block; padding: 4px;"><time-ago datetime="token.expiration"></time-ago></strong>.
|
||||
Please create a new token and revoke this token in user settings.
|
||||
</div>
|
||||
</div>
|
||||
<div ng-repeat="message in messages">
|
||||
<div class="quay-service-status-description" ng-class="message.severity">
|
||||
<span ng-switch on="message.media_type">
|
||||
|
|
|
@ -9,8 +9,10 @@ angular.module('quay').directive('quayMessageBar', function () {
|
|||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {},
|
||||
controller: function ($scope, $element, ApiService) {
|
||||
controller: function ($scope, $element, ApiService, NotificationService) {
|
||||
$scope.messages = [];
|
||||
$scope.NotificationService = NotificationService;
|
||||
|
||||
ApiService.getGlobalMessages().then(function (data) {
|
||||
$scope.messages = data['messages'] || [];
|
||||
}, function (resp) {
|
||||
|
|
|
@ -14,7 +14,8 @@ export class AppSpecificTokenManagerComponent {
|
|||
private tokenCredentials: any;
|
||||
private revokeTokenInfo: any;
|
||||
|
||||
constructor(@Inject('ApiService') private ApiService: any, @Inject('UserService') private UserService: any) {
|
||||
constructor(@Inject('ApiService') private ApiService: any, @Inject('UserService') private UserService: any,
|
||||
@Inject('NotificationService') private NotificationService: any) {
|
||||
this.loadTokens();
|
||||
}
|
||||
|
||||
|
@ -49,6 +50,9 @@ export class AppSpecificTokenManagerComponent {
|
|||
|
||||
this.ApiService.revokeAppToken(null, params).then((resp) => {
|
||||
this.loadTokens();
|
||||
|
||||
// Update the notification service so it hides any banners if we revoked an expiring token.
|
||||
this.NotificationService.update();
|
||||
callback(true);
|
||||
}, errorHandler);
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ function($rootScope, $interval, UserService, ApiService, StringBuilderService, P
|
|||
'notifications': [],
|
||||
'notificationClasses': [],
|
||||
'notificationSummaries': [],
|
||||
'expiringAppTokens': [],
|
||||
'additionalNotifications': false
|
||||
};
|
||||
|
||||
|
@ -272,6 +273,13 @@ function($rootScope, $interval, UserService, ApiService, StringBuilderService, P
|
|||
notificationService.additionalNotifications = resp['additional'];
|
||||
notificationService.notificationClasses = notificationService.getClasses(notificationService.notifications);
|
||||
});
|
||||
|
||||
var params = {
|
||||
'expiring': true
|
||||
};
|
||||
ApiService.listAppTokens(null, params).then(function(resp) {
|
||||
notificationService.expiringAppTokens = resp['tokens'];
|
||||
});
|
||||
};
|
||||
|
||||
notificationService.reset = function() {
|
||||
|
|
Reference in a new issue