From 372937d7b567caa381e900dcf06ba7dbeeadab29 Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Wed, 6 Nov 2024 17:04:18 -0500 Subject: [PATCH] Tests for token expiry re-routing --- ui/app/components/forbidden-message.js | 1 + ui/app/controllers/settings/tokens.js | 3 +- ui/app/routes/application.js | 1 + .../components/forbidden-message.hbs | 4 +- ui/tests/acceptance/token-test.js | 112 ++++++++++++++++++ 5 files changed, 118 insertions(+), 3 deletions(-) diff --git a/ui/app/components/forbidden-message.js b/ui/app/components/forbidden-message.js index a864fa94522..ef0c5d59d5f 100644 --- a/ui/app/components/forbidden-message.js +++ b/ui/app/components/forbidden-message.js @@ -11,6 +11,7 @@ import { inject as service } from '@ember/service'; export default class ForbiddenMessage extends Component { @service token; @service store; + @service router; get authMethods() { return this.store.findAll('auth-method'); diff --git a/ui/app/controllers/settings/tokens.js b/ui/app/controllers/settings/tokens.js index 4ddd552512b..957ebfb62d4 100644 --- a/ui/app/controllers/settings/tokens.js +++ b/ui/app/controllers/settings/tokens.js @@ -200,7 +200,8 @@ export default class Tokens extends Controller { title: 'Successfully signed in', message: 'You were redirected back to the page you were on before you were signed out.', - type: 'success', + color: 'success', + timeout: 10000, }); } } diff --git a/ui/app/routes/application.js b/ui/app/routes/application.js index b2cc7effaa1..d45418bc12c 100644 --- a/ui/app/routes/application.js +++ b/ui/app/routes/application.js @@ -153,6 +153,7 @@ export default class ApplicationRoute extends Route { e.detail === 'ACL token not found' ) ) { + this.token.postExpiryPath = this.router.currentURL; this.router.transitionTo('settings.tokens'); } else { this.controllerFor('application').set('error', error); diff --git a/ui/app/templates/components/forbidden-message.hbs b/ui/app/templates/components/forbidden-message.hbs index 42c29977ce4..502d2235dce 100644 --- a/ui/app/templates/components/forbidden-message.hbs +++ b/ui/app/templates/components/forbidden-message.hbs @@ -13,12 +13,12 @@ {{else}} required {{/if}} - permission for this resource.
Contact your administrator if this is an error. + permission for this resource.
Contact your administrator if this is an error. {{else}} {{#if this.authMethods}} Sign in with {{#each this.authMethods as |authMethod|}} - {{authMethod.name}}, + {{authMethod.name}}, {{/each}} or {{/if}} diff --git a/ui/tests/acceptance/token-test.js b/ui/tests/acceptance/token-test.js index c29dd44b200..fb68c711889 100644 --- a/ui/tests/acceptance/token-test.js +++ b/ui/tests/acceptance/token-test.js @@ -36,6 +36,8 @@ let job; let node; let managementToken; let clientToken; +let recentlyExpiredToken; +let soonExpiringToken; module('Acceptance | tokens', function (hooks) { setupApplicationTest(hooks); @@ -53,6 +55,13 @@ module('Acceptance | tokens', function (hooks) { job = server.create('job'); managementToken = server.create('token'); clientToken = server.create('token'); + recentlyExpiredToken = server.create('token', { + expirationTime: moment().add(-5, 'm').toDate(), + }); + soonExpiringToken = server.create('token', { + expirationTime: moment().add(1, 's').toDate(), + type: 'management', + }); }); test('it passes an accessibility audit', async function (assert) { @@ -757,6 +766,109 @@ module('Acceptance | tokens', function (hooks) { window.localStorage.nomadTokenSecret = null; }); + // Note: this differs from the 500-throwing errors above. + // In Nomad 1.5, errors for expired tokens moved from 500 "ACL token expired" to 403 "Permission Denied" + // In practice, the UI handles this differently: 403s can be either ACL-policy-denial or token-expired-denial related. + // As such, instead of an automatic redirect to the tokens page, like we did for a 500, we prompt the user with in-app + // error messages but otherwise keep them on their route, with actions to re-authenticate. + test('When a token expires with permission denial, the user is prompted to redirect to the token page (jobs page)', async function (assert) { + assert.expect(4); + window.localStorage.clear(); + + window.localStorage.nomadTokenSecret = recentlyExpiredToken.secretId; // simulate refreshing the page with an expired token + server.pretender.get('/v1/jobs/statuses', function () { + return [403, {}, 'Permission Denied']; + }); + + await visit('/jobs'); + + assert + .dom('[data-test-error]') + .exists('Error message is shown on the Jobs page'); + await click('[data-test-permission-link]'); + assert.equal( + currentURL(), + '/settings/tokens', + 'Redirected to the tokens page' + ); + + server.pretender.get('/v1/jobs/statuses', function () { + return [200, {}, null]; + }); + await Tokens.visit(); + + await Tokens.secret(recentlyExpiredToken.secretId).submit(); + assert.equal(currentURL(), '/jobs'); + + assert.dom('.flash-message.alert-success').exists(); + }); + + // Evaluations page (and others) fall back to application.hbs handling of error messages + test('When a token expires with permission denial, the user is prompted to redirect to the token page (evaluations page)', async function (assert) { + window.localStorage.clear(); + window.localStorage.nomadTokenSecret = recentlyExpiredToken.secretId; // simulate refreshing the page with an expired token + server.pretender.get('/v1/evaluations', function () { + return [403, {}, 'Permission Denied']; + }); + + await visit('/evaluations'); + + assert + .dom('[data-test-error]') + .exists('Error message is shown on the Evaluations page'); + await click('[data-test-error-acl-link]'); + assert.equal( + currentURL(), + '/settings/tokens', + 'Redirected to the tokens page' + ); + + server.pretender.get('/v1/evaluations', function () { + return [200, {}, JSON.stringify([])]; + }); + + await Tokens.secret(managementToken.secretId).submit(); + + assert.equal(currentURL(), '/evaluations'); + + assert.dom('.flash-message.alert-success').exists(); + }); + + module('Token Expiry and redirect', function (hooks) { + hooks.beforeEach(function () { + window.localStorage.nomadTokenSecret = soonExpiringToken.secretId; + }); + + test('When a token expires while the user is on a page, the notification saves redirect route', async function (assert) { + // window.localStorage.nomadTokenSecret = soonExpiringToken.secretId; + await Jobs.visit(); + assert.equal(currentURL(), '/jobs'); + + assert + .dom('.flash-message.alert-warning button') + .exists('A global alert exists and has a clickable button'); + + await click('.flash-message.alert-warning button'); + + assert.equal( + currentURL(), + '/settings/tokens', + 'Redirected to tokens page on notification action' + ); + + assert + .dom('[data-test-token-expired]') + .exists('Notification is rendered'); + + await Tokens.secret(managementToken.secretId).submit(); + assert.equal( + currentURL(), + '/jobs', + 'Redirected to initial route on manager sign in' + ); + }); + }); + function getHeader({ requestHeaders }, name) { // Headers are case-insensitive, but object property look up is not return (