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')
|
app_specific_token = model.appspecifictoken.create_token(user, 'some token')
|
||||||
token = _token(APP_SPECIFIC_TOKEN_USERNAME, app_specific_token.token_code)
|
token = _token(APP_SPECIFIC_TOKEN_USERNAME, app_specific_token.token_code)
|
||||||
result = validate_basic_auth(token)
|
result = validate_basic_auth(token)
|
||||||
print result.tuple()
|
|
||||||
assert result == ValidateResult(AuthKind.basic, appspecifictoken=app_specific_token)
|
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)
|
assert result == ValidateResult(AuthKind.credentials, robot=robot)
|
||||||
|
|
||||||
def test_valid_robot_for_disabled_user(app):
|
def test_valid_robot_for_disabled_user(app):
|
||||||
user = model.user.get_user('devtable')
|
user = model.user.get_user('devtable')
|
||||||
user.enabled = False
|
user.enabled = False
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
|
|
|
@ -11,13 +11,17 @@ from util.timedeltastring import convert_to_timedelta
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=1)
|
@lru_cache(maxsize=1)
|
||||||
def _default_expiration():
|
def _default_expiration():
|
||||||
expiration_str = config.app_config.get('APP_SPECIFIC_TOKEN_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
|
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):
|
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
|
""" 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. """
|
(including `None`), then the default from config is used. """
|
||||||
|
|
|
@ -1,19 +1,25 @@
|
||||||
""" Manages app specific tokens for the current user. """
|
""" Manages app specific tokens for the current user. """
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import math
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
from flask import request
|
from flask import request
|
||||||
|
|
||||||
import features
|
import features
|
||||||
|
|
||||||
|
from app import app
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
from data import model
|
from data import model
|
||||||
from endpoints.api import (ApiResource, nickname, resource, validate_json_request,
|
from endpoints.api import (ApiResource, nickname, resource, validate_json_request,
|
||||||
log_action, require_user_admin, require_fresh_login,
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def token_view(token, include_code=False):
|
def token_view(token, include_code=False):
|
||||||
data = {
|
data = {
|
||||||
'uuid': token.uuid,
|
'uuid': token.uuid,
|
||||||
|
@ -30,6 +36,11 @@ def token_view(token, include_code=False):
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
# The default window to use when looking up tokens that will be expiring.
|
||||||
|
_DEFAULT_TOKEN_EXPIRATION_WINDOW = '4w'
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/user/apptoken')
|
@resource('/v1/user/apptoken')
|
||||||
@show_if(features.APP_SPECIFIC_TOKENS)
|
@show_if(features.APP_SPECIFIC_TOKENS)
|
||||||
class AppTokens(ApiResource):
|
class AppTokens(ApiResource):
|
||||||
|
@ -51,11 +62,23 @@ class AppTokens(ApiResource):
|
||||||
|
|
||||||
@require_user_admin
|
@require_user_admin
|
||||||
@nickname('listAppTokens')
|
@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. """
|
""" 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 {
|
return {
|
||||||
'tokens': [token_view(token, include_code=False) for token in tokens],
|
'tokens': [token_view(token, include_code=False) for token in tokens],
|
||||||
|
'only_expiring': expiring,
|
||||||
}
|
}
|
||||||
|
|
||||||
@require_user_admin
|
@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 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])
|
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.
|
# Get the token and ensure we have its code.
|
||||||
resp = conduct_api_call(cl, AppToken, 'GET', {'token_uuid': token_uuid}, None, 200).json
|
resp = conduct_api_call(cl, AppToken, 'GET', {'token_uuid': token_uuid}, None, 200).json
|
||||||
assert resp['token']['uuid'] == token_uuid
|
assert resp['token']['uuid'] == token_uuid
|
||||||
|
|
|
@ -1,4 +1,11 @@
|
||||||
<div class="announcement inline quay-message-bar-element" ng-show="messages.length">
|
<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 ng-repeat="message in messages">
|
||||||
<div class="quay-service-status-description" ng-class="message.severity">
|
<div class="quay-service-status-description" ng-class="message.severity">
|
||||||
<span ng-switch on="message.media_type">
|
<span ng-switch on="message.media_type">
|
||||||
|
|
|
@ -9,8 +9,10 @@ angular.module('quay').directive('quayMessageBar', function () {
|
||||||
transclude: false,
|
transclude: false,
|
||||||
restrict: 'C',
|
restrict: 'C',
|
||||||
scope: {},
|
scope: {},
|
||||||
controller: function ($scope, $element, ApiService) {
|
controller: function ($scope, $element, ApiService, NotificationService) {
|
||||||
$scope.messages = [];
|
$scope.messages = [];
|
||||||
|
$scope.NotificationService = NotificationService;
|
||||||
|
|
||||||
ApiService.getGlobalMessages().then(function (data) {
|
ApiService.getGlobalMessages().then(function (data) {
|
||||||
$scope.messages = data['messages'] || [];
|
$scope.messages = data['messages'] || [];
|
||||||
}, function (resp) {
|
}, function (resp) {
|
||||||
|
|
|
@ -14,7 +14,8 @@ export class AppSpecificTokenManagerComponent {
|
||||||
private tokenCredentials: any;
|
private tokenCredentials: any;
|
||||||
private revokeTokenInfo: 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();
|
this.loadTokens();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,6 +50,9 @@ export class AppSpecificTokenManagerComponent {
|
||||||
|
|
||||||
this.ApiService.revokeAppToken(null, params).then((resp) => {
|
this.ApiService.revokeAppToken(null, params).then((resp) => {
|
||||||
this.loadTokens();
|
this.loadTokens();
|
||||||
|
|
||||||
|
// Update the notification service so it hides any banners if we revoked an expiring token.
|
||||||
|
this.NotificationService.update();
|
||||||
callback(true);
|
callback(true);
|
||||||
}, errorHandler);
|
}, errorHandler);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ function($rootScope, $interval, UserService, ApiService, StringBuilderService, P
|
||||||
'notifications': [],
|
'notifications': [],
|
||||||
'notificationClasses': [],
|
'notificationClasses': [],
|
||||||
'notificationSummaries': [],
|
'notificationSummaries': [],
|
||||||
|
'expiringAppTokens': [],
|
||||||
'additionalNotifications': false
|
'additionalNotifications': false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -272,6 +273,13 @@ function($rootScope, $interval, UserService, ApiService, StringBuilderService, P
|
||||||
notificationService.additionalNotifications = resp['additional'];
|
notificationService.additionalNotifications = resp['additional'];
|
||||||
notificationService.notificationClasses = notificationService.getClasses(notificationService.notifications);
|
notificationService.notificationClasses = notificationService.getClasses(notificationService.notifications);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
'expiring': true
|
||||||
|
};
|
||||||
|
ApiService.listAppTokens(null, params).then(function(resp) {
|
||||||
|
notificationService.expiringAppTokens = resp['tokens'];
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
notificationService.reset = function() {
|
notificationService.reset = function() {
|
||||||
|
|
Reference in a new issue