[ui] Token management interface on policy pages (#15435)
* basic-functionality demo for token CRUD * Styling for tokens crud * Tokens crud styles * Expires, not expiry * Mobile styles etc * Refresh and redirect rules for policy save and token creation * Delete method and associated serializer change * Ability-checking for tokens * Update policies acceptance tests to reflect new redirect rules * Token ability unit tests * Mirage config methods for token crud * Token CRUD acceptance tests * A couple visual diff snapshots * Add and Delete abilities referenced for token operations * Changing timeouts and adding a copy to clipboard action * replaced accessor with secret when copying to clipboard * PR comments addressed * Simplified error passing for policy editor
This commit is contained in:
parent
b0730ebb02
commit
dce8717866
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
ui: The web UI now provides a Token Management interface for management users on policy pages
|
||||
```
|
|
@ -0,0 +1,10 @@
|
|||
import AbstractAbility from './abstract';
|
||||
import { alias } from '@ember/object/computed';
|
||||
|
||||
export default class extends AbstractAbility {
|
||||
@alias('selfTokenIsManagement') canRead;
|
||||
@alias('selfTokenIsManagement') canList;
|
||||
@alias('selfTokenIsManagement') canWrite;
|
||||
@alias('selfTokenIsManagement') canUpdate;
|
||||
@alias('selfTokenIsManagement') canDestroy;
|
||||
}
|
|
@ -2,6 +2,7 @@ import { inject as service } from '@ember/service';
|
|||
import { default as ApplicationAdapter, namespace } from './application';
|
||||
import OTTExchangeError from '../utils/ott-exchange-error';
|
||||
import classic from 'ember-classic-decorator';
|
||||
import { singularize } from 'ember-inflector';
|
||||
|
||||
@classic
|
||||
export default class TokenAdapter extends ApplicationAdapter {
|
||||
|
@ -9,6 +10,17 @@ export default class TokenAdapter extends ApplicationAdapter {
|
|||
|
||||
namespace = namespace + '/acl';
|
||||
|
||||
createRecord(_store, type, snapshot) {
|
||||
let data = this.serialize(snapshot);
|
||||
data.Policies = data.PolicyIDs;
|
||||
return this.ajax(`${this.buildURL()}/token`, 'POST', { data });
|
||||
}
|
||||
|
||||
// Delete at /token instead of /tokens
|
||||
urlForDeleteRecord(identifier, modelName) {
|
||||
return `${this.buildURL()}/${singularize(modelName)}/${identifier}`;
|
||||
}
|
||||
|
||||
findSelf() {
|
||||
return this.ajax(`${this.buildURL()}/token/self`, 'GET').then((token) => {
|
||||
const store = this.store;
|
||||
|
|
|
@ -2,7 +2,6 @@ import Component from '@glimmer/component';
|
|||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { alias } from '@ember/object/computed';
|
||||
import messageForError from 'nomad-ui/utils/message-from-adapter-error';
|
||||
|
||||
export default class PolicyEditorComponent extends Component {
|
||||
@service flashMessages;
|
||||
|
@ -23,10 +22,12 @@ export default class PolicyEditorComponent extends Component {
|
|||
const nameRegex = '^[a-zA-Z0-9-]{1,128}$';
|
||||
if (!this.policy.name?.match(nameRegex)) {
|
||||
throw new Error(
|
||||
'Policy name must be 1-128 characters long and can only contain letters, numbers, and dashes.'
|
||||
`Policy name must be 1-128 characters long and can only contain letters, numbers, and dashes.`
|
||||
);
|
||||
}
|
||||
|
||||
const shouldRedirectAfterSave = this.policy.isNew;
|
||||
|
||||
if (
|
||||
this.policy.isNew &&
|
||||
this.store.peekRecord('policy', this.policy.name)
|
||||
|
@ -47,12 +48,13 @@ export default class PolicyEditorComponent extends Component {
|
|||
timeout: 5000,
|
||||
});
|
||||
|
||||
this.router.transitionTo('policies');
|
||||
if (shouldRedirectAfterSave) {
|
||||
this.router.transitionTo('policies.policy', this.policy.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('error and its', error);
|
||||
this.flashMessages.add({
|
||||
title: `Error creating Policy ${this.policy.name}`,
|
||||
message: messageForError(error),
|
||||
message: error,
|
||||
type: 'error',
|
||||
destroyOnClick: false,
|
||||
sticky: true,
|
||||
|
|
|
@ -2,7 +2,7 @@ import Component from '@glimmer/component';
|
|||
|
||||
export default class Tooltip extends Component {
|
||||
get text() {
|
||||
const inputText = this.args.text;
|
||||
const inputText = this.args.text?.toString();
|
||||
if (!inputText || inputText.length < 30) {
|
||||
return inputText;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ export default class PoliciesIndexController extends Controller {
|
|||
@service router;
|
||||
get policies() {
|
||||
return this.model.policies.map((policy) => {
|
||||
policy.tokens = this.model.tokens.filter((token) => {
|
||||
policy.tokens = (this.model.tokens || []).filter((token) => {
|
||||
return token.policies.includes(policy);
|
||||
});
|
||||
return policy;
|
||||
|
|
|
@ -3,14 +3,26 @@ import Controller from '@ember/controller';
|
|||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { alias } from '@ember/object/computed';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
export default class PoliciesPolicyController extends Controller {
|
||||
@service flashMessages;
|
||||
@service router;
|
||||
@service store;
|
||||
|
||||
@alias('model.policy') policy;
|
||||
@alias('model.tokens') tokens;
|
||||
|
||||
@tracked
|
||||
error = null;
|
||||
|
||||
@tracked isDeleting = false;
|
||||
|
||||
get newTokenString() {
|
||||
return `nomad acl token create -name="<TOKEN_NAME>" -policy="${this.policy.name}" -type=client -ttl=<8h>`;
|
||||
}
|
||||
|
||||
@action
|
||||
onDeletePrompt() {
|
||||
this.isDeleting = true;
|
||||
|
@ -23,8 +35,8 @@ export default class PoliciesPolicyController extends Controller {
|
|||
|
||||
@task(function* () {
|
||||
try {
|
||||
yield this.model.deleteRecord();
|
||||
yield this.model.save();
|
||||
yield this.policy.deleteRecord();
|
||||
yield this.policy.save();
|
||||
this.flashMessages.add({
|
||||
title: 'Policy Deleted',
|
||||
type: 'success',
|
||||
|
@ -34,7 +46,7 @@ export default class PoliciesPolicyController extends Controller {
|
|||
this.router.transitionTo('policies');
|
||||
} catch (err) {
|
||||
this.flashMessages.add({
|
||||
title: `Error deleting Policy ${this.model.name}`,
|
||||
title: `Error deleting Policy ${this.policy.name}`,
|
||||
message: err,
|
||||
type: 'error',
|
||||
destroyOnClick: false,
|
||||
|
@ -43,4 +55,69 @@ export default class PoliciesPolicyController extends Controller {
|
|||
}
|
||||
})
|
||||
deletePolicy;
|
||||
|
||||
async refreshTokens() {
|
||||
this.tokens = this.store
|
||||
.peekAll('token')
|
||||
.filter((token) =>
|
||||
token.policyNames?.includes(decodeURIComponent(this.policy.name))
|
||||
);
|
||||
}
|
||||
|
||||
@task(function* () {
|
||||
try {
|
||||
const newToken = this.store.createRecord('token', {
|
||||
name: `Example Token for ${this.policy.name}`,
|
||||
policies: [this.policy],
|
||||
// New date 10 minutes into the future
|
||||
expirationTime: new Date(Date.now() + 10 * 60 * 1000),
|
||||
type: 'client',
|
||||
});
|
||||
yield newToken.save();
|
||||
yield this.refreshTokens();
|
||||
this.flashMessages.add({
|
||||
title: 'Example Token Created',
|
||||
message: `${newToken.secret}`,
|
||||
type: 'success',
|
||||
destroyOnClick: false,
|
||||
timeout: 30000,
|
||||
customAction: {
|
||||
label: 'Copy to Clipboard',
|
||||
action: () => {
|
||||
navigator.clipboard.writeText(newToken.secret);
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
this.error = {
|
||||
title: 'Error creating new token',
|
||||
description: err,
|
||||
};
|
||||
|
||||
throw err;
|
||||
}
|
||||
})
|
||||
createTestToken;
|
||||
|
||||
@task(function* (token) {
|
||||
try {
|
||||
yield token.deleteRecord();
|
||||
yield token.save();
|
||||
yield this.refreshTokens();
|
||||
this.flashMessages.add({
|
||||
title: 'Token successfully deleted',
|
||||
type: 'success',
|
||||
destroyOnClick: false,
|
||||
timeout: 5000,
|
||||
});
|
||||
} catch (err) {
|
||||
this.error = {
|
||||
title: 'Error deleting token',
|
||||
description: err,
|
||||
};
|
||||
|
||||
throw err;
|
||||
}
|
||||
})
|
||||
deleteToken;
|
||||
}
|
||||
|
|
|
@ -21,7 +21,9 @@ export default class PoliciesRoute extends Route.extend(
|
|||
async model() {
|
||||
return await hash({
|
||||
policies: this.store.query('policy', { reload: true }),
|
||||
tokens: this.store.query('token', { reload: true }),
|
||||
tokens:
|
||||
this.can.can('list tokens') &&
|
||||
this.store.query('token', { reload: true }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,15 +2,23 @@ import Route from '@ember/routing/route';
|
|||
import withForbiddenState from 'nomad-ui/mixins/with-forbidden-state';
|
||||
import WithModelErrorHandling from 'nomad-ui/mixins/with-model-error-handling';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
|
||||
export default class PoliciesPolicyRoute extends Route.extend(
|
||||
withForbiddenState,
|
||||
WithModelErrorHandling
|
||||
) {
|
||||
@service store;
|
||||
model(params) {
|
||||
return this.store.findRecord('policy', decodeURIComponent(params.name), {
|
||||
reload: true,
|
||||
async model(params) {
|
||||
return hash({
|
||||
policy: this.store.findRecord('policy', decodeURIComponent(params.name), {
|
||||
reload: true,
|
||||
}),
|
||||
tokens: this.store
|
||||
.peekAll('token')
|
||||
.filter((token) =>
|
||||
token.policyNames?.includes(decodeURIComponent(params.name))
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,3 +25,48 @@ table.policies {
|
|||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.token-operations {
|
||||
margin-bottom: 3rem;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-template-columns: repeat(auto-fit, 50%);
|
||||
grid-auto-rows: minmax(100px, auto);
|
||||
gap: 1rem;
|
||||
|
||||
.boxed-section {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
|
||||
.external-link svg {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
button.create-test-token, pre {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-content: flex-start;
|
||||
white-space: normal;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media #{$mq-hidden-gutter} {
|
||||
.token-operations {
|
||||
grid-auto-flow: row;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
table.tokens {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
|
|
@ -32,7 +32,9 @@
|
|||
@class="policies no-mobile-condense" as |t|>
|
||||
<t.head>
|
||||
<th>Policy Name</th>
|
||||
<th>Tokens</th>
|
||||
{{#if (can "list token")}}
|
||||
<th>Tokens</th>
|
||||
{{/if}}
|
||||
</t.head>
|
||||
<t.body as |row|>
|
||||
<tr data-test-policy-row {{on "click" (action "openPolicy" row.model)}}
|
||||
|
@ -43,14 +45,16 @@
|
|||
<td data-test-policy-name>
|
||||
<LinkTo @route="policies.policy" @model={{row.model.name}}>{{row.model.name}}</LinkTo>
|
||||
</td>
|
||||
<td data-test-policy-token-count>
|
||||
<span>
|
||||
{{row.model.tokens.length}}
|
||||
{{#if (filter-by "isExpired" row.model.tokens)}}
|
||||
<span class="number-expired">({{get (filter-by "isExpired" row.model.tokens) "length"}} expired)</span>
|
||||
{{/if}}
|
||||
</span>
|
||||
</td>
|
||||
{{#if (can "list token")}}
|
||||
<td data-test-policy-token-count>
|
||||
<span>
|
||||
<span data-test-policy-total-tokens>{{row.model.tokens.length}}</span>
|
||||
{{#if (filter-by "isExpired" row.model.tokens)}}
|
||||
<span data-test-policy-expired-tokens class="number-expired">({{get (filter-by "isExpired" row.model.tokens) "length"}} expired)</span>
|
||||
{{/if}}
|
||||
</span>
|
||||
</td>
|
||||
{{/if}}
|
||||
</tr>
|
||||
</t.body>
|
||||
</ListTable>
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
<Breadcrumb @crumb={{hash label=this.model.name args=(array "policies.policy" this.model.name)}} />
|
||||
|
||||
<Breadcrumb @crumb={{hash label=this.policy.name args=(array "policies.policy" this.policy.name)}} />
|
||||
{{page-title "Policy"}}
|
||||
<section class="section">
|
||||
<h1 class="title with-flex" data-test-title>
|
||||
<div>
|
||||
{{this.model.name}}
|
||||
{{this.policy.name}}
|
||||
</div>
|
||||
{{#if (can "destroy policy")}}
|
||||
<div>
|
||||
<TwoStepButton
|
||||
data-test-delete-button
|
||||
@idleText="Delete"
|
||||
@idleText="Delete policy"
|
||||
@cancelText="Cancel"
|
||||
@confirmText="Yes, delete"
|
||||
@confirmationMessage="Are you sure?"
|
||||
|
@ -23,7 +22,119 @@
|
|||
{{/if}}
|
||||
</h1>
|
||||
<PolicyEditor
|
||||
@policy={{this.model}}
|
||||
@policy={{this.policy}}
|
||||
/>
|
||||
|
||||
{{#if (can "list token")}}
|
||||
<hr />
|
||||
|
||||
<h2 class="title">
|
||||
Tokens
|
||||
</h2>
|
||||
|
||||
{{#if (can "write token")}}
|
||||
<div class="token-operations">
|
||||
<div class="boxed-section">
|
||||
<div class="boxed-section-head">
|
||||
<h3>Create a Test Token</h3>
|
||||
</div>
|
||||
<div class="boxed-section-body">
|
||||
<p class="is-info">Create a test token that expires in 10 minutes for testing purposes.</p>
|
||||
<label>
|
||||
<button
|
||||
type="button"
|
||||
class="button is-info is-outlined create-test-token"
|
||||
data-test-create-test-token
|
||||
{{on "click" (perform this.createTestToken)}}
|
||||
>Create Test Token</button>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="boxed-section">
|
||||
<div class="boxed-section-head">
|
||||
<h3>Create Tokens from the Nomad CLI</h3>
|
||||
</div>
|
||||
<div class="boxed-section-body">
|
||||
<p>When you're ready to create more tokens, you can do so via the <a class="external-link" href="https://developer.hashicorp.com/nomad/docs/commands" target="_blank" rel="noopener noreferrer">Nomad CLI <FlightIcon @name="external-link" /></a> with the following:
|
||||
<pre>
|
||||
<code>{{this.newTokenString}}</code>
|
||||
<CopyButton
|
||||
data-test-copy-button
|
||||
@clipboardText={{this.newTokenString}}
|
||||
@compact={{true}}
|
||||
>
|
||||
</CopyButton>
|
||||
</pre>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.tokens.length}}
|
||||
<ListTable
|
||||
@source={{this.tokens}}
|
||||
@class="tokens no-mobile-condense" as |t|>
|
||||
<t.head>
|
||||
<th>Name</th>
|
||||
<th>Created</th>
|
||||
<th>Expires</th>
|
||||
{{#if (can "destroy token")}}
|
||||
<th>Delete</th>
|
||||
{{/if}}
|
||||
</t.head>
|
||||
<t.body as |row|>
|
||||
<tr data-test-policy-token-row>
|
||||
<td data-test-token-name>
|
||||
<Tooltip @text={{row.model.id}}>
|
||||
{{row.model.name}}
|
||||
</Tooltip>
|
||||
</td>
|
||||
<td>
|
||||
{{moment-from-now row.model.createTime interval=1000}}
|
||||
</td>
|
||||
<td>
|
||||
{{#if row.model.expirationTime}}
|
||||
<Tooltip @text={{row.model.expirationTime}}>
|
||||
<span data-test-token-expiration-time class="{{if row.model.isExpired "has-text-danger"}}">{{moment-from-now row.model.expirationTime interval=1000}}</span>
|
||||
</Tooltip>
|
||||
{{else}}
|
||||
<span class="has-text-grey">Never</span>
|
||||
{{/if}}
|
||||
</td>
|
||||
{{#if (can "destroy token")}}
|
||||
<td class="is-200px">
|
||||
<TwoStepButton
|
||||
data-test-delete-token-button
|
||||
@idleText="Delete"
|
||||
@cancelText="Cancel"
|
||||
@confirmText="Yes, delete"
|
||||
@confirmationMessage="Are you sure?"
|
||||
@awaitingConfirmation={{row.model.isPendingDeletion}}
|
||||
@onConfirm={{perform this.deleteToken row.model}}
|
||||
@inlineText={{true}}
|
||||
@classes={{hash
|
||||
idleButton="is-danger is-outlined"
|
||||
confirmButton="is-danger"
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
{{/if}}
|
||||
</tr>
|
||||
</t.body>
|
||||
</ListTable>
|
||||
{{else}}
|
||||
<div class="empty-message">
|
||||
<h3 data-test-empty-policies-list-headline class="empty-message-headline">
|
||||
No Tokens
|
||||
</h3>
|
||||
<p class="empty-message-body">
|
||||
No tokens are using this policy.
|
||||
</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
</section>
|
||||
|
||||
{{outlet}}
|
|
@ -447,6 +447,23 @@ export default function () {
|
|||
return this.serialize(tokens.all());
|
||||
});
|
||||
|
||||
this.delete('/acl/token/:id', function (schema, request) {
|
||||
const { id } = request.params;
|
||||
server.db.tokens.remove(id);
|
||||
return '';
|
||||
});
|
||||
|
||||
this.post('/acl/token', function (schema, request) {
|
||||
const { Name, Policies, Type } = JSON.parse(request.requestBody);
|
||||
return server.create('token', {
|
||||
name: Name,
|
||||
policyIds: Policies,
|
||||
type: Type,
|
||||
id: faker.random.uuid(),
|
||||
createTime: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
this.get('/acl/token/self', function ({ tokens }, req) {
|
||||
const secret = req.requestHeaders['X-Nomad-Token'];
|
||||
const tokenForSecret = tokens.findBy({ secretId: secret });
|
||||
|
|
|
@ -14,6 +14,7 @@ export default Factory.extend({
|
|||
oneTimeSecret: () => faker.random.uuid(),
|
||||
|
||||
afterCreate(token, server) {
|
||||
if (token.policyIds && token.policyIds.length) return;
|
||||
const policyIds = Array(faker.random.number({ min: 1, max: 5 }))
|
||||
.fill(0)
|
||||
.map(() => faker.hacker.verb())
|
||||
|
|
|
@ -46,7 +46,11 @@ module('Acceptance | policies', function (hooks) {
|
|||
assert.dom('[data-test-title]').includesText(server.db.policies[0].name);
|
||||
await click('button[type="submit"]');
|
||||
assert.dom('.flash-message.alert-success').exists();
|
||||
assert.equal(currentURL(), '/policies');
|
||||
assert.equal(
|
||||
currentURL(),
|
||||
`/policies/${server.db.policies[0].name}`,
|
||||
'remain on page after save'
|
||||
);
|
||||
// Reset Token
|
||||
window.localStorage.nomadTokenSecret = null;
|
||||
});
|
||||
|
@ -68,7 +72,12 @@ module('Acceptance | policies', function (hooks) {
|
|||
await typeIn('[data-test-policy-name-input]', 'My-Fun-Policy');
|
||||
await click('button[type="submit"]');
|
||||
assert.dom('.flash-message.alert-success').exists();
|
||||
assert.equal(currentURL(), '/policies');
|
||||
assert.equal(
|
||||
currentURL(),
|
||||
'/policies/My-Fun-Policy',
|
||||
'redirected to the now-created policy'
|
||||
);
|
||||
await visit('/policies');
|
||||
const newPolicy = [...findAll('[data-test-policy-name]')].filter((a) =>
|
||||
a.textContent.includes('My-Fun-Policy')
|
||||
)[0];
|
||||
|
|
|
@ -13,6 +13,7 @@ import percySnapshot from '@percy/ember';
|
|||
import faker from 'nomad-ui/mirage/faker';
|
||||
import moment from 'moment';
|
||||
import { run } from '@ember/runloop';
|
||||
import { allScenarios } from '../../mirage/scenarios/default';
|
||||
|
||||
let job;
|
||||
let node;
|
||||
|
@ -424,6 +425,123 @@ module('Acceptance | tokens', function (hooks) {
|
|||
);
|
||||
});
|
||||
|
||||
test('Tokens are shown on the policies index page', async function (assert) {
|
||||
allScenarios.policiesTestCluster(server);
|
||||
// Create an expired token
|
||||
server.create('token', {
|
||||
name: 'Expired Token',
|
||||
id: 'just-expired',
|
||||
policyIds: [server.db.policies[0].name],
|
||||
expirationTime: new Date(new Date().getTime() - 10 * 60 * 1000), // 10 minutes ago
|
||||
});
|
||||
|
||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
||||
await visit('/policies');
|
||||
assert.dom('[data-test-policy-token-count]').exists();
|
||||
const expectedFirstPolicyTokens = server.db.tokens.filter((token) => {
|
||||
return token.policyIds.includes(server.db.policies[0].name);
|
||||
});
|
||||
assert
|
||||
.dom('[data-test-policy-total-tokens]')
|
||||
.hasText(expectedFirstPolicyTokens.length.toString());
|
||||
assert.dom('[data-test-policy-expired-tokens]').hasText('(1 expired)');
|
||||
window.localStorage.nomadTokenSecret = null;
|
||||
});
|
||||
|
||||
test('Tokens are shown on a policy page', async function (assert) {
|
||||
allScenarios.policiesTestCluster(server);
|
||||
// Create an expired token
|
||||
server.create('token', {
|
||||
name: 'Expired Token',
|
||||
id: 'just-expired',
|
||||
policyIds: [server.db.policies[0].name],
|
||||
expirationTime: new Date(new Date().getTime() - 10 * 60 * 1000), // 10 minutes ago
|
||||
});
|
||||
|
||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
||||
await visit('/policies');
|
||||
|
||||
await click('[data-test-policy-row]:first-child');
|
||||
assert.equal(currentURL(), `/policies/${server.db.policies[0].name}`);
|
||||
|
||||
const expectedFirstPolicyTokens = server.db.tokens.filter((token) => {
|
||||
return token.policyIds.includes(server.db.policies[0].name);
|
||||
});
|
||||
|
||||
assert
|
||||
.dom('[data-test-policy-token-row]')
|
||||
.exists(
|
||||
{ count: expectedFirstPolicyTokens.length },
|
||||
'Expected number of tokens are shown'
|
||||
);
|
||||
assert.dom('[data-test-token-expiration-time]').hasText('10 minutes ago');
|
||||
|
||||
window.localStorage.nomadTokenSecret = null;
|
||||
});
|
||||
|
||||
test('Tokens Deletion', async function (assert) {
|
||||
allScenarios.policiesTestCluster(server);
|
||||
// Create an expired token
|
||||
server.create('token', {
|
||||
name: 'Doomed Token',
|
||||
id: 'enjoying-my-day-here',
|
||||
policyIds: [server.db.policies[0].name],
|
||||
});
|
||||
|
||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
||||
await visit('/policies');
|
||||
|
||||
await click('[data-test-policy-row]:first-child');
|
||||
assert.equal(currentURL(), `/policies/${server.db.policies[0].name}`);
|
||||
|
||||
assert
|
||||
.dom('[data-test-policy-token-row]')
|
||||
.exists({ count: 3 }, 'Expected number of tokens are shown');
|
||||
|
||||
const doomedTokenRow = [...findAll('[data-test-policy-token-row]')].find(
|
||||
(a) => a.textContent.includes('Doomed Token')
|
||||
);
|
||||
|
||||
assert.dom(doomedTokenRow).exists();
|
||||
|
||||
await click(doomedTokenRow.querySelector('button'));
|
||||
assert
|
||||
.dom(doomedTokenRow.querySelector('[data-test-confirm-button]'))
|
||||
.exists();
|
||||
await click(doomedTokenRow.querySelector('[data-test-confirm-button]'));
|
||||
assert.dom('.flash-message.alert-success').exists();
|
||||
assert
|
||||
.dom('[data-test-policy-token-row]')
|
||||
.exists({ count: 2 }, 'One fewer token after deletion');
|
||||
await percySnapshot(assert);
|
||||
window.localStorage.nomadTokenSecret = null;
|
||||
});
|
||||
|
||||
test('Test Token Creation', async function (assert) {
|
||||
allScenarios.policiesTestCluster(server);
|
||||
|
||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
||||
await visit('/policies');
|
||||
|
||||
await click('[data-test-policy-row]:first-child');
|
||||
assert.equal(currentURL(), `/policies/${server.db.policies[0].name}`);
|
||||
|
||||
assert
|
||||
.dom('[data-test-policy-token-row]')
|
||||
.exists({ count: 2 }, 'Expected number of tokens are shown');
|
||||
|
||||
await click('[data-test-create-test-token]');
|
||||
assert.dom('.flash-message.alert-success').exists();
|
||||
assert
|
||||
.dom('[data-test-policy-token-row]')
|
||||
.exists({ count: 3 }, 'One more token after test token creation');
|
||||
assert
|
||||
.dom('[data-test-policy-token-row]:last-child [data-test-token-name]')
|
||||
.hasText(`Example Token for ${server.db.policies[0].name}`);
|
||||
await percySnapshot(assert);
|
||||
window.localStorage.nomadTokenSecret = null;
|
||||
});
|
||||
|
||||
function getHeader({ requestHeaders }, name) {
|
||||
// Headers are case-insensitive, but object property look up is not
|
||||
return (
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/* eslint-disable ember/avoid-leaking-state-in-ember-objects */
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import Service from '@ember/service';
|
||||
import setupAbility from 'nomad-ui/tests/helpers/setup-ability';
|
||||
|
||||
module('Unit | Ability | token', function (hooks) {
|
||||
setupTest(hooks);
|
||||
setupAbility('token')(hooks);
|
||||
|
||||
test('A non-management user can do nothing with tokens', function (assert) {
|
||||
const mockToken = Service.extend({
|
||||
aclEnabled: true,
|
||||
selfToken: { type: 'client' },
|
||||
});
|
||||
this.owner.register('service:token', mockToken);
|
||||
assert.notOk(this.ability.canRead);
|
||||
assert.notOk(this.ability.canList);
|
||||
assert.notOk(this.ability.canWrite);
|
||||
assert.notOk(this.ability.canUpdate);
|
||||
assert.notOk(this.ability.canDestroy);
|
||||
});
|
||||
|
||||
test('A management user can do everything with tokens', function (assert) {
|
||||
const mockToken = Service.extend({
|
||||
aclEnabled: true,
|
||||
selfToken: { type: 'management' },
|
||||
});
|
||||
this.owner.register('service:token', mockToken);
|
||||
assert.ok(this.ability.canRead);
|
||||
assert.ok(this.ability.canList);
|
||||
assert.ok(this.ability.canWrite);
|
||||
assert.ok(this.ability.canUpdate);
|
||||
assert.ok(this.ability.canDestroy);
|
||||
});
|
||||
|
||||
test('A non-ACL agent (bypassAuthorization) does not allow anything', function (assert) {
|
||||
const mockToken = Service.extend({
|
||||
aclEnabled: false,
|
||||
});
|
||||
this.owner.register('service:token', mockToken);
|
||||
assert.notOk(this.ability.canRead);
|
||||
assert.notOk(this.ability.canList);
|
||||
assert.notOk(this.ability.canWrite);
|
||||
assert.notOk(this.ability.canUpdate);
|
||||
assert.notOk(this.ability.canDestroy);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue