From 888b564a9bee3749c2eedd42034242f1825de710 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 12 Dec 2017 17:51:54 -0500 Subject: [PATCH] Add a banner to the Quay UI when an app specific token is about to expire --- auth/test/test_basic.py | 1 - auth/test/test_credentials.py | 4 +-- data/model/appspecifictoken.py | 6 +++- endpoints/api/appspecifictokens.py | 29 +++++++++++++++++-- endpoints/api/test/test_appspecifictoken.py | 4 +++ static/directives/quay-message-bar.html | 7 +++++ static/js/directives/quay-message-bar.js | 4 ++- .../app-specific-token-manager.component.ts | 6 +++- static/js/services/notification-service.js | 8 +++++ 9 files changed, 60 insertions(+), 9 deletions(-) diff --git a/auth/test/test_basic.py b/auth/test/test_basic.py index 0bfb60606..a25fe8b50 100644 --- a/auth/test/test_basic.py +++ b/auth/test/test_basic.py @@ -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) diff --git a/auth/test/test_credentials.py b/auth/test/test_credentials.py index 4b795ed6d..1000258c5 100644 --- a/auth/test/test_credentials.py +++ b/auth/test/test_credentials.py @@ -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) diff --git a/data/model/appspecifictoken.py b/data/model/appspecifictoken.py index 37489326e..a9df58606 100644 --- a/data/model/appspecifictoken.py +++ b/data/model/appspecifictoken.py @@ -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. """ diff --git a/endpoints/api/appspecifictokens.py b/endpoints/api/appspecifictokens.py index 3e3525dc1..a387d82bb 100644 --- a/endpoints/api/appspecifictokens.py +++ b/endpoints/api/appspecifictokens.py @@ -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 diff --git a/endpoints/api/test/test_appspecifictoken.py b/endpoints/api/test/test_appspecifictoken.py index e9e95fc59..f71c306e7 100644 --- a/endpoints/api/test/test_appspecifictoken.py +++ b/endpoints/api/test/test_appspecifictoken.py @@ -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 diff --git a/static/directives/quay-message-bar.html b/static/directives/quay-message-bar.html index cc6de4a1c..ccf3ca2ce 100644 --- a/static/directives/quay-message-bar.html +++ b/static/directives/quay-message-bar.html @@ -1,4 +1,11 @@
+
+
+ Your external application token {{ token.title }} + will be expiring . + Please create a new token and revoke this token in user settings. +
+
diff --git a/static/js/directives/quay-message-bar.js b/static/js/directives/quay-message-bar.js index 9d3954d6b..0c4c04a03 100644 --- a/static/js/directives/quay-message-bar.js +++ b/static/js/directives/quay-message-bar.js @@ -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) { diff --git a/static/js/directives/ui/app-specific-token-manager/app-specific-token-manager.component.ts b/static/js/directives/ui/app-specific-token-manager/app-specific-token-manager.component.ts index 640a96d3d..19431e0c6 100644 --- a/static/js/directives/ui/app-specific-token-manager/app-specific-token-manager.component.ts +++ b/static/js/directives/ui/app-specific-token-manager/app-specific-token-manager.component.ts @@ -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); } diff --git a/static/js/services/notification-service.js b/static/js/services/notification-service.js index ab8a118c5..ef856e735 100644 --- a/static/js/services/notification-service.js +++ b/static/js/services/notification-service.js @@ -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() {