Add a banner to the Quay UI when an app specific token is about to expire

This commit is contained in:
Joseph Schorr 2017-12-12 17:51:54 -05:00
parent 5b4f5f9859
commit 888b564a9b
9 changed files with 60 additions and 9 deletions

View file

@ -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)

View file

@ -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)

View file

@ -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. """

View file

@ -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

View file

@ -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

View file

@ -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">

View file

@ -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) {

View file

@ -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);
}

View file

@ -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() {