open-nomad/ui/tests/acceptance/token-test.js
Buck Doyle 6d67e90763
Add exchange of one-time token on UI load (#10066)
This adds UI support for receiving the one-time token passed via query parameter, as in #10134
and related PRs, and exchanging it for its corresponding secret ID. When this works, it’s mostly
invisible, with a brief flash of the OTT onscreen.

The authentication failure message now suggests the -authenticate flag.

When OTT exchange fails, it shows a whole-page error.

This includes a known UX shortcoming in that the OTT will not disappear from the URL when an
identifier is specified on the command line, like nomad ui -authenticate jobname. The goal is to
address that shortcoming in a forthcoming pull request.
2021-04-01 13:21:30 -05:00

194 lines
6.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { find, visit } from '@ember/test-helpers';
import { module, skip, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';
import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
import Tokens from 'nomad-ui/tests/pages/settings/tokens';
import Jobs from 'nomad-ui/tests/pages/jobs/list';
import JobDetail from 'nomad-ui/tests/pages/jobs/detail';
import ClientDetail from 'nomad-ui/tests/pages/clients/detail';
import Layout from 'nomad-ui/tests/pages/layout';
let job;
let node;
let managementToken;
let clientToken;
module('Acceptance | tokens', function(hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
hooks.beforeEach(function() {
window.localStorage.clear();
window.sessionStorage.clear();
server.create('agent');
node = server.create('node');
job = server.create('job');
managementToken = server.create('token');
clientToken = server.create('token');
});
test('it passes an accessibility audit', async function(assert) {
await Tokens.visit();
await a11yAudit(assert);
});
test('the token form sets the token in local storage', async function(assert) {
const { secretId } = managementToken;
await Tokens.visit();
assert.ok(window.localStorage.nomadTokenSecret == null, 'No token secret set');
assert.equal(document.title, 'Tokens - Nomad');
await Tokens.secret(secretId).submit();
assert.equal(window.localStorage.nomadTokenSecret, secretId, 'Token secret was set');
});
// TODO: unskip once store.unloadAll reliably waits for in-flight requests to settle
skip('the x-nomad-token header gets sent with requests once it is set', async function(assert) {
const { secretId } = managementToken;
await JobDetail.visit({ id: job.id });
await ClientDetail.visit({ id: node.id });
assert.ok(server.pretender.handledRequests.length > 1, 'Requests have been made');
server.pretender.handledRequests.forEach(req => {
assert.notOk(getHeader(req, 'x-nomad-token'), `No token for ${req.url}`);
});
const requestPosition = server.pretender.handledRequests.length;
await Tokens.visit();
await Tokens.secret(secretId).submit();
await JobDetail.visit({ id: job.id });
await ClientDetail.visit({ id: node.id });
const newRequests = server.pretender.handledRequests.slice(requestPosition);
assert.ok(newRequests.length > 1, 'New requests have been made');
// Cross-origin requests can't have a token
newRequests.forEach(req => {
assert.equal(getHeader(req, 'x-nomad-token'), secretId, `Token set for ${req.url}`);
});
});
test('an error message is shown when authenticating a token fails', async function(assert) {
const { secretId } = managementToken;
const bogusSecret = 'this-is-not-the-secret';
assert.notEqual(
secretId,
bogusSecret,
'bogus secret is not somehow coincidentally equal to the real secret'
);
await Tokens.visit();
await Tokens.secret(bogusSecret).submit();
assert.ok(window.localStorage.nomadTokenSecret == null, 'Token secret is discarded on failure');
assert.ok(Tokens.errorMessage, 'Token error message is shown');
assert.notOk(Tokens.successMessage, 'Token success message is not shown');
assert.equal(Tokens.policies.length, 0, 'No token policies are shown');
});
test('a success message and a special management token message are shown when authenticating succeeds', async function(assert) {
const { secretId } = managementToken;
await Tokens.visit();
await Tokens.secret(secretId).submit();
assert.ok(Tokens.successMessage, 'Token success message is shown');
assert.notOk(Tokens.errorMessage, 'Token error message is not shown');
assert.ok(Tokens.managementMessage, 'Token management message is shown');
assert.equal(Tokens.policies.length, 0, 'No token policies are shown');
});
test('a success message and associated policies are shown when authenticating succeeds', async function(assert) {
const { secretId } = clientToken;
const policy = clientToken.policies.models[0];
policy.update('description', 'Make sure there is a description');
await Tokens.visit();
await Tokens.secret(secretId).submit();
assert.ok(Tokens.successMessage, 'Token success message is shown');
assert.notOk(Tokens.errorMessage, 'Token error message is not shown');
assert.notOk(Tokens.managementMessage, 'Token management message is not shown');
assert.equal(
Tokens.policies.length,
clientToken.policies.length,
'Each policy associated with the token is listed'
);
const policyElement = Tokens.policies.objectAt(0);
assert.equal(policyElement.name, policy.name, 'Policy Name');
assert.equal(policyElement.description, policy.description, 'Policy Description');
assert.equal(policyElement.rules, policy.rules, 'Policy Rules');
});
test('setting a token clears the store', async function(assert) {
const { secretId } = clientToken;
await Jobs.visit();
assert.ok(find('.job-row'), 'Jobs found');
await Tokens.visit();
await Tokens.secret(secretId).submit();
server.pretender.get('/v1/jobs', function() {
return [200, {}, '[]'];
});
await Jobs.visit();
// If jobs are lingering in the store, they would show up
assert.notOk(find('[data-test-job-row]'), 'No jobs found');
});
test('when namespaces are enabled, setting or clearing a token refetches namespaces available with new permissions', async function(assert) {
const { secretId } = clientToken;
server.createList('namespace', 2);
await Tokens.visit();
const requests = server.pretender.handledRequests;
assert.equal(requests.filter(req => req.url === '/v1/namespaces').length, 1);
await Tokens.secret(secretId).submit();
assert.equal(requests.filter(req => req.url === '/v1/namespaces').length, 2);
await Tokens.clear();
assert.equal(requests.filter(req => req.url === '/v1/namespaces').length, 3);
});
test('when the ott query parameter is present upon application load its exchanged for a token', async function(assert) {
const { oneTimeSecret, secretId } = managementToken;
await JobDetail.visit({ id: job.id, ott: oneTimeSecret });
await Tokens.visit();
assert.equal(window.localStorage.nomadTokenSecret, secretId, 'Token secret was set');
});
test('when the ott exchange fails an error is shown', async function(assert) {
await visit('/?ott=fake');
assert.ok(Layout.error.isPresent);
assert.equal(Layout.error.title, 'Token Exchange Error');
assert.equal(Layout.error.message, 'Failed to exchange the one-time token.');
});
function getHeader({ requestHeaders }, name) {
// Headers are case-insensitive, but object property look up is not
return (
requestHeaders[name] ||
requestHeaders[name.toLowerCase()] ||
requestHeaders[name.toUpperCase()]
);
}
});