From 9fa01ac47c36be0ec2c475374af75b3c6fa0929d Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Wed, 30 Nov 2022 12:16:43 -0500 Subject: [PATCH 01/18] basic-functionality demo for token CRUD --- ui/app/adapters/token.js | 8 ++ ui/app/components/tooltip.js | 2 +- ui/app/controllers/policies/policy.js | 37 +++++++++ ui/app/routes/policies/policy.js | 6 +- ui/app/styles/components/policies.scss | 7 ++ ui/app/templates/policies/policy.hbs | 107 ++++++++++++++++++++++++- 6 files changed, 161 insertions(+), 6 deletions(-) diff --git a/ui/app/adapters/token.js b/ui/app/adapters/token.js index 81018aeb972..594b75c7f40 100644 --- a/ui/app/adapters/token.js +++ b/ui/app/adapters/token.js @@ -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,13 @@ export default class TokenAdapter extends ApplicationAdapter { namespace = namespace + '/acl'; + createRecord(_store, type, snapshot) { + let data = this.serialize(snapshot); + console.log('DATA GOING OUT', data); + data.Policies = data.PolicyIDs; // TODO: temp hack + return this.ajax(`${this.buildURL()}/token`, 'POST', { data }); + } + findSelf() { return this.ajax(`${this.buildURL()}/token/self`, 'GET').then((token) => { const store = this.store; diff --git a/ui/app/components/tooltip.js b/ui/app/components/tooltip.js index a9fb3120c9c..feb2ece0229 100644 --- a/ui/app/components/tooltip.js +++ b/ui/app/components/tooltip.js @@ -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; } diff --git a/ui/app/controllers/policies/policy.js b/ui/app/controllers/policies/policy.js index f54663dd482..826c6e24ee4 100644 --- a/ui/app/controllers/policies/policy.js +++ b/ui/app/controllers/policies/policy.js @@ -3,12 +3,16 @@ 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; + @tracked + error = null; + @tracked isDeleting = false; @action @@ -43,4 +47,37 @@ export default class PoliciesPolicyController extends Controller { } }) deletePolicy; + + @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(); + console.table(newToken.toJSON()) + console.log('Accessor:', newToken.accessor) + this.flashMessages.add({ + title: 'Example Token Created', + message: `${newToken.accessor}`, + type: 'success', + destroyOnClick: false, + timeout: 60000, + }); + } catch (err) { + this.error = { + title: 'Error creating new token', + description: err, + }; + + throw err; + } + }) createTestToken; + + } + diff --git a/ui/app/routes/policies/policy.js b/ui/app/routes/policies/policy.js index acd3a2156f6..42a1d468a66 100644 --- a/ui/app/routes/policies/policy.js +++ b/ui/app/routes/policies/policy.js @@ -8,9 +8,11 @@ export default class PoliciesPolicyRoute extends Route.extend( WithModelErrorHandling ) { @service store; - model(params) { - return this.store.findRecord('policy', decodeURIComponent(params.name), { + async model(params) { + const policy = await this.store.findRecord('policy', decodeURIComponent(params.name), { reload: true, }); + const tokens = this.store.peekAll('token').filter(token => token.policyNames?.includes(policy.name)); + return { policy, tokens }; } } diff --git a/ui/app/styles/components/policies.scss b/ui/app/styles/components/policies.scss index 30919b766df..9d86d926733 100644 --- a/ui/app/styles/components/policies.scss +++ b/ui/app/styles/components/policies.scss @@ -25,3 +25,10 @@ table.policies { margin-bottom: 1rem; } } + +.create-test-token { + pre { + background-color: black; + color: white; + } +} diff --git a/ui/app/templates/policies/policy.hbs b/ui/app/templates/policies/policy.hbs index ea81ca92a0d..5ab64b42246 100644 --- a/ui/app/templates/policies/policy.hbs +++ b/ui/app/templates/policies/policy.hbs @@ -1,10 +1,10 @@ - + {{page-title "Policy"}}

- {{this.model.name}} + {{this.policy.name}}
{{#if (can "destroy policy")}}
@@ -23,7 +23,108 @@ {{/if}}

+ +
+
+
+

Create a test token for this policy

+ +
+

Create a test token that expires in 10 minutes for testing purposes.

+

When you're ready to create more tokens, you can do so in your terminal with +

nomad acl token create -name="<TOKEN_NAME>" -policy={{this.policy.name}} -type=client -ttl=<8h>
+

+
+ +
+

+ Tokens for {{this.policy.name}} +

+ {{#if this.tokens.length}} + + + Name + Created + Expires + Extend + Delete + + + + + + {{row.model.name}} + + + + {{moment-from-now row.model.createTime interval=1000}} + + + {{#if row.model.expirationTime}} + + {{moment-from-now row.model.expirationTime interval=1000}} + + {{else}} + Never + {{/if}} + + + {{#if row.model.expirationTime}} + {{#if row.model.isExpired}} + Expired + {{else}} + + {{/if}} + {{else}} + N/A + {{/if}} + + + + + + + + {{else}} +
+

+ No Tokens +

+

+ No tokens are using this policy. +

+
+ {{/if}}
+ {{outlet}} \ No newline at end of file From 9d93d5489e0a8166d8d95f3b19a1e432fac146d8 Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Tue, 6 Dec 2022 15:34:18 -0500 Subject: [PATCH 02/18] Styling for tokens crud --- ui/app/components/policy-editor.js | 11 +-- ui/app/controllers/policies/policy.js | 9 ++- ui/app/routes/policies/policy.js | 10 +-- ui/app/styles/components/policies.scss | 18 +++-- ui/app/templates/policies/policy.hbs | 94 ++++++++++++++------------ 5 files changed, 82 insertions(+), 60 deletions(-) diff --git a/ui/app/components/policy-editor.js b/ui/app/components/policy-editor.js index 0005909f50b..0f78932ff7c 100644 --- a/ui/app/components/policy-editor.js +++ b/ui/app/components/policy-editor.js @@ -22,9 +22,13 @@ export default class PolicyEditorComponent extends Component { try { 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.' - ); + throw { + errors: [ + { + detail: 'Policy name must be 1-128 characters long and can only contain letters, numbers, and dashes.' + } + ] + }; } if ( @@ -49,7 +53,6 @@ export default class PolicyEditorComponent extends Component { this.router.transitionTo('policies'); } catch (error) { - console.log('error and its', error); this.flashMessages.add({ title: `Error creating Policy ${this.policy.name}`, message: messageForError(error), diff --git a/ui/app/controllers/policies/policy.js b/ui/app/controllers/policies/policy.js index 826c6e24ee4..b05c7ad9f1b 100644 --- a/ui/app/controllers/policies/policy.js +++ b/ui/app/controllers/policies/policy.js @@ -10,6 +10,9 @@ export default class PoliciesPolicyController extends Controller { @service flashMessages; @service router; + @alias ('model.policy') policy; + @alias ('model.tokens') tokens; + @tracked error = null; @@ -27,8 +30,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', @@ -38,7 +41,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, diff --git a/ui/app/routes/policies/policy.js b/ui/app/routes/policies/policy.js index 42a1d468a66..d26c3a3b1cf 100644 --- a/ui/app/routes/policies/policy.js +++ b/ui/app/routes/policies/policy.js @@ -2,6 +2,7 @@ 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, @@ -9,10 +10,11 @@ export default class PoliciesPolicyRoute extends Route.extend( ) { @service store; async model(params) { - const policy = await this.store.findRecord('policy', decodeURIComponent(params.name), { - reload: true, + return await hash({ + policy: this.store.findRecord('policy', decodeURIComponent(params.name), { + reload: true, + }), + tokens: this.store.peekAll('token').filter(token => token.policyNames?.includes(decodeURIComponent(params.name))), }); - const tokens = this.store.peekAll('token').filter(token => token.policyNames?.includes(policy.name)); - return { policy, tokens }; } } diff --git a/ui/app/styles/components/policies.scss b/ui/app/styles/components/policies.scss index 9d86d926733..a59b26670cc 100644 --- a/ui/app/styles/components/policies.scss +++ b/ui/app/styles/components/policies.scss @@ -26,9 +26,19 @@ table.policies { } } -.create-test-token { - pre { - background-color: black; - color: white; +.token-operations { + margin-bottom: 3rem; + display: grid; + grid-template-columns: 1fr 1fr; + grid-auto-rows: minmax(100px, auto); + gap: 1rem; + + .notification { + padding: 1rem; + margin: 0; + + button, pre { + margin-top: 1rem; + } } } diff --git a/ui/app/templates/policies/policy.hbs b/ui/app/templates/policies/policy.hbs index 5ab64b42246..9c7d039a6e3 100644 --- a/ui/app/templates/policies/policy.hbs +++ b/ui/app/templates/policies/policy.hbs @@ -1,5 +1,5 @@ - +{{log "uhhh" this.policy}} {{page-title "Policy"}}

@@ -10,7 +10,7 @@

-
-
-

Create a test token for this policy

- -
-

Create a test token that expires in 10 minutes for testing purposes.

-

When you're ready to create more tokens, you can do so in your terminal with -

nomad acl token create -name="<TOKEN_NAME>" -policy={{this.policy.name}} -type=client -ttl=<8h>
-

-
-
-

+

Tokens for {{this.policy.name}} -

+

{{#if this.tokens.length}} Name Created - Expires - Extend + Expiry Delete @@ -76,29 +60,7 @@ Never {{/if}} - - {{#if row.model.expirationTime}} - {{#if row.model.isExpired}} - Expired - {{else}} - - {{/if}} - {{else}} - N/A - {{/if}} - - + @@ -125,6 +88,47 @@

{{/if}} + +
+
+
+

Create a Test Token

+

Create a test token that expires in 10 minutes for testing purposes.

+ +
+
+

Create Tokens from the Nomad CLI

+

When you're ready to create more tokens, you can do so in your terminal with +

nomad acl token create -name="<TOKEN_NAME>" -policy={{this.policy.name}} -type=client -ttl=<8h>
+

+
+
+ + {{!--
+

+
Tokens for {{this.policy.name}}
+ +

+

When you're ready to create more tokens, you can do so in your terminal with +

nomad acl token create -name="<TOKEN_NAME>" -policy={{this.policy.name}} -type=client -ttl=<8h>
+

+
--}} + +
{{outlet}} \ No newline at end of file From 4b6d04f499aa422af41c37d677ca966dc4ef1e73 Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Wed, 7 Dec 2022 11:58:11 -0500 Subject: [PATCH 03/18] Tokens crud styles --- ui/app/adapters/token.js | 1 - ui/app/components/policy-editor.js | 7 ++- ui/app/controllers/policies/policy.js | 17 +++--- ui/app/routes/policies/policy.js | 6 +- ui/app/styles/components/policies.scss | 23 +++++-- ui/app/templates/policies/policy.hbs | 83 +++++++++++++------------- 6 files changed, 76 insertions(+), 61 deletions(-) diff --git a/ui/app/adapters/token.js b/ui/app/adapters/token.js index 594b75c7f40..571c3e8af1a 100644 --- a/ui/app/adapters/token.js +++ b/ui/app/adapters/token.js @@ -2,7 +2,6 @@ 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 { diff --git a/ui/app/components/policy-editor.js b/ui/app/components/policy-editor.js index 0f78932ff7c..24db31b850b 100644 --- a/ui/app/components/policy-editor.js +++ b/ui/app/components/policy-editor.js @@ -25,9 +25,10 @@ export default class PolicyEditorComponent extends Component { throw { errors: [ { - detail: 'Policy name must be 1-128 characters long and can only contain letters, numbers, and dashes.' - } - ] + detail: + 'Policy name must be 1-128 characters long and can only contain letters, numbers, and dashes.', + }, + ], }; } diff --git a/ui/app/controllers/policies/policy.js b/ui/app/controllers/policies/policy.js index b05c7ad9f1b..25cd4adc0a5 100644 --- a/ui/app/controllers/policies/policy.js +++ b/ui/app/controllers/policies/policy.js @@ -10,8 +10,8 @@ export default class PoliciesPolicyController extends Controller { @service flashMessages; @service router; - @alias ('model.policy') policy; - @alias ('model.tokens') tokens; + @alias('model.policy') policy; + @alias('model.tokens') tokens; @tracked error = null; @@ -53,17 +53,16 @@ export default class PoliciesPolicyController extends Controller { @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" + type: 'client', }); yield newToken.save(); - console.table(newToken.toJSON()) - console.log('Accessor:', newToken.accessor) + console.table(newToken.toJSON()); + console.log('Accessor:', newToken.accessor); this.flashMessages.add({ title: 'Example Token Created', message: `${newToken.accessor}`, @@ -79,8 +78,6 @@ export default class PoliciesPolicyController extends Controller { throw err; } - }) createTestToken; - - + }) + createTestToken; } - diff --git a/ui/app/routes/policies/policy.js b/ui/app/routes/policies/policy.js index d26c3a3b1cf..17a0333bd9d 100644 --- a/ui/app/routes/policies/policy.js +++ b/ui/app/routes/policies/policy.js @@ -14,7 +14,11 @@ export default class PoliciesPolicyRoute extends Route.extend( policy: this.store.findRecord('policy', decodeURIComponent(params.name), { reload: true, }), - tokens: this.store.peekAll('token').filter(token => token.policyNames?.includes(decodeURIComponent(params.name))), + tokens: this.store + .peekAll('token') + .filter((token) => + token.policyNames?.includes(decodeURIComponent(params.name)) + ), }); } } diff --git a/ui/app/styles/components/policies.scss b/ui/app/styles/components/policies.scss index a59b26670cc..4ded314301f 100644 --- a/ui/app/styles/components/policies.scss +++ b/ui/app/styles/components/policies.scss @@ -29,16 +29,31 @@ table.policies { .token-operations { margin-bottom: 3rem; display: grid; - grid-template-columns: 1fr 1fr; + grid-auto-flow: column; + grid-template-columns: repeat(auto-fit, 50%); grid-auto-rows: minmax(100px, auto); gap: 1rem; - .notification { - padding: 1rem; + .boxed-section { + padding: 0; margin: 0; + display: grid; + grid-template-rows: auto 1fr; - button, pre { + 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; + } } } + +table.tokens { + margin-bottom: 3rem; +} diff --git a/ui/app/templates/policies/policy.hbs b/ui/app/templates/policies/policy.hbs index 9c7d039a6e3..947a3c6b902 100644 --- a/ui/app/templates/policies/policy.hbs +++ b/ui/app/templates/policies/policy.hbs @@ -10,7 +10,7 @@

- Tokens for {{this.policy.name}} + Tokens

+ + +
+
+
+

Create a Test Token

+
+
+

Create a test token that expires in 10 minutes for testing purposes.

+ +
+
+
+
+

Create Tokens from the Nomad CLI

+
+
+

When you're ready to create more tokens, you can do so in your terminal with +

+            nomad acl token create -name="<TOKEN_NAME>" -policy={{this.policy.name}} -type=client -ttl=<8h>
+            
+            
+          
+

+
+
+
+ {{#if this.tokens.length}} {{/if}} -
-
-
-

Create a Test Token

-

Create a test token that expires in 10 minutes for testing purposes.

- -
-
-

Create Tokens from the Nomad CLI

-

When you're ready to create more tokens, you can do so in your terminal with -

nomad acl token create -name="<TOKEN_NAME>" -policy={{this.policy.name}} -type=client -ttl=<8h>
-

-
-
- - {{!--
-

-
Tokens for {{this.policy.name}}
- -

-

When you're ready to create more tokens, you can do so in your terminal with -

nomad acl token create -name="<TOKEN_NAME>" -policy={{this.policy.name}} -type=client -ttl=<8h>
-

-
--}} - - {{outlet}} \ No newline at end of file From 57ac662b91a917406528ea9c63923893ad53dfd2 Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Wed, 7 Dec 2022 13:53:16 -0500 Subject: [PATCH 04/18] Expires, not expiry --- ui/app/templates/policies/policy.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/templates/policies/policy.hbs b/ui/app/templates/policies/policy.hbs index 947a3c6b902..1c632078560 100644 --- a/ui/app/templates/policies/policy.hbs +++ b/ui/app/templates/policies/policy.hbs @@ -77,7 +77,7 @@ Name Created - Expiry + Expires Delete From 6d4406bae631825c1199a91d353739a894049469 Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Wed, 7 Dec 2022 14:11:46 -0500 Subject: [PATCH 05/18] Mobile styles etc --- ui/app/controllers/policies/policy.js | 4 ++++ ui/app/styles/components/policies.scss | 13 +++++++++++++ ui/app/templates/policies/policy.hbs | 8 ++++---- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/ui/app/controllers/policies/policy.js b/ui/app/controllers/policies/policy.js index 25cd4adc0a5..86c3b4d4e3b 100644 --- a/ui/app/controllers/policies/policy.js +++ b/ui/app/controllers/policies/policy.js @@ -18,6 +18,10 @@ export default class PoliciesPolicyController extends Controller { @tracked isDeleting = false; + get newTokenString() { + return `nomad acl token create -name="" -policy=${this.policy.name} -type=client -ttl=<8h>` + } + @action onDeletePrompt() { this.isDeleting = true; diff --git a/ui/app/styles/components/policies.scss b/ui/app/styles/components/policies.scss index 4ded314301f..83f262eafe9 100644 --- a/ui/app/styles/components/policies.scss +++ b/ui/app/styles/components/policies.scss @@ -40,6 +40,11 @@ table.policies { display: grid; grid-template-rows: auto 1fr; + .external-link svg { + position: relative; + top: 3px; + } + button.create-test-token, pre { margin-top: 1rem; } @@ -54,6 +59,14 @@ table.policies { } } +@media #{$mq-hidden-gutter} { + .token-operations { + grid-auto-flow: row; + grid-template-columns: 1fr; + } +} + + table.tokens { margin-bottom: 3rem; } diff --git a/ui/app/templates/policies/policy.hbs b/ui/app/templates/policies/policy.hbs index 1c632078560..1ece36b9bb4 100644 --- a/ui/app/templates/policies/policy.hbs +++ b/ui/app/templates/policies/policy.hbs @@ -55,12 +55,12 @@

Create Tokens from the Nomad CLI

-

When you're ready to create more tokens, you can do so in your terminal with +

When you're ready to create more tokens, you can do so via the Nomad CLI with the following:

-            nomad acl token create -name="<TOKEN_NAME>" -policy={{this.policy.name}} -type=client -ttl=<8h>
+            {{this.newTokenString}}
             
             
@@ -73,7 +73,7 @@
   {{#if this.tokens.length}}
     
+      @class="tokens no-mobile-condense" as |t|>
       
         Name
         Created

From c8bc0a3da20087616021e93816659b71919363ac Mon Sep 17 00:00:00 2001
From: Phil Renaud 
Date: Wed, 7 Dec 2022 14:32:49 -0500
Subject: [PATCH 06/18] Refresh and redirect rules for policy save and token
 creation

---
 ui/app/components/policy-editor.js    |  6 +++++-
 ui/app/controllers/policies/policy.js | 14 +++++++++++---
 2 files changed, 16 insertions(+), 4 deletions(-)

diff --git a/ui/app/components/policy-editor.js b/ui/app/components/policy-editor.js
index 24db31b850b..d3818b7dc8f 100644
--- a/ui/app/components/policy-editor.js
+++ b/ui/app/components/policy-editor.js
@@ -32,6 +32,8 @@ export default class PolicyEditorComponent extends Component {
         };
       }
 
+      const shouldRedirectAfterSave = this.policy.isNew;
+
       if (
         this.policy.isNew &&
         this.store.peekRecord('policy', this.policy.name)
@@ -52,7 +54,9 @@ export default class PolicyEditorComponent extends Component {
         timeout: 5000,
       });
 
-      this.router.transitionTo('policies');
+      if (shouldRedirectAfterSave) {
+        this.router.transitionTo('policies.policy', this.policy.id);
+      }
     } catch (error) {
       this.flashMessages.add({
         title: `Error creating Policy ${this.policy.name}`,
diff --git a/ui/app/controllers/policies/policy.js b/ui/app/controllers/policies/policy.js
index 86c3b4d4e3b..a25b93fa2ff 100644
--- a/ui/app/controllers/policies/policy.js
+++ b/ui/app/controllers/policies/policy.js
@@ -9,6 +9,7 @@ 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;
@@ -19,7 +20,7 @@ export default class PoliciesPolicyController extends Controller {
   @tracked isDeleting = false;
 
   get newTokenString() {
-    return `nomad acl token create -name="" -policy=${this.policy.name} -type=client -ttl=<8h>`
+    return `nomad acl token create -name="" -policy=${this.policy.name} -type=client -ttl=<8h>`;
   }
 
   @action
@@ -55,6 +56,14 @@ 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', {
@@ -65,8 +74,7 @@ export default class PoliciesPolicyController extends Controller {
         type: 'client',
       });
       yield newToken.save();
-      console.table(newToken.toJSON());
-      console.log('Accessor:', newToken.accessor);
+      this.refreshTokens();
       this.flashMessages.add({
         title: 'Example Token Created',
         message: `${newToken.accessor}`,

From 6f807862b5443766662a8671a8a569ee4328dd0a Mon Sep 17 00:00:00 2001
From: Phil Renaud 
Date: Wed, 7 Dec 2022 16:22:33 -0500
Subject: [PATCH 07/18] Delete method and associated serializer change

---
 ui/app/adapters/token.js              |  9 +++++++--
 ui/app/controllers/policies/policy.js | 22 ++++++++++++++++++++++
 2 files changed, 29 insertions(+), 2 deletions(-)

diff --git a/ui/app/adapters/token.js b/ui/app/adapters/token.js
index 571c3e8af1a..00edc5ca889 100644
--- a/ui/app/adapters/token.js
+++ b/ui/app/adapters/token.js
@@ -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 {
@@ -11,11 +12,15 @@ export default class TokenAdapter extends ApplicationAdapter {
 
   createRecord(_store, type, snapshot) {
     let data = this.serialize(snapshot);
-    console.log('DATA GOING OUT', data);
-    data.Policies = data.PolicyIDs; // TODO: temp hack
+    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;
diff --git a/ui/app/controllers/policies/policy.js b/ui/app/controllers/policies/policy.js
index a25b93fa2ff..798e8892910 100644
--- a/ui/app/controllers/policies/policy.js
+++ b/ui/app/controllers/policies/policy.js
@@ -92,4 +92,26 @@ export default class PoliciesPolicyController extends Controller {
     }
   })
   createTestToken;
+
+  @task(function* (token) {
+    try {
+      yield token.deleteRecord();
+      yield token.save();
+      this.refreshTokens();
+      this.flashMessages.add({
+        title: 'Token successfully deleted',
+        type: 'success',
+        destroyOnClick: false,
+        timeout: 60000,
+      });
+    } catch (err) {
+      this.error = {
+        title: 'Error deleting token',
+        description: err,
+      };
+
+      throw err;
+    }
+  })
+  deleteToken;
 }

From e5ab54463fed98146eb3c693c7b1a7582b565874 Mon Sep 17 00:00:00 2001
From: Phil Renaud 
Date: Wed, 7 Dec 2022 16:55:40 -0500
Subject: [PATCH 08/18] Ability-checking for tokens

---
 ui/app/abilities/token.js             |  10 ++
 ui/app/controllers/policies/index.js  |   2 +-
 ui/app/routes/policies.js             |   4 +-
 ui/app/templates/policies/index.hbs   |  22 +--
 ui/app/templates/policies/policy.hbs  | 192 +++++++++++++-------------
 ui/tests/unit/abilities/token-test.js |  11 ++
 6 files changed, 134 insertions(+), 107 deletions(-)
 create mode 100644 ui/app/abilities/token.js
 create mode 100644 ui/tests/unit/abilities/token-test.js

diff --git a/ui/app/abilities/token.js b/ui/app/abilities/token.js
new file mode 100644
index 00000000000..0302f107c01
--- /dev/null
+++ b/ui/app/abilities/token.js
@@ -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;
+}
diff --git a/ui/app/controllers/policies/index.js b/ui/app/controllers/policies/index.js
index faaa78dd707..c0c2b2755f7 100644
--- a/ui/app/controllers/policies/index.js
+++ b/ui/app/controllers/policies/index.js
@@ -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;
diff --git a/ui/app/routes/policies.js b/ui/app/routes/policies.js
index bcb385e059c..8b0c894aaa5 100644
--- a/ui/app/routes/policies.js
+++ b/ui/app/routes/policies.js
@@ -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 }),
     });
   }
 }
diff --git a/ui/app/templates/policies/index.hbs b/ui/app/templates/policies/index.hbs
index 116dde74f2d..ab02f3b7c6b 100644
--- a/ui/app/templates/policies/index.hbs
+++ b/ui/app/templates/policies/index.hbs
@@ -32,7 +32,9 @@
       @class="policies no-mobile-condense" as |t|>
       
         Policy Name
-        Tokens
+        {{#if (can "list token")}}
+          Tokens
+        {{/if}}
       
       
         
 						{{row.model.name}}
 					
-          
-						
-              {{row.model.tokens.length}}
-              {{#if (filter-by "isExpired" row.model.tokens)}}
-                ({{get (filter-by "isExpired" row.model.tokens) "length"}} expired)
-              {{/if}}
-            
-					
+          {{#if (can "list token")}}
+            
+              
+                {{row.model.tokens.length}}
+                {{#if (filter-by "isExpired" row.model.tokens)}}
+                  ({{get (filter-by "isExpired" row.model.tokens) "length"}} expired)
+                {{/if}}
+              
+            
+          {{/if}}
         
       
     
diff --git a/ui/app/templates/policies/policy.hbs b/ui/app/templates/policies/policy.hbs
index 1ece36b9bb4..3bb2c9e8f72 100644
--- a/ui/app/templates/policies/policy.hbs
+++ b/ui/app/templates/policies/policy.hbs
@@ -1,5 +1,4 @@
 
-{{log "uhhh" this.policy}}
 {{page-title "Policy"}}
 

@@ -26,107 +25,108 @@ @policy={{this.policy}} /> -
+ {{#if (can "list token")}} +
-

- Tokens -

+

+ Tokens +

- -
-
-
-

Create a Test Token

-
-
-

Create a test token that expires in 10 minutes for testing purposes.

- +
+
+
+

Create a Test Token

+
+
+

Create a test token that expires in 10 minutes for testing purposes.

+ +
-
-
-
-

Create Tokens from the Nomad CLI

+
+
+

Create Tokens from the Nomad CLI

+
+
+

When you're ready to create more tokens, you can do so via the Nomad CLI with the following: +

+              {{this.newTokenString}}
+              
+              
+            
+

+
-
-

When you're ready to create more tokens, you can do so via the Nomad CLI with the following: -

-            {{this.newTokenString}}
-            
-            
-          
+
+ + {{#if this.tokens.length}} + + + Name + Created + Expires + Delete + + + + + + {{row.model.name}} + + + + {{moment-from-now row.model.createTime interval=1000}} + + + {{#if row.model.expirationTime}} + + {{moment-from-now row.model.expirationTime interval=1000}} + + {{else}} + Never + {{/if}} + + + + + + + + {{else}} +
+

+ No Tokens +

+

+ No tokens are using this policy.

-
-
- - {{#if this.tokens.length}} - - - Name - Created - Expires - Delete - - - - - - {{row.model.name}} - - - - {{moment-from-now row.model.createTime interval=1000}} - - - {{#if row.model.expirationTime}} - - {{moment-from-now row.model.expirationTime interval=1000}} - - {{else}} - Never - {{/if}} - - - - - - - - {{else}} -
-

- No Tokens -

-

- No tokens are using this policy. -

-
- {{/if}} + {{/if}} + {{/if}}
diff --git a/ui/tests/unit/abilities/token-test.js b/ui/tests/unit/abilities/token-test.js new file mode 100644 index 00000000000..74185ec0f4e --- /dev/null +++ b/ui/tests/unit/abilities/token-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Ability | token', function (hooks) { + setupTest(hooks); + + test('it exists', function (assert) { + const ability = this.owner.lookup('ability:token'); + assert.ok(ability); + }); +}); From e33e258ea41797d7b5e08c57aab489f112b59728 Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Thu, 8 Dec 2022 11:47:34 -0500 Subject: [PATCH 09/18] Update policies acceptance tests to reflect new redirect rules --- ui/tests/acceptance/policies-test.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ui/tests/acceptance/policies-test.js b/ui/tests/acceptance/policies-test.js index ed09d5645fa..f257652a8e5 100644 --- a/ui/tests/acceptance/policies-test.js +++ b/ui/tests/acceptance/policies-test.js @@ -46,7 +46,7 @@ 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 +68,8 @@ 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]; From 084a84d2fddad15ff95fa596d68d4010917845a4 Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Thu, 8 Dec 2022 12:01:39 -0500 Subject: [PATCH 10/18] Token ability unit tests --- ui/tests/unit/abilities/token-test.js | 45 +++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/ui/tests/unit/abilities/token-test.js b/ui/tests/unit/abilities/token-test.js index 74185ec0f4e..c4817dd6db7 100644 --- a/ui/tests/unit/abilities/token-test.js +++ b/ui/tests/unit/abilities/token-test.js @@ -1,11 +1,50 @@ +// @ts-check 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('it exists', function (assert) { - const ability = this.owner.lookup('ability:token'); - assert.ok(ability); + 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); + }); + + }); From e5a1e64e56e6ab2fda672d6afe3a827b887c67ec Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Thu, 8 Dec 2022 13:25:54 -0500 Subject: [PATCH 11/18] Mirage config methods for token crud --- ui/mirage/config.js | 17 +++++++++++++++++ ui/mirage/factories/token.js | 1 + ui/tests/acceptance/policies-test.js | 12 ++++++++++-- ui/tests/unit/abilities/token-test.js | 6 ++---- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/ui/mirage/config.js b/ui/mirage/config.js index a1d3def5888..e8196724913 100644 --- a/ui/mirage/config.js +++ b/ui/mirage/config.js @@ -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 }); diff --git a/ui/mirage/factories/token.js b/ui/mirage/factories/token.js index 12961a3bb62..1f3a8cb1431 100644 --- a/ui/mirage/factories/token.js +++ b/ui/mirage/factories/token.js @@ -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()) diff --git a/ui/tests/acceptance/policies-test.js b/ui/tests/acceptance/policies-test.js index f257652a8e5..db03c33e1bc 100644 --- a/ui/tests/acceptance/policies-test.js +++ b/ui/tests/acceptance/policies-test.js @@ -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/${server.db.policies[0].name}`, 'remain on page after save'); + assert.equal( + currentURL(), + `/policies/${server.db.policies[0].name}`, + 'remain on page after save' + ); // Reset Token window.localStorage.nomadTokenSecret = null; }); @@ -68,7 +72,11 @@ 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/My-Fun-Policy', 'redirected to the now-created policy'); + 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') diff --git a/ui/tests/unit/abilities/token-test.js b/ui/tests/unit/abilities/token-test.js index c4817dd6db7..a6ae191b5c8 100644 --- a/ui/tests/unit/abilities/token-test.js +++ b/ui/tests/unit/abilities/token-test.js @@ -1,4 +1,4 @@ -// @ts-check +/* eslint-disable ember/avoid-leaking-state-in-ember-objects */ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import Service from '@ember/service'; @@ -36,7 +36,7 @@ module('Unit | Ability | token', function (hooks) { test('A non-ACL agent (bypassAuthorization) does not allow anything', function (assert) { const mockToken = Service.extend({ - aclEnabled: false + aclEnabled: false, }); this.owner.register('service:token', mockToken); assert.notOk(this.ability.canRead); @@ -45,6 +45,4 @@ module('Unit | Ability | token', function (hooks) { assert.notOk(this.ability.canUpdate); assert.notOk(this.ability.canDestroy); }); - - }); From 813305f3100e528aa50d5dae53eff91ba1b68fbe Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Thu, 8 Dec 2022 16:48:36 -0500 Subject: [PATCH 12/18] Token CRUD acceptance tests --- ui/app/templates/policies/index.hbs | 4 +- ui/app/templates/policies/policy.hbs | 7 +- ui/tests/acceptance/token-test.js | 99 ++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 5 deletions(-) diff --git a/ui/app/templates/policies/index.hbs b/ui/app/templates/policies/index.hbs index ab02f3b7c6b..8ad5903df15 100644 --- a/ui/app/templates/policies/index.hbs +++ b/ui/app/templates/policies/index.hbs @@ -48,9 +48,9 @@ {{#if (can "list token")}} - {{row.model.tokens.length}} + {{row.model.tokens.length}} {{#if (filter-by "isExpired" row.model.tokens)}} - ({{get (filter-by "isExpired" row.model.tokens) "length"}} expired) + ({{get (filter-by "isExpired" row.model.tokens) "length"}} expired) {{/if}} diff --git a/ui/app/templates/policies/policy.hbs b/ui/app/templates/policies/policy.hbs index 3bb2c9e8f72..0d37b806229 100644 --- a/ui/app/templates/policies/policy.hbs +++ b/ui/app/templates/policies/policy.hbs @@ -80,8 +80,8 @@ Delete - - + + {{row.model.name}} @@ -92,7 +92,7 @@ {{#if row.model.expirationTime}} - {{moment-from-now row.model.expirationTime interval=1000}} + {{moment-from-now row.model.expirationTime interval=1000}} {{else}} Never @@ -100,6 +100,7 @@ { + 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'); + 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}`); + + window.localStorage.nomadTokenSecret = null; + }); + function getHeader({ requestHeaders }, name) { // Headers are case-insensitive, but object property look up is not return ( From b72702b872359ed7923839acdc34f49811d35a69 Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Thu, 8 Dec 2022 16:54:51 -0500 Subject: [PATCH 13/18] A couple visual diff snapshots --- ui/tests/acceptance/token-test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/tests/acceptance/token-test.js b/ui/tests/acceptance/token-test.js index 20c511d73ae..02a6939734b 100644 --- a/ui/tests/acceptance/token-test.js +++ b/ui/tests/acceptance/token-test.js @@ -501,6 +501,7 @@ module('Acceptance | tokens', function (hooks) { 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; }); @@ -519,7 +520,7 @@ module('Acceptance | tokens', function (hooks) { 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; }); From 396de14e89e188ff3fbae565ff30c961b6586d4d Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Thu, 8 Dec 2022 16:57:18 -0500 Subject: [PATCH 14/18] Add and Delete abilities referenced for token operations --- ui/app/templates/policies/policy.hbs | 106 ++++++++++++++------------- ui/tests/acceptance/token-test.js | 50 +++++++++---- 2 files changed, 90 insertions(+), 66 deletions(-) diff --git a/ui/app/templates/policies/policy.hbs b/ui/app/templates/policies/policy.hbs index 0d37b806229..a7da7964678 100644 --- a/ui/app/templates/policies/policy.hbs +++ b/ui/app/templates/policies/policy.hbs @@ -32,42 +32,44 @@ Tokens -
-
-
-

Create a Test Token

+ {{#if (can "write token")}} +
+
+
+

Create a Test Token

+
+
+

Create a test token that expires in 10 minutes for testing purposes.

+ +
-
-

Create a test token that expires in 10 minutes for testing purposes.

- +
+
+

Create Tokens from the Nomad CLI

+
+
+

When you're ready to create more tokens, you can do so via the Nomad CLI with the following: +

+                {{this.newTokenString}}
+                
+                
+              
+

+
-
-
-

Create Tokens from the Nomad CLI

-
-
-

When you're ready to create more tokens, you can do so via the Nomad CLI with the following: -

-              {{this.newTokenString}}
-              
-              
-            
-

-
-
-
+ {{/if}} {{#if this.tokens.length}} Name Created Expires - Delete + {{#if (can "destroy token")}} + Delete + {{/if}} @@ -98,22 +102,24 @@ Never {{/if}} - - - + {{#if (can "destroy token")}} + + + + {{/if}} diff --git a/ui/tests/acceptance/token-test.js b/ui/tests/acceptance/token-test.js index 02a6939734b..c7c0cfbc2fe 100644 --- a/ui/tests/acceptance/token-test.js +++ b/ui/tests/acceptance/token-test.js @@ -432,17 +432,19 @@ module('Acceptance | tokens', function (hooks) { name: 'Expired Token', id: 'just-expired', policyIds: [server.db.policies[0].name], - expirationTime: new Date(new Date().getTime() -10 * 60 * 1000), // 10 minutes ago + 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(); + 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)"); + 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; }); @@ -453,7 +455,7 @@ module('Acceptance | tokens', function (hooks) { name: 'Expired Token', id: 'just-expired', policyIds: [server.db.policies[0].name], - expirationTime: new Date(new Date().getTime() -10 * 60 * 1000), // 10 minutes ago + expirationTime: new Date(new Date().getTime() - 10 * 60 * 1000), // 10 minutes ago }); window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId; @@ -466,13 +468,17 @@ module('Acceptance | tokens', function (hooks) { 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-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 @@ -488,19 +494,25 @@ module('Acceptance | tokens', function (hooks) { 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'); + 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') + 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(); + 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'); + assert + .dom('[data-test-policy-token-row]') + .exists({ count: 2 }, 'One fewer token after deletion'); await percySnapshot(assert); window.localStorage.nomadTokenSecret = null; }); @@ -514,12 +526,18 @@ module('Acceptance | tokens', function (hooks) { 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'); + 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}`); + 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; }); From e4fcb4a6b2ce251f67618abaa31bdf9a601ef42a Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Fri, 9 Dec 2022 10:58:25 -0500 Subject: [PATCH 15/18] Changing timeouts and adding a copy to clipboard action --- ui/app/controllers/policies/policy.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/ui/app/controllers/policies/policy.js b/ui/app/controllers/policies/policy.js index 798e8892910..f8dcbb78b07 100644 --- a/ui/app/controllers/policies/policy.js +++ b/ui/app/controllers/policies/policy.js @@ -20,7 +20,7 @@ export default class PoliciesPolicyController extends Controller { @tracked isDeleting = false; get newTokenString() { - return `nomad acl token create -name="" -policy=${this.policy.name} -type=client -ttl=<8h>`; + return `nomad acl token create -name="" -policy="${this.policy.name}" -type=client -ttl=<8h>`; } @action @@ -80,7 +80,13 @@ export default class PoliciesPolicyController extends Controller { message: `${newToken.accessor}`, type: 'success', destroyOnClick: false, - timeout: 60000, + timeout: 30000, + customAction: { + label: 'Copy to Clipboard', + action: () => { + navigator.clipboard.writeText(newToken.accessor); + }, + }, }); } catch (err) { this.error = { @@ -102,7 +108,7 @@ export default class PoliciesPolicyController extends Controller { title: 'Token successfully deleted', type: 'success', destroyOnClick: false, - timeout: 60000, + timeout: 5000, }); } catch (err) { this.error = { From 60cfa38d9f1a301ef073e32182a8aa59add4ac90 Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Wed, 14 Dec 2022 14:52:39 -0500 Subject: [PATCH 16/18] replaced accessor with secret when copying to clipboard --- ui/app/controllers/policies/policy.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/app/controllers/policies/policy.js b/ui/app/controllers/policies/policy.js index f8dcbb78b07..e347c42057b 100644 --- a/ui/app/controllers/policies/policy.js +++ b/ui/app/controllers/policies/policy.js @@ -77,14 +77,14 @@ export default class PoliciesPolicyController extends Controller { this.refreshTokens(); this.flashMessages.add({ title: 'Example Token Created', - message: `${newToken.accessor}`, + message: `${newToken.secret}`, type: 'success', destroyOnClick: false, timeout: 30000, customAction: { label: 'Copy to Clipboard', action: () => { - navigator.clipboard.writeText(newToken.accessor); + navigator.clipboard.writeText(newToken.secret); }, }, }); From 926e9a289e9e1faa060336837160a2ceedb0d60f Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Wed, 14 Dec 2022 17:00:49 -0500 Subject: [PATCH 17/18] PR comments addressed --- ui/app/controllers/policies/policy.js | 4 ++-- ui/app/routes/policies/policy.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/app/controllers/policies/policy.js b/ui/app/controllers/policies/policy.js index e347c42057b..b504e5c1ecf 100644 --- a/ui/app/controllers/policies/policy.js +++ b/ui/app/controllers/policies/policy.js @@ -74,7 +74,7 @@ export default class PoliciesPolicyController extends Controller { type: 'client', }); yield newToken.save(); - this.refreshTokens(); + yield this.refreshTokens(); this.flashMessages.add({ title: 'Example Token Created', message: `${newToken.secret}`, @@ -103,7 +103,7 @@ export default class PoliciesPolicyController extends Controller { try { yield token.deleteRecord(); yield token.save(); - this.refreshTokens(); + yield this.refreshTokens(); this.flashMessages.add({ title: 'Token successfully deleted', type: 'success', diff --git a/ui/app/routes/policies/policy.js b/ui/app/routes/policies/policy.js index 17a0333bd9d..215c8386def 100644 --- a/ui/app/routes/policies/policy.js +++ b/ui/app/routes/policies/policy.js @@ -10,7 +10,7 @@ export default class PoliciesPolicyRoute extends Route.extend( ) { @service store; async model(params) { - return await hash({ + return hash({ policy: this.store.findRecord('policy', decodeURIComponent(params.name), { reload: true, }), From 0200e2d0385259b4112cf8b445208f455b926e00 Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Thu, 15 Dec 2022 11:29:03 -0500 Subject: [PATCH 18/18] Simplified error passing for policy editor --- .changelog/15435.txt | 3 +++ ui/app/components/policy-editor.js | 14 ++++---------- 2 files changed, 7 insertions(+), 10 deletions(-) create mode 100644 .changelog/15435.txt diff --git a/.changelog/15435.txt b/.changelog/15435.txt new file mode 100644 index 00000000000..383647319ed --- /dev/null +++ b/.changelog/15435.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: The web UI now provides a Token Management interface for management users on policy pages +``` diff --git a/ui/app/components/policy-editor.js b/ui/app/components/policy-editor.js index d3818b7dc8f..d37ef128743 100644 --- a/ui/app/components/policy-editor.js +++ b/ui/app/components/policy-editor.js @@ -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; @@ -22,14 +21,9 @@ export default class PolicyEditorComponent extends Component { try { const nameRegex = '^[a-zA-Z0-9-]{1,128}$'; if (!this.policy.name?.match(nameRegex)) { - throw { - errors: [ - { - detail: - 'Policy name must be 1-128 characters long and can only contain letters, numbers, and dashes.', - }, - ], - }; + throw new Error( + `Policy name must be 1-128 characters long and can only contain letters, numbers, and dashes.` + ); } const shouldRedirectAfterSave = this.policy.isNew; @@ -60,7 +54,7 @@ export default class PolicyEditorComponent extends Component { } catch (error) { this.flashMessages.add({ title: `Error creating Policy ${this.policy.name}`, - message: messageForError(error), + message: error, type: 'error', destroyOnClick: false, sticky: true,