diff --git a/.changelog/20483.txt b/.changelog/20483.txt new file mode 100644 index 00000000000..ebe6ee298dc --- /dev/null +++ b/.changelog/20483.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: Added a UI for creating, editing and deleting Sentinel Policies +``` diff --git a/ui/app/abilities/sentinel-policy.js b/ui/app/abilities/sentinel-policy.js new file mode 100644 index 00000000000..1da17deb975 --- /dev/null +++ b/ui/app/abilities/sentinel-policy.js @@ -0,0 +1,27 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import AbstractAbility from './abstract'; +import { alias, and, or } from '@ember/object/computed'; +import { computed } from '@ember/object'; + +export default class SentinelPolicy extends AbstractAbility { + @alias('hasFeatureAndManagement') canRead; + @alias('hasFeatureAndManagement') canList; + @alias('hasFeatureAndManagement') canWrite; + @alias('hasFeatureAndManagement') canUpdate; + @alias('hasFeatureAndManagement') canDestroy; + + @or('bypassAuthorization', 'selfTokenIsManagement') + hasAuthority; + + @and('sentinelIsPresent', 'hasAuthority') + hasFeatureAndManagement; + + @computed('features.[]') + get sentinelIsPresent() { + return this.featureIsPresent('Sentinel Policies'); + } +} diff --git a/ui/app/adapters/sentinel-policy.js b/ui/app/adapters/sentinel-policy.js new file mode 100644 index 00000000000..72bab4b5a9e --- /dev/null +++ b/ui/app/adapters/sentinel-policy.js @@ -0,0 +1,25 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { default as ApplicationAdapter } from './application'; +import classic from 'ember-classic-decorator'; + +@classic +export default class SentinelPolicyAdapter extends ApplicationAdapter { + pathForType = () => 'sentinel/policies'; + + // namespace = namespace + '/acl'; + urlForCreateRecord(_modelName, model) { + return this.urlForUpdateRecord(model.attr('name'), 'sentinel/policy'); + } + + urlForFindRecord(id) { + return '/v1/sentinel/policy/' + id; + } + + urlForDeleteRecord(id) { + return '/v1/sentinel/policy/' + id; + } +} diff --git a/ui/app/components/access-control-subnav.js b/ui/app/components/administration-subnav.js similarity index 81% rename from ui/app/components/access-control-subnav.js rename to ui/app/components/administration-subnav.js index 9f15fc113a6..0e1e6ca1597 100644 --- a/ui/app/components/access-control-subnav.js +++ b/ui/app/components/administration-subnav.js @@ -8,6 +8,6 @@ import { tagName } from '@ember-decorators/component'; import { inject as service } from '@ember/service'; @tagName('') -export default class AccessControlSubnav extends Component { +export default class AdministrationSubnav extends Component { @service keyboard; } diff --git a/ui/app/components/namespace-editor.js b/ui/app/components/namespace-editor.js index fd2e84cc045..a67716556e0 100644 --- a/ui/app/components/namespace-editor.js +++ b/ui/app/components/namespace-editor.js @@ -78,7 +78,7 @@ export default class NamespaceEditorComponent extends Component { if (shouldRedirectAfterSave) { this.router.transitionTo( - 'access-control.namespaces.acl-namespace', + 'administration.namespaces.acl-namespace', this.namespace.name ); } diff --git a/ui/app/components/policy-editor.hbs b/ui/app/components/policy-editor.hbs index 7d3b495680f..af2713152a3 100644 --- a/ui/app/components/policy-editor.hbs +++ b/ui/app/components/policy-editor.hbs @@ -26,12 +26,12 @@ class="policy-editor" data-test-policy-editor {{code-mirror - screenReaderLabel="Policy definition" - theme="hashi" - mode="ruby" - content=@policy.rules - onUpdate=this.updatePolicyRules - autofocus=false + screenReaderLabel="Policy definition" + theme="hashi" + mode="ruby" + content=@policy.rules + onUpdate=this.updatePolicyRules + autofocus=false extraKeys=(hash Cmd-Enter=this.save) }} /> diff --git a/ui/app/components/policy-editor.js b/ui/app/components/policy-editor.js index 2f6c5ac90ac..040276485f5 100644 --- a/ui/app/components/policy-editor.js +++ b/ui/app/components/policy-editor.js @@ -60,7 +60,7 @@ export default class PolicyEditorComponent extends Component { if (shouldRedirectAfterSave) { this.router.transitionTo( - 'access-control.policies.policy', + 'administration.policies.policy', this.policy.id ); } diff --git a/ui/app/components/role-editor.hbs b/ui/app/components/role-editor.hbs index 4eb18c126b3..a2cadb76816 100644 --- a/ui/app/components/role-editor.hbs +++ b/ui/app/components/role-editor.hbs @@ -55,7 +55,7 @@ {{B.data.name}} {{B.data.description}} - + View Policy Definition diff --git a/ui/app/components/role-editor.js b/ui/app/components/role-editor.js index 9094700cbc9..ce8b972276a 100644 --- a/ui/app/components/role-editor.js +++ b/ui/app/components/role-editor.js @@ -73,7 +73,7 @@ export default class RoleEditorComponent extends Component { }); if (shouldRedirectAfterSave) { - this.router.transitionTo('access-control.roles.role', this.role.id); + this.router.transitionTo('administration.roles.role', this.role.id); } } catch (err) { let message = err.errors?.length diff --git a/ui/app/components/sentinel-policy-editor.hbs b/ui/app/components/sentinel-policy-editor.hbs new file mode 100644 index 00000000000..ebe872815af --- /dev/null +++ b/ui/app/components/sentinel-policy-editor.hbs @@ -0,0 +1,90 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 +~}} + +
+ {{#if @policy.isNew }} + + Policy Name + + {{/if}} + +
+
+ Policy Definition +
+
+
+
+
+ +
+ +
+ +
+ + Enforcement Level + See Sentinel Policy documentation for more information. + + Advisory + + + Soft Mandatory + + + Hard Mandatory + + +
+ +
+ {{#if (can "update sentinel-policy")}} + + {{/if}} +
+ diff --git a/ui/app/components/sentinel-policy-editor.js b/ui/app/components/sentinel-policy-editor.js new file mode 100644 index 00000000000..379b8014527 --- /dev/null +++ b/ui/app/components/sentinel-policy-editor.js @@ -0,0 +1,92 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +// @ts-check + +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import { alias } from '@ember/object/computed'; +import messageFromAdapterError from 'nomad-ui/utils/message-from-adapter-error'; + +export default class SentinelPolicyEditorComponent extends Component { + @service notifications; + @service router; + @service store; + + @alias('args.policy') policy; + + @action updatePolicy(value) { + this.policy.set('policy', value); + } + + @action updatePolicyName({ target: { value } }) { + this.policy.set('name', value); + } + + @action updatePolicyEnforcementLevel({ target: { id } }) { + this.policy.set('enforcementLevel', id); + } + + @action async save(e) { + if (e instanceof Event) { + e.preventDefault(); // code-mirror "command+enter" submits the form, but doesnt have a preventDefault() + } + 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.` + ); + } + if (this.policy.description?.length > 256) { + throw new Error( + `Policy description must be under 256 characters long.` + ); + } + + const shouldRedirectAfterSave = this.policy.isNew; + // Because we set the ID for adapter/serialization reasons just before save here, + // that becomes a barrier to our Unique Name validation. So we explicltly exclude + // the current policy when checking for uniqueness. + if ( + this.policy.isNew && + this.store + .peekAll('sentinel-policy') + .filter((policy) => policy !== this.policy) + .findBy('name', this.policy.name) + ) { + throw new Error( + `A sentinel policy with name ${this.policy.name} already exists.` + ); + } + this.policy.set('id', this.policy.name); + await this.policy.save(); + + this.notifications.add({ + title: 'Sentinel Policy Saved', + color: 'success', + }); + + if (shouldRedirectAfterSave) { + this.router.transitionTo( + 'administration.sentinel-policies.policy', + this.policy.name + ); + } + } catch (err) { + let message = err.errors?.length + ? messageFromAdapterError(err) + : err.message || 'Unknown Error'; + + this.notifications.add({ + title: `Error creating Sentinel Policy ${this.policy.name}`, + message, + color: 'critical', + sticky: true, + }); + } + } +} diff --git a/ui/app/components/token-editor.hbs b/ui/app/components/token-editor.hbs index 8450b048e64..6b3a822258a 100644 --- a/ui/app/components/token-editor.hbs +++ b/ui/app/components/token-editor.hbs @@ -145,7 +145,7 @@ {{B.data.name}} {{B.data.description}} - + View Policy Definition @@ -158,7 +158,7 @@ No Policies

- Get started by creating a new policy + Get started by creating a new policy

{{/if}} @@ -196,7 +196,7 @@
{{#each B.data.policies as |policy|}} {{#if policy.name}} - + {{/if}} {{else}} Role contains no policies @@ -204,7 +204,7 @@
- + View Role Info @@ -217,7 +217,7 @@ No Roles

- Get started by creating a new role + Get started by creating a new role

{{/if}} diff --git a/ui/app/components/token-editor.js b/ui/app/components/token-editor.js index 7856fbfe13f..3cd8eb16183 100644 --- a/ui/app/components/token-editor.js +++ b/ui/app/components/token-editor.js @@ -105,7 +105,7 @@ export default class TokenEditorComponent extends Component { if (shouldRedirectAfterSave) { this.router.transitionTo( - 'access-control.tokens.token', + 'administration.tokens.token', this.activeToken.id ); } diff --git a/ui/app/components/variable-form/related-entities.hbs b/ui/app/components/variable-form/related-entities.hbs index 9c33a18d349..f4f1bd11ec3 100644 --- a/ui/app/components/variable-form/related-entities.hbs +++ b/ui/app/components/variable-form/related-entities.hbs @@ -15,6 +15,6 @@ job {{@job}} {{else}} all nomad jobs in this namespace - {{/if}} + {{/if}} diff --git a/ui/app/controllers/access-control/namespaces/acl-namespace.js b/ui/app/controllers/administration/namespaces/acl-namespace.js similarity index 95% rename from ui/app/controllers/access-control/namespaces/acl-namespace.js rename to ui/app/controllers/administration/namespaces/acl-namespace.js index b452b4d358b..63c3fe29665 100644 --- a/ui/app/controllers/access-control/namespaces/acl-namespace.js +++ b/ui/app/controllers/administration/namespaces/acl-namespace.js @@ -24,7 +24,7 @@ export default class AccessControlNamespacesAclNamespaceController extends Contr type: `success`, destroyOnClick: false, }); - this.router.transitionTo('access-control.namespaces'); + this.router.transitionTo('administration.namespaces'); } catch (err) { // A failed delete resulted in errors when you then navigated away and back // to the show page rollbackWithoutChangedAttrs fixes it, but there might diff --git a/ui/app/controllers/access-control/namespaces/index.js b/ui/app/controllers/administration/namespaces/index.js similarity index 86% rename from ui/app/controllers/access-control/namespaces/index.js rename to ui/app/controllers/administration/namespaces/index.js index 3a78ae2973a..f42dd83165b 100644 --- a/ui/app/controllers/access-control/namespaces/index.js +++ b/ui/app/controllers/administration/namespaces/index.js @@ -14,13 +14,13 @@ export default class AccessControlNamespacesIndexController extends Controller { @action openNamespace(namespace) { this.router.transitionTo( - 'access-control.namespaces.acl-namespace', + 'administration.namespaces.acl-namespace', namespace.name ); } @action goToNewNamespace() { - this.router.transitionTo('access-control.namespaces.new'); + this.router.transitionTo('administration.namespaces.new'); } get columns() { diff --git a/ui/app/controllers/access-control/policies/index.js b/ui/app/controllers/administration/policies/index.js similarity index 94% rename from ui/app/controllers/access-control/policies/index.js rename to ui/app/controllers/administration/policies/index.js index 322dcb6bec9..3a50b27a6c1 100644 --- a/ui/app/controllers/access-control/policies/index.js +++ b/ui/app/controllers/administration/policies/index.js @@ -54,11 +54,11 @@ export default class AccessControlPoliciesIndexController extends Controller { } @action openPolicy(policy) { - this.router.transitionTo('access-control.policies.policy', policy.name); + this.router.transitionTo('administration.policies.policy', policy.name); } @action goToNewPolicy() { - this.router.transitionTo('access-control.policies.new'); + this.router.transitionTo('administration.policies.new'); } @task(function* (policy) { diff --git a/ui/app/controllers/access-control/policies/policy.js b/ui/app/controllers/administration/policies/policy.js similarity index 98% rename from ui/app/controllers/access-control/policies/policy.js rename to ui/app/controllers/administration/policies/policy.js index 523d6485104..dc0366aae94 100644 --- a/ui/app/controllers/access-control/policies/policy.js +++ b/ui/app/controllers/administration/policies/policy.js @@ -42,7 +42,7 @@ export default class AccessControlPoliciesPolicyController extends Controller { type: `success`, destroyOnClick: false, }); - this.router.transitionTo('access-control.policies'); + this.router.transitionTo('administration.policies'); } catch (err) { this.notifications.add({ title: `Error deleting Policy ${this.policy.name}`, diff --git a/ui/app/controllers/access-control/roles/index.js b/ui/app/controllers/administration/roles/index.js similarity index 93% rename from ui/app/controllers/access-control/roles/index.js rename to ui/app/controllers/administration/roles/index.js index 4807b149096..cdbbc0aaada 100644 --- a/ui/app/controllers/access-control/roles/index.js +++ b/ui/app/controllers/administration/roles/index.js @@ -60,11 +60,11 @@ export default class AccessControlRolesIndexController extends Controller { } @action openRole(role) { - this.router.transitionTo('access-control.roles.role', role.id); + this.router.transitionTo('administration.roles.role', role.id); } @action goToNewRole() { - this.router.transitionTo('access-control.roles.new'); + this.router.transitionTo('administration.roles.new'); } @task(function* (role) { diff --git a/ui/app/controllers/access-control/roles/role.js b/ui/app/controllers/administration/roles/role.js similarity index 97% rename from ui/app/controllers/access-control/roles/role.js rename to ui/app/controllers/administration/roles/role.js index a2c2189faa5..0c6ac410a0c 100644 --- a/ui/app/controllers/access-control/roles/role.js +++ b/ui/app/controllers/administration/roles/role.js @@ -32,7 +32,7 @@ export default class AccessControlRolesRoleController extends Controller { type: `success`, destroyOnClick: false, }); - this.router.transitionTo('access-control.roles'); + this.router.transitionTo('administration.roles'); } catch (err) { this.notifications.add({ title: `Error deleting Role ${this.role.name}`, diff --git a/ui/app/controllers/administration/sentinel-policies.js b/ui/app/controllers/administration/sentinel-policies.js new file mode 100644 index 00000000000..5a020ae857a --- /dev/null +++ b/ui/app/controllers/administration/sentinel-policies.js @@ -0,0 +1,8 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Controller from '@ember/controller'; + +export default class AdministrationSentinelPoliciesController extends Controller {} diff --git a/ui/app/controllers/administration/sentinel-policies/gallery.js b/ui/app/controllers/administration/sentinel-policies/gallery.js new file mode 100644 index 00000000000..69b66cb26c9 --- /dev/null +++ b/ui/app/controllers/administration/sentinel-policies/gallery.js @@ -0,0 +1,27 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +// @ts-check +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; +import TEMPLATES from 'nomad-ui/utils/default-sentinel-policy-templates'; + +export default class SentinelPoliciesNewGalleryController extends Controller { + @service notifications; + @service router; + @service store; + @tracked selectedTemplate = null; + + get templates() { + return TEMPLATES; + } + + @action + onChange(e) { + this.selectedTemplate = e.target.id; + } +} diff --git a/ui/app/controllers/administration/sentinel-policies/index.js b/ui/app/controllers/administration/sentinel-policies/index.js new file mode 100644 index 00000000000..d418eae881e --- /dev/null +++ b/ui/app/controllers/administration/sentinel-policies/index.js @@ -0,0 +1,78 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Controller from '@ember/controller'; +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import { task } from 'ember-concurrency'; + +export default class SentinelPoliciesIndexController extends Controller { + @service router; + @service notifications; + + @action openPolicy(policy) { + this.router.transitionTo( + 'administration.sentinel-policies.policy', + policy.name + ); + } + + @action goToNewPolicy() { + this.router.transitionTo('administration.sentinel-policies.new'); + } + + @action goToTemplateGallery() { + this.router.transitionTo('administration.sentinel-policies.gallery'); + } + + get columns() { + return [ + { + key: 'name', + label: 'Name', + isSortable: true, + }, + { + key: 'description', + label: 'Description', + }, + { + key: 'enforcementLevel', + label: 'Enforcement Level', + isSortable: true, + }, + { + key: 'delete', + label: 'Delete', + }, + ]; + } + + @task(function* (policy) { + try { + yield policy.deleteRecord(); + yield policy.save(); + + if (this.store.peekRecord('policy', policy.id)) { + this.store.unloadRecord(policy); + } + + this.notifications.add({ + title: `Sentinel policy ${policy.name} successfully deleted`, + color: 'success', + }); + } catch (err) { + this.notifications.add({ + title: 'Error deleting policy', + color: 'critical', + sticky: true, + message: err, + }); + + throw err; + } + }) + deletePolicy; +} diff --git a/ui/app/controllers/administration/sentinel-policies/new.js b/ui/app/controllers/administration/sentinel-policies/new.js new file mode 100644 index 00000000000..2c24c977b32 --- /dev/null +++ b/ui/app/controllers/administration/sentinel-policies/new.js @@ -0,0 +1,11 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +// @ts-check +import Controller from '@ember/controller'; + +export default class SentinelPoliciesNewController extends Controller { + queryParams = ['template']; +} diff --git a/ui/app/controllers/administration/sentinel-policies/policy.js b/ui/app/controllers/administration/sentinel-policies/policy.js new file mode 100644 index 00000000000..9c4c32f3750 --- /dev/null +++ b/ui/app/controllers/administration/sentinel-policies/policy.js @@ -0,0 +1,46 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +// @ts-check +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; +import { task } from 'ember-concurrency'; +import rollbackWithoutChangedAttrs from 'nomad-ui/utils/rollback-without-changed-attrs'; +import messageFromAdapterError from 'nomad-ui/utils/message-from-adapter-error'; + +export default class SentinelPoliciesPolicyController extends Controller { + @service notifications; + @service router; + + @task(function* () { + try { + yield this.model.destroyRecord(); + this.notifications.add({ + title: 'Policy Deleted', + color: 'success', + type: `success`, + destroyOnClick: false, + }); + this.router.transitionTo('administration.sentinel-policies.index'); + } catch (err) { + // A failed delete resulted in errors when you then navigated away and back + // to the show page rollbackWithoutChangedAttrs fixes it, but there might + // be a more idiomatic way + rollbackWithoutChangedAttrs(this.model); + + let message = err.errors?.length + ? messageFromAdapterError(err) + : err.message || 'Unknown Error'; + + this.notifications.add({ + title: `Error deleting Policy ${this.model.name}`, + message, + color: 'critical', + sticky: true, + }); + } + }) + deletePolicy; +} diff --git a/ui/app/controllers/access-control/tokens/index.js b/ui/app/controllers/administration/tokens/index.js similarity index 88% rename from ui/app/controllers/access-control/tokens/index.js rename to ui/app/controllers/administration/tokens/index.js index 1150feabaa5..c8bd34503b4 100644 --- a/ui/app/controllers/access-control/tokens/index.js +++ b/ui/app/controllers/administration/tokens/index.js @@ -38,10 +38,10 @@ export default class AccessControlTokensIndexController extends Controller { } @action openToken(token) { - this.router.transitionTo('access-control.tokens.token', token.id); + this.router.transitionTo('administration.tokens.token', token.id); } @action goToNewToken() { - this.router.transitionTo('access-control.tokens.new'); + this.router.transitionTo('administration.tokens.new'); } } diff --git a/ui/app/controllers/access-control/tokens/token.js b/ui/app/controllers/administration/tokens/token.js similarity index 94% rename from ui/app/controllers/access-control/tokens/token.js rename to ui/app/controllers/administration/tokens/token.js index 2527fa5928b..00be42885a7 100644 --- a/ui/app/controllers/access-control/tokens/token.js +++ b/ui/app/controllers/administration/tokens/token.js @@ -28,7 +28,7 @@ export default class AccessControlTokensTokenController extends Controller { type: `success`, destroyOnClick: false, }); - this.router.transitionTo('access-control.tokens'); + this.router.transitionTo('administration.tokens'); } catch (err) { this.notifications.add({ title: `Error deleting Token ${this.activeToken.name}`, diff --git a/ui/app/models/sentinel-policy.js b/ui/app/models/sentinel-policy.js new file mode 100644 index 00000000000..8da46bb6766 --- /dev/null +++ b/ui/app/models/sentinel-policy.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Model from '@ember-data/model'; +import { attr } from '@ember-data/model'; + +export default class SentinelPolicy extends Model { + @attr('string') name; + @attr('string') description; + @attr('string') scope; + @attr('string') enforcementLevel; + @attr('string') policy; + @attr('string') hash; + @attr('number') createIndex; + @attr('number') modifyIndex; +} diff --git a/ui/app/router.js b/ui/app/router.js index dde28b832bb..174c5c3c4e8 100644 --- a/ui/app/router.js +++ b/ui/app/router.js @@ -112,7 +112,7 @@ Router.map(function () { }); }); - this.route('access-control', function () { + this.route('administration', function () { this.route('policies', function () { this.route('new'); this.route('policy', { @@ -139,6 +139,11 @@ Router.map(function () { path: '/:name', }); }); + this.route('sentinel-policies', function () { + this.route('new'); + this.route('gallery'); + this.route('policy', { path: '/:id' }); + }); }); // Mirage-only route for testing OIDC flow if (config['ember-cli-mirage']) { diff --git a/ui/app/routes/access-control.js b/ui/app/routes/administration.js similarity index 91% rename from ui/app/routes/access-control.js rename to ui/app/routes/administration.js index e824871bd67..a5872b553c7 100644 --- a/ui/app/routes/access-control.js +++ b/ui/app/routes/administration.js @@ -9,7 +9,7 @@ import WithModelErrorHandling from 'nomad-ui/mixins/with-model-error-handling'; import { inject as service } from '@ember/service'; import RSVP from 'rsvp'; -export default class AccessControlRoute extends Route.extend( +export default class AdministrationRoute extends Route.extend( withForbiddenState, WithModelErrorHandling ) { @@ -35,6 +35,9 @@ export default class AccessControlRoute extends Route.extend( roles: this.store.findAll('role', { reload: true }), tokens: this.store.findAll('token', { reload: true }), namespaces: this.store.findAll('namespace', { reload: true }), + sentinelPolicies: this.can.can('list sentinel-policy') + ? this.store.findAll('sentinel-policy', { reload: true }) + : [], }); } diff --git a/ui/app/routes/access-control/namespaces/acl-namespace.js b/ui/app/routes/administration/namespaces/acl-namespace.js similarity index 100% rename from ui/app/routes/access-control/namespaces/acl-namespace.js rename to ui/app/routes/administration/namespaces/acl-namespace.js diff --git a/ui/app/routes/access-control/namespaces/new.js b/ui/app/routes/administration/namespaces/new.js similarity index 95% rename from ui/app/routes/access-control/namespaces/new.js rename to ui/app/routes/administration/namespaces/new.js index b82610b5e08..77dfdc7b84d 100644 --- a/ui/app/routes/access-control/namespaces/new.js +++ b/ui/app/routes/administration/namespaces/new.js @@ -12,7 +12,7 @@ export default class AccessControlNamespacesNewRoute extends Route { beforeModel() { if (this.can.cannot('write namespace')) { - this.router.transitionTo('/access-control/namespaces'); + this.router.transitionTo('/administration/namespaces'); } } diff --git a/ui/app/routes/access-control/policies/new.js b/ui/app/routes/administration/policies/new.js similarity index 97% rename from ui/app/routes/access-control/policies/new.js rename to ui/app/routes/administration/policies/new.js index eaf0199e362..c965bf3f8d2 100644 --- a/ui/app/routes/access-control/policies/new.js +++ b/ui/app/routes/administration/policies/new.js @@ -90,7 +90,7 @@ export default class AccessControlPoliciesNewRoute extends Route { beforeModel() { if (this.can.cannot('write policy')) { - this.router.transitionTo('/access-control/policies'); + this.router.transitionTo('/administration/policies'); } } diff --git a/ui/app/routes/access-control/policies/policy.js b/ui/app/routes/administration/policies/policy.js similarity index 100% rename from ui/app/routes/access-control/policies/policy.js rename to ui/app/routes/administration/policies/policy.js diff --git a/ui/app/routes/access-control/roles/new.js b/ui/app/routes/administration/roles/new.js similarity index 93% rename from ui/app/routes/access-control/roles/new.js rename to ui/app/routes/administration/roles/new.js index 84b41483dc6..96d9fd2ca6d 100644 --- a/ui/app/routes/access-control/roles/new.js +++ b/ui/app/routes/administration/roles/new.js @@ -12,7 +12,7 @@ export default class AccessControlRolesNewRoute extends Route { beforeModel() { if (this.can.cannot('write role')) { - this.router.transitionTo('/access-control/roles'); + this.router.transitionTo('/administration/roles'); } } diff --git a/ui/app/routes/access-control/roles/role.js b/ui/app/routes/administration/roles/role.js similarity index 100% rename from ui/app/routes/access-control/roles/role.js rename to ui/app/routes/administration/roles/role.js diff --git a/ui/app/routes/administration/sentinel-policies.js b/ui/app/routes/administration/sentinel-policies.js new file mode 100644 index 00000000000..a3f1cb2724b --- /dev/null +++ b/ui/app/routes/administration/sentinel-policies.js @@ -0,0 +1,17 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { inject as service } from '@ember/service'; +import Route from '@ember/routing/route'; +import classic from 'ember-classic-decorator'; + +@classic +export default class AdministrationSentinelPoliciesRoute extends Route { + @service store; + + model() { + return this.store.findAll('sentinel-policy', { reload: true }); + } +} diff --git a/ui/app/routes/administration/sentinel-policies/new.js b/ui/app/routes/administration/sentinel-policies/new.js new file mode 100644 index 00000000000..60dbb25ff46 --- /dev/null +++ b/ui/app/routes/administration/sentinel-policies/new.js @@ -0,0 +1,51 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import TEMPLATES from 'nomad-ui/utils/default-sentinel-policy-templates'; + +export default class NewRoute extends Route { + @service store; + + queryParams = { + template: { + refreshModel: true, + }, + }; + + model({ template }) { + let policy = '#I always pass\nmain = rule { true }\n'; + let name = ''; + let description = ''; + + if (template) { + let matchingTemplate = TEMPLATES.find((t) => t.name == template); + if (matchingTemplate) { + policy = matchingTemplate.policy; + name = matchingTemplate.name; + description = matchingTemplate.description; + } + } + + return this.store.createRecord('sentinel-policy', { + name, + policy, + description, + enforcementLevel: 'advisory', + scope: 'submit-job', + }); + } + + resetController(controller, isExiting) { + if (isExiting) { + // If user didn't save, delete the freshly created model + if (controller.model.isNew) { + controller.model.destroyRecord(); + controller.set('template', null); + } + } + } +} diff --git a/ui/app/routes/administration/sentinel-policies/policy.js b/ui/app/routes/administration/sentinel-policies/policy.js new file mode 100644 index 00000000000..e4a5b542a7a --- /dev/null +++ b/ui/app/routes/administration/sentinel-policies/policy.js @@ -0,0 +1,21 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { inject as service } from '@ember/service'; +import Route from '@ember/routing/route'; + +export default class PolicyRoute extends Route { + @service store; + + async model(params) { + return await this.store.findRecord( + 'sentinel-policy', + decodeURIComponent(params.id), + { + reload: true, + } + ); + } +} diff --git a/ui/app/routes/access-control/tokens/new.js b/ui/app/routes/administration/tokens/new.js similarity index 93% rename from ui/app/routes/access-control/tokens/new.js rename to ui/app/routes/administration/tokens/new.js index 979b84526da..38e242f6774 100644 --- a/ui/app/routes/access-control/tokens/new.js +++ b/ui/app/routes/administration/tokens/new.js @@ -12,7 +12,7 @@ export default class AccessControlTokensNewRoute extends Route { beforeModel() { if (this.can.cannot('write token')) { - this.router.transitionTo('/access-control/tokens'); + this.router.transitionTo('/administration/tokens'); } } diff --git a/ui/app/routes/access-control/tokens/token.js b/ui/app/routes/administration/tokens/token.js similarity index 89% rename from ui/app/routes/access-control/tokens/token.js rename to ui/app/routes/administration/tokens/token.js index 24ef10a67bd..475d9c81fb4 100644 --- a/ui/app/routes/access-control/tokens/token.js +++ b/ui/app/routes/administration/tokens/token.js @@ -19,9 +19,9 @@ export default class AccessControlTokensTokenRoute extends Route.extend( // Route guard to prevent you from wrecking your current token beforeModel() { - let id = this.paramsFor('access-control.tokens.token').id; + let id = this.paramsFor('administration.tokens.token').id; if (this.token.selfToken && this.token.selfToken.id === id) { - this.transitionTo('/access-control/tokens'); + this.transitionTo('/administration/tokens'); } } diff --git a/ui/app/serializers/sentinel-policy.js b/ui/app/serializers/sentinel-policy.js new file mode 100644 index 00000000000..79cda792852 --- /dev/null +++ b/ui/app/serializers/sentinel-policy.js @@ -0,0 +1,12 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import ApplicationSerializer from './application'; +import classic from 'ember-classic-decorator'; + +@classic +export default class SentinelPolicy extends ApplicationSerializer { + primaryKey = 'Name'; +} diff --git a/ui/app/styles/components/access-control.scss b/ui/app/styles/components/access-control.scss index 4f5b9924217..561d7d46104 100644 --- a/ui/app/styles/components/access-control.scss +++ b/ui/app/styles/components/access-control.scss @@ -17,7 +17,7 @@ .section-cards { display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1rem; & > div { padding: 1rem; @@ -83,7 +83,6 @@ .policy-editor { max-height: 600px; - overflow: auto; } .namespace-editor-wrapper { diff --git a/ui/app/styles/core/forms.scss b/ui/app/styles/core/forms.scss index 5f67c6207bf..c841756a23a 100644 --- a/ui/app/styles/core/forms.scss +++ b/ui/app/styles/core/forms.scss @@ -96,6 +96,16 @@ .radio-group { padding: 16px 0px; + + .hds-form-radio-card--checked { + background-color: var(--token-color-surface-action); + } + + .hds-form-radio-card__control-wrapper { + height: 0; + padding: 0; + overflow: hidden; + } } .button-group { diff --git a/ui/app/templates/access-control.hbs b/ui/app/templates/access-control.hbs deleted file mode 100644 index d970551d6f9..00000000000 --- a/ui/app/templates/access-control.hbs +++ /dev/null @@ -1,12 +0,0 @@ -{{! - Copyright (c) HashiCorp, Inc. - SPDX-License-Identifier: BUSL-1.1 -~}} - -{{page-title "Access Control"}} - - - - - {{outlet}} - \ No newline at end of file diff --git a/ui/app/templates/administration.hbs b/ui/app/templates/administration.hbs new file mode 100644 index 00000000000..7f50562325e --- /dev/null +++ b/ui/app/templates/administration.hbs @@ -0,0 +1,12 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 +~}} + +{{page-title "Administration"}} + + + + + {{outlet}} + diff --git a/ui/app/templates/access-control/index.hbs b/ui/app/templates/administration/index.hbs similarity index 68% rename from ui/app/templates/access-control/index.hbs rename to ui/app/templates/administration/index.hbs index 3ddda57ef5f..fbf158af114 100644 --- a/ui/app/templates/access-control/index.hbs +++ b/ui/app/templates/administration/index.hbs @@ -14,40 +14,51 @@
{{this.model.tokens.length}} {{pluralize "Token" this.model.tokens.length}}

User access tokens are associated with one or more policies or roles to grant specific capabilities.

- +
{{this.model.roles.length}} {{pluralize "Role" this.model.roles.length}}

Roles group one or more Policies into higher-level sets of permissions.

- +
{{this.model.policies.length}} {{pluralize "Policy" this.model.policies.length}}

Sets of rules defining the capabilities granted to adhering tokens.

- +
{{this.model.namespaces.length}} {{pluralize "Namespace" this.model.namespaces.length}}

Namespaces allow jobs and other objects to be segmented from each other.

- +
+ {{#if (can "read sentinel-policy")}} + + + {{this.model.sentinelPolicies.length}} {{pluralize "Sentinel Policy" this.model.sentinelPolicies.length}} + +

Sentinel Policies allow operators to express rules as code and have those rules automatically enforced when jobs are planned.

+ +
+ {{/if}}
{{outlet}} \ No newline at end of file diff --git a/ui/app/templates/access-control/namespaces.hbs b/ui/app/templates/administration/namespaces.hbs similarity index 55% rename from ui/app/templates/access-control/namespaces.hbs rename to ui/app/templates/administration/namespaces.hbs index 29156e94253..3241749455b 100644 --- a/ui/app/templates/access-control/namespaces.hbs +++ b/ui/app/templates/administration/namespaces.hbs @@ -4,5 +4,5 @@ ~}} {{page-title "Namespaces"}} - + {{outlet}} \ No newline at end of file diff --git a/ui/app/templates/access-control/namespaces/acl-namespace.hbs b/ui/app/templates/administration/namespaces/acl-namespace.hbs similarity index 91% rename from ui/app/templates/access-control/namespaces/acl-namespace.hbs rename to ui/app/templates/administration/namespaces/acl-namespace.hbs index d58fc1f3c19..77a8f2c7ba1 100644 --- a/ui/app/templates/access-control/namespaces/acl-namespace.hbs +++ b/ui/app/templates/administration/namespaces/acl-namespace.hbs @@ -2,7 +2,7 @@ Copyright (c) HashiCorp, Inc. SPDX-License-Identifier: BUSL-1.1 ~}} - + {{page-title "Namespace"}}
diff --git a/ui/app/templates/access-control/namespaces/index.hbs b/ui/app/templates/administration/namespaces/index.hbs similarity index 92% rename from ui/app/templates/access-control/namespaces/index.hbs rename to ui/app/templates/administration/namespaces/index.hbs index dc9180e887b..bb4b427eb15 100644 --- a/ui/app/templates/access-control/namespaces/index.hbs +++ b/ui/app/templates/administration/namespaces/index.hbs @@ -13,7 +13,7 @@ - {{B.data.name}} + {{B.data.name}} {{B.data.description}} diff --git a/ui/app/templates/access-control/namespaces/new.hbs b/ui/app/templates/administration/namespaces/new.hbs similarity index 75% rename from ui/app/templates/access-control/namespaces/new.hbs rename to ui/app/templates/administration/namespaces/new.hbs index aa36bfe66a1..613ce099254 100644 --- a/ui/app/templates/access-control/namespaces/new.hbs +++ b/ui/app/templates/administration/namespaces/new.hbs @@ -3,7 +3,7 @@ SPDX-License-Identifier: BUSL-1.1 ~}} - + {{page-title "Create Namespace"}}

diff --git a/ui/app/templates/access-control/policies.hbs b/ui/app/templates/administration/policies.hbs similarity index 56% rename from ui/app/templates/access-control/policies.hbs rename to ui/app/templates/administration/policies.hbs index ad1807ef0e7..ab0ccba0c53 100644 --- a/ui/app/templates/access-control/policies.hbs +++ b/ui/app/templates/administration/policies.hbs @@ -4,5 +4,5 @@ ~}} {{page-title "Policies"}} - + {{outlet}} diff --git a/ui/app/templates/access-control/policies/index.hbs b/ui/app/templates/administration/policies/index.hbs similarity index 93% rename from ui/app/templates/access-control/policies/index.hbs rename to ui/app/templates/administration/policies/index.hbs index b2716fb2c00..85c45d9c61d 100644 --- a/ui/app/templates/access-control/policies/index.hbs +++ b/ui/app/templates/administration/policies/index.hbs @@ -13,7 +13,7 @@ - {{B.data.name}} + {{B.data.name}} {{B.data.description}} {{#if (can "list token")}} @@ -76,7 +76,7 @@ No Policies

- Get started by creating a new policy + Get started by creating a new policy

{{/if}} diff --git a/ui/app/templates/access-control/policies/new.hbs b/ui/app/templates/administration/policies/new.hbs similarity index 75% rename from ui/app/templates/access-control/policies/new.hbs rename to ui/app/templates/administration/policies/new.hbs index a2c92105be4..7b84fc665ab 100644 --- a/ui/app/templates/access-control/policies/new.hbs +++ b/ui/app/templates/administration/policies/new.hbs @@ -3,7 +3,7 @@ SPDX-License-Identifier: BUSL-1.1 ~}} - + {{page-title "Create Policy"}}

diff --git a/ui/app/templates/access-control/policies/policy.hbs b/ui/app/templates/administration/policies/policy.hbs similarity index 97% rename from ui/app/templates/access-control/policies/policy.hbs rename to ui/app/templates/administration/policies/policy.hbs index a899dda970b..b73550bdccf 100644 --- a/ui/app/templates/access-control/policies/policy.hbs +++ b/ui/app/templates/administration/policies/policy.hbs @@ -3,7 +3,7 @@ SPDX-License-Identifier: BUSL-1.1 ~}} - + {{page-title "Policy"}}

diff --git a/ui/app/templates/access-control/roles.hbs b/ui/app/templates/administration/roles.hbs similarity index 57% rename from ui/app/templates/access-control/roles.hbs rename to ui/app/templates/administration/roles.hbs index 5ad0534371e..91dfdab65d3 100644 --- a/ui/app/templates/access-control/roles.hbs +++ b/ui/app/templates/administration/roles.hbs @@ -4,5 +4,5 @@ ~}} {{page-title "Roles"}} - + {{outlet}} \ No newline at end of file diff --git a/ui/app/templates/access-control/roles/index.hbs b/ui/app/templates/administration/roles/index.hbs similarity index 93% rename from ui/app/templates/access-control/roles/index.hbs rename to ui/app/templates/administration/roles/index.hbs index de16e0f5b47..ec62b217159 100644 --- a/ui/app/templates/access-control/roles/index.hbs +++ b/ui/app/templates/administration/roles/index.hbs @@ -13,7 +13,7 @@ - {{B.data.name}} + {{B.data.name}} {{B.data.description}} {{#if (can "list token")}} @@ -63,7 +63,7 @@ {{#each B.data.policyNames as |policyName|}} {{#let (find-by "name" policyName this.model.policies) as |policy|}} {{#if policy}} - + {{else}}

- Get started by creating a new role + Get started by creating a new role

{{/if}} diff --git a/ui/app/templates/access-control/roles/new.hbs b/ui/app/templates/administration/roles/new.hbs similarity index 80% rename from ui/app/templates/access-control/roles/new.hbs rename to ui/app/templates/administration/roles/new.hbs index c8d623ccb0c..ac144bd88bc 100644 --- a/ui/app/templates/access-control/roles/new.hbs +++ b/ui/app/templates/administration/roles/new.hbs @@ -3,7 +3,7 @@ SPDX-License-Identifier: BUSL-1.1 ~}} - + {{page-title "Create Role"}}

@@ -20,7 +20,7 @@ No Policies

- At least one Policy is required to create a Role; create a new policy + At least one Policy is required to create a Role; create a new policy

{{/if}} diff --git a/ui/app/templates/access-control/roles/role.hbs b/ui/app/templates/administration/roles/role.hbs similarity index 97% rename from ui/app/templates/access-control/roles/role.hbs rename to ui/app/templates/administration/roles/role.hbs index 926acb87058..0f75be33e8c 100644 --- a/ui/app/templates/access-control/roles/role.hbs +++ b/ui/app/templates/administration/roles/role.hbs @@ -2,7 +2,7 @@ Copyright (c) HashiCorp, Inc. SPDX-License-Identifier: BUSL-1.1 ~}} - + {{page-title "Role"}}

diff --git a/ui/app/templates/administration/sentinel-policies.hbs b/ui/app/templates/administration/sentinel-policies.hbs new file mode 100644 index 00000000000..1dc81ec3d2d --- /dev/null +++ b/ui/app/templates/administration/sentinel-policies.hbs @@ -0,0 +1,8 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 +~}} + + +{{page-title "Sentinel Policies"}} +{{outlet}} diff --git a/ui/app/templates/administration/sentinel-policies/gallery.hbs b/ui/app/templates/administration/sentinel-policies/gallery.hbs new file mode 100644 index 00000000000..5d11488cee8 --- /dev/null +++ b/ui/app/templates/administration/sentinel-policies/gallery.hbs @@ -0,0 +1,35 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 +~}} + + +{{page-title "Sentinel Policy Gallery"}} +
+ + Choose a Template + + Select a policy template below. You will have an opportunity to modify the policy before it is submitted. + + +
+ + Select a Template + {{#each this.templates as |template|}} + + {{template.displayName}} + {{template.description}} + + {{/each}} + +
+
+ + + + +
+
diff --git a/ui/app/templates/administration/sentinel-policies/index.hbs b/ui/app/templates/administration/sentinel-policies/index.hbs new file mode 100644 index 00000000000..736adeed3d8 --- /dev/null +++ b/ui/app/templates/administration/sentinel-policies/index.hbs @@ -0,0 +1,78 @@ +{{! +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: BUSL-1.1 +~}} + +
+ + Sentinel Policies + + Nomad integrates with HashiCorp Sentinel to allow operators to express policies as code and have those policies automatically enforced. This allows operators to define a "sandbox" and restrict actions to only those compliant with that policy. + + + {{#if (can "write sentinel-policy")}} + + + + + + + {{else}} + + {{/if}} + + + + {{#if this.model}} + + <:body as |B|> + + + {{B.data.name}} + + {{B.data.description}} + {{B.data.enforcementLevel}} + {{#if (can "destroy sentinel-policy")}} + + + + {{/if}} + + + + {{else}} +
+

+ No Sentinel Policies +

+

+ Get started by creating a policy from scratch or + by creating one from the policy gallery. +

+
+ {{/if}} +
diff --git a/ui/app/templates/administration/sentinel-policies/new.hbs b/ui/app/templates/administration/sentinel-policies/new.hbs new file mode 100644 index 00000000000..5a5d935226b --- /dev/null +++ b/ui/app/templates/administration/sentinel-policies/new.hbs @@ -0,0 +1,26 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 +~}} + + +{{page-title "Create a Policy"}} +
+ + Create Sentinel Policy + + Nomad integrates with HashiCorp Sentinel to allow operators to express policies as code and have those policies automatically enforced. This allows operators to define a "sandbox" and restrict actions to only those compliant with that policy. + + + + + + + + +
diff --git a/ui/app/templates/administration/sentinel-policies/policy.hbs b/ui/app/templates/administration/sentinel-policies/policy.hbs new file mode 100644 index 00000000000..45a445902c8 --- /dev/null +++ b/ui/app/templates/administration/sentinel-policies/policy.hbs @@ -0,0 +1,32 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 +~}} + + +{{page-title (concat "Sentinel Policy: " this.model.name)}} + +
+ + {{this.model.name}} + + {{#if (can "destroy sentinel-policy")}} +
+ +
+ {{/if}} +
+
+ + +
diff --git a/ui/app/templates/access-control/tokens.hbs b/ui/app/templates/administration/tokens.hbs similarity index 57% rename from ui/app/templates/access-control/tokens.hbs rename to ui/app/templates/administration/tokens.hbs index f1987c3aa7e..56bcc5a3bb6 100644 --- a/ui/app/templates/access-control/tokens.hbs +++ b/ui/app/templates/administration/tokens.hbs @@ -4,5 +4,5 @@ ~}} {{page-title "Tokens"}} - + {{outlet}} diff --git a/ui/app/templates/access-control/tokens/index.hbs b/ui/app/templates/administration/tokens/index.hbs similarity index 94% rename from ui/app/templates/access-control/tokens/index.hbs rename to ui/app/templates/administration/tokens/index.hbs index 76f0ed113cb..aafaf5b422e 100644 --- a/ui/app/templates/access-control/tokens/index.hbs +++ b/ui/app/templates/administration/tokens/index.hbs @@ -13,7 +13,7 @@ {{B.data.name}} {{else}} - + {{B.data.name}} {{/if}} @@ -84,7 +84,7 @@ --}} {{#each B.data.roles as |role|}} {{#if role.name}} - + {{/if}} {{else}} {{#if (eq B.data.type "management")}} @@ -101,7 +101,7 @@ {{#each B.data.policyNames as |policyName|}} {{#let (find-by "name" policyName this.model.policies) as |policy|}} {{#if policy}} - + {{else}}

- Get started by creating a new policy + Get started by creating a new policy

{{/if}} diff --git a/ui/app/templates/access-control/tokens/new.hbs b/ui/app/templates/administration/tokens/new.hbs similarity index 79% rename from ui/app/templates/access-control/tokens/new.hbs rename to ui/app/templates/administration/tokens/new.hbs index 85c54292a5a..32545da73a5 100644 --- a/ui/app/templates/access-control/tokens/new.hbs +++ b/ui/app/templates/administration/tokens/new.hbs @@ -3,7 +3,7 @@ SPDX-License-Identifier: BUSL-1.1 ~}} - + {{page-title "Create Token"}}

diff --git a/ui/app/templates/access-control/tokens/token.hbs b/ui/app/templates/administration/tokens/token.hbs similarity index 93% rename from ui/app/templates/access-control/tokens/token.hbs rename to ui/app/templates/administration/tokens/token.hbs index 9a7f69ba5a3..5c9c4d03b06 100644 --- a/ui/app/templates/access-control/tokens/token.hbs +++ b/ui/app/templates/administration/tokens/token.hbs @@ -2,7 +2,7 @@ Copyright (c) HashiCorp, Inc. SPDX-License-Identifier: BUSL-1.1 ~}} - + {{page-title "Token"}}

diff --git a/ui/app/templates/components/access-control-subnav.hbs b/ui/app/templates/components/access-control-subnav.hbs deleted file mode 100644 index e380095276e..00000000000 --- a/ui/app/templates/components/access-control-subnav.hbs +++ /dev/null @@ -1,14 +0,0 @@ -{{! - Copyright (c) HashiCorp, Inc. - SPDX-License-Identifier: BUSL-1.1 -~}} - -
-
    -
  • Overview
  • -
  • Tokens
  • -
  • Roles
  • -
  • Policies
  • -
  • Namespaces
  • -
-
diff --git a/ui/app/templates/components/administration-subnav.hbs b/ui/app/templates/components/administration-subnav.hbs new file mode 100644 index 00000000000..bdb69e8b614 --- /dev/null +++ b/ui/app/templates/components/administration-subnav.hbs @@ -0,0 +1,17 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 +~}} + +
+
    +
  • Overview
  • +
  • Tokens
  • +
  • Roles
  • +
  • Policies
  • +
  • Namespaces
  • + {{#if (can "list sentinel-policy")}} +
  • Sentinel Policies
  • + {{/if}} +
+
diff --git a/ui/app/templates/components/gutter-menu.hbs b/ui/app/templates/components/gutter-menu.hbs index a5210e8cbae..8dc68113030 100644 --- a/ui/app/templates/components/gutter-menu.hbs +++ b/ui/app/templates/components/gutter-menu.hbs @@ -136,15 +136,15 @@ {{keyboard-shortcut menuLevel=true pattern=(array "g" "a") - action=(action this.transitionTo 'access-control') + action=(action this.transitionTo 'administration') }} > - Access Control + Administration {{/if}} diff --git a/ui/app/templates/components/job-editor.hbs b/ui/app/templates/components/job-editor.hbs index 76277dfd251..4dcfbb8b76e 100644 --- a/ui/app/templates/components/job-editor.hbs +++ b/ui/app/templates/components/job-editor.hbs @@ -23,7 +23,7 @@ >
Upload file
diff --git a/ui/app/templates/components/job-editor/read.hbs b/ui/app/templates/components/job-editor/read.hbs index 68abf6f0a86..94677a96067 100644 --- a/ui/app/templates/components/job-editor/read.hbs +++ b/ui/app/templates/components/job-editor/read.hbs @@ -51,7 +51,7 @@
{{#if (eq @data.view "job-spec")}}
0 + } +} + +main = rule { canary_required }`; diff --git a/ui/app/utils/sentinel_policy_templates/count-limits.js b/ui/app/utils/sentinel_policy_templates/count-limits.js new file mode 100644 index 00000000000..3a0aa3be6cc --- /dev/null +++ b/ui/app/utils/sentinel_policy_templates/count-limits.js @@ -0,0 +1,15 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +export default `main = rule { all_counts_under } + +# all_counts_under checks that all task group counts are under a certain value + +all_counts_under = rule { + all job.task_groups as tg { + tg.count < 100 + } +} +`; diff --git a/ui/app/utils/sentinel_policy_templates/no-friday-deploys.js b/ui/app/utils/sentinel_policy_templates/no-friday-deploys.js new file mode 100644 index 00000000000..22da732f6a8 --- /dev/null +++ b/ui/app/utils/sentinel_policy_templates/no-friday-deploys.js @@ -0,0 +1,11 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +export default `import "time" + +is_weekday = rule { time.day not in ["friday", "saturday", "sunday"] } +is_open_hours = rule { time.hour > 8 and time.hour < 16 } + +main = rule { is_open_hours and is_weekday }`; diff --git a/ui/app/utils/sentinel_policy_templates/resource-limits.js b/ui/app/utils/sentinel_policy_templates/resource-limits.js new file mode 100644 index 00000000000..3ab6dd8b064 --- /dev/null +++ b/ui/app/utils/sentinel_policy_templates/resource-limits.js @@ -0,0 +1,21 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +export default `import "units" + +resource_check = func(task_groups, resource) { + result = 0 + for task_groups as g { + for g.tasks as t { + result = result + t.resources[resource] * g.count + } + } + return result +} + +main = rule { + resource_check(job.task_groups, "cpu") <= 1500 and + resource_check(job.task_groups, "memory_mb") <= 2500 +}`; diff --git a/ui/app/utils/sentinel_policy_templates/restrict-images.js b/ui/app/utils/sentinel_policy_templates/restrict-images.js new file mode 100644 index 00000000000..d8e8dfbc7d8 --- /dev/null +++ b/ui/app/utils/sentinel_policy_templates/restrict-images.js @@ -0,0 +1,32 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +export default `# This policy restricts which Docker images are allowed and also prevents use of +# the "latest" tag since the image must specify a tag that starts with a number. + +# Allowed Docker images +allowed_images = [ + "https://hub.docker.internal", + "nginx", + "mongo", +] + +# Restrict allowed Docker images +restrict_images = rule { + all job.task_groups as tg { + all tg.tasks as task { + any allowed_images as allowed { + # Note that we require ":" and a tag after it + # which must start with a number, preventing "latest" + task.config.image matches allowed + ":[0-9](.*)" + } + } + } +} + +# Main rule +main = rule { + restrict_images +}`; diff --git a/ui/mirage/config.js b/ui/mirage/config.js index 8f3d3c6f75c..859c7de496d 100644 --- a/ui/mirage/config.js +++ b/ui/mirage/config.js @@ -881,6 +881,23 @@ export default function () { return this.serialize(policies.all()); }); + this.get('/sentinel/policies', function (schema, req) { + return this.serialize(schema.sentinelPolicies.all()); + }); + + this.post('/sentinel/policy/:id', function (schema, req) { + const { Name, Description, Rules } = JSON.parse(req.requestBody); + return server.create('sentinelPolicy', { + name: Name, + description: Description, + rules: Rules, + }); + }); + + this.get('/sentinel/policy/:id', function ({ sentinelPolicies }, req) { + return this.serialize(sentinelPolicies.findBy({ name: req.params.id })); + }); + this.delete('/acl/policy/:id', function (schema, request) { const { id } = request.params; diff --git a/ui/mirage/scenarios/default.js b/ui/mirage/scenarios/default.js index 40bd9ce7329..bdbbe5f34da 100644 --- a/ui/mirage/scenarios/default.js +++ b/ui/mirage/scenarios/default.js @@ -90,6 +90,7 @@ function jobsIndexTestCluster(server) { function smallCluster(server) { faker.seed(1); server.create('feature', { name: 'Dynamic Application Sizing' }); + server.create('feature', { name: 'Sentinel Policies' }); server.createList('agent', 3, 'withConsulLink', 'withVaultLink'); server.createList('node-pool', 2); server.createList('node', 5); @@ -604,6 +605,7 @@ function variableTestCluster(server) { } function policiesTestCluster(server) { + server.create('feature', { name: 'Sentinel Policies' }); faker.seed(1); createTokens(server); server.createList('agent', 3, 'withConsulLink', 'withVaultLink'); diff --git a/ui/tests/acceptance/access-control-test.js b/ui/tests/acceptance/access-control-test.js index 4e841837972..f75ce3f2187 100644 --- a/ui/tests/acceptance/access-control-test.js +++ b/ui/tests/acceptance/access-control-test.js @@ -7,7 +7,7 @@ import { module, test } from 'qunit'; import { currentURL, triggerKeyEvent } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; -import AccessControl from 'nomad-ui/tests/pages/access-control'; +import Administration from 'nomad-ui/tests/pages/administration'; import Tokens from 'nomad-ui/tests/pages/settings/tokens'; import { allScenarios } from '../../mirage/scenarios/default'; import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit'; @@ -28,22 +28,22 @@ module('Acceptance | access control', function (hooks) { test('Access Control is only accessible by a management user', async function (assert) { assert.expect(7); - await AccessControl.visit(); + await Administration.visit(); assert.equal( currentURL(), '/jobs', - 'redirected to the jobs page if a non-management token on /access-control' + 'redirected to the jobs page if a non-management token on /administration' ); - await AccessControl.visitTokens(); + await Administration.visitTokens(); assert.equal( currentURL(), '/jobs', 'redirected to the jobs page if a non-management token on /tokens' ); - assert.dom('[data-test-gutter-link="access-control"]').doesNotExist(); + assert.dom('[data-test-gutter-link="administration"]').doesNotExist(); await Tokens.visit(); const managementToken = server.db.tokens.findBy( @@ -52,22 +52,22 @@ module('Acceptance | access control', function (hooks) { const { secretId } = managementToken; await Tokens.secret(secretId).submit(); - assert.dom('[data-test-gutter-link="access-control"]').exists(); + assert.dom('[data-test-gutter-link="administration"]').exists(); - await AccessControl.visit(); + await Administration.visit(); assert.equal( currentURL(), - '/access-control', - 'management token can access /access-control' + '/administration', + 'management token can access /administration' ); await a11yAudit(assert); - await AccessControl.visitTokens(); + await Administration.visitTokens(); assert.equal( currentURL(), - '/access-control/tokens', - 'management token can access /access-control/tokens' + '/administration/tokens', + 'management token can access /administration/tokens' ); }); @@ -79,7 +79,7 @@ module('Acceptance | access control', function (hooks) { const { secretId } = managementToken; await Tokens.secret(secretId).submit(); - await AccessControl.visit(); + await Administration.visit(); assert.dom('[data-test-tokens-card]').exists(); assert.dom('[data-test-roles-card]').exists(); assert.dom('[data-test-policies-card]').exists(); @@ -112,16 +112,16 @@ module('Acceptance | access control', function (hooks) { const { secretId } = managementToken; await Tokens.secret(secretId).submit(); - await AccessControl.visit(); + await Administration.visit(); - assert.equal(currentURL(), '/access-control'); + assert.equal(currentURL(), '/administration'); await triggerKeyEvent('.page-layout', 'keydown', 'ArrowRight', { shiftKey: true, }); assert.equal( currentURL(), - `/access-control/tokens`, + `/administration/tokens`, 'Shift+ArrowRight takes you to the next tab (Tokens)' ); @@ -130,7 +130,7 @@ module('Acceptance | access control', function (hooks) { }); assert.equal( currentURL(), - `/access-control/roles`, + `/administration/roles`, 'Shift+ArrowRight takes you to the next tab (Roles)' ); @@ -139,7 +139,7 @@ module('Acceptance | access control', function (hooks) { }); assert.equal( currentURL(), - `/access-control/policies`, + `/administration/policies`, 'Shift+ArrowRight takes you to the next tab (Policies)' ); @@ -148,7 +148,7 @@ module('Acceptance | access control', function (hooks) { }); assert.equal( currentURL(), - `/access-control/namespaces`, + `/administration/namespaces`, 'Shift+ArrowRight takes you to the next tab (Namespaces)' ); @@ -157,7 +157,7 @@ module('Acceptance | access control', function (hooks) { }); assert.equal( currentURL(), - `/access-control`, + `/administration`, 'Shift+ArrowLeft takes you back to the Access Control index page' ); }); diff --git a/ui/tests/acceptance/namespaces-test.js b/ui/tests/acceptance/namespaces-test.js index d226475c13e..f45167ee129 100644 --- a/ui/tests/acceptance/namespaces-test.js +++ b/ui/tests/acceptance/namespaces-test.js @@ -26,9 +26,9 @@ module('Acceptance | namespaces', function (hooks) { assert.expect(4); allScenarios.namespacesTestCluster(server); window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId; - await visit('/access-control/namespaces'); - assert.dom('[data-test-gutter-link="access-control"]').exists(); - assert.equal(currentURL(), '/access-control/namespaces'); + await visit('/administration/namespaces'); + assert.dom('[data-test-gutter-link="administration"]').exists(); + assert.equal(currentURL(), '/administration/namespaces'); assert .dom('[data-test-namespace-row]') .exists({ count: server.db.namespaces.length }); @@ -41,9 +41,9 @@ module('Acceptance | namespaces', function (hooks) { test('Prevents namespaes access if you lack a management token', async function (assert) { allScenarios.namespacesTestCluster(server); window.localStorage.nomadTokenSecret = server.db.tokens[1].secretId; - await visit('/access-control/namespaces'); + await visit('/administration/namespaces'); assert.equal(currentURL(), '/jobs'); - assert.dom('[data-test-gutter-link="access-control"]').doesNotExist(); + assert.dom('[data-test-gutter-link="administration"]').doesNotExist(); // Reset Token window.localStorage.nomadTokenSecret = null; }); @@ -52,15 +52,15 @@ module('Acceptance | namespaces', function (hooks) { assert.expect(7); allScenarios.namespacesTestCluster(server); window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId; - await visit('/access-control/namespaces'); + await visit('/administration/namespaces'); await click('[data-test-create-namespace]'); - assert.equal(currentURL(), '/access-control/namespaces/new'); + assert.equal(currentURL(), '/administration/namespaces/new'); await typeIn('[data-test-namespace-name-input]', 'My New Namespace'); await click('button[data-test-save-namespace]'); assert .dom('.flash-message.alert-critical') .exists('Doesnt let you save a bad name'); - assert.equal(currentURL(), '/access-control/namespaces/new'); + assert.equal(currentURL(), '/administration/namespaces/new'); document.querySelector('[data-test-namespace-name-input]').value = ''; // clear await typeIn('[data-test-namespace-name-input]', 'My-New-Namespace'); await click('button[data-test-save-namespace]'); @@ -68,16 +68,16 @@ module('Acceptance | namespaces', function (hooks) { assert.equal( currentURL(), - '/access-control/namespaces/My-New-Namespace', + '/administration/namespaces/My-New-Namespace', 'redirected to the now-created namespace' ); - await visit('/access-control/namespaces'); + await visit('/administration/namespaces'); const newNs = [...findAll('[data-test-namespace-name]')].filter((a) => a.textContent.includes('My-New-Namespace') )[0]; assert.ok(newNs, 'Namespace is in the list'); await click(newNs); - assert.equal(currentURL(), '/access-control/namespaces/My-New-Namespace'); + assert.equal(currentURL(), '/administration/namespaces/My-New-Namespace'); await percySnapshot(assert); // Reset Token window.localStorage.nomadTokenSecret = null; @@ -87,7 +87,7 @@ module('Acceptance | namespaces', function (hooks) { assert.expect(2); allScenarios.namespacesTestCluster(server, { enterprise: true }); window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId; - await visit('/access-control/namespaces'); + await visit('/administration/namespaces'); await click('[data-test-create-namespace]'); // Get the dom node text for the description @@ -114,7 +114,7 @@ module('Acceptance | namespaces', function (hooks) { assert.expect(2); allScenarios.namespacesTestCluster(server, { enterprise: false }); window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId; - await visit('/access-control/namespaces'); + await visit('/administration/namespaces'); await click('[data-test-create-namespace]'); // Get the dom node text for the description @@ -132,7 +132,7 @@ module('Acceptance | namespaces', function (hooks) { test('Modifying an existing namespace', async function (assert) { allScenarios.namespacesTestCluster(server); window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId; - await visit('/access-control/namespaces'); + await visit('/administration/namespaces'); await click('[data-test-namespace-row]:first-child a'); // Table sorts by name by default let firstNamespace = server.db.namespaces.sort((a, b) => { @@ -140,7 +140,7 @@ module('Acceptance | namespaces', function (hooks) { })[0]; assert.equal( currentURL(), - `/access-control/namespaces/${firstNamespace.name}` + `/administration/namespaces/${firstNamespace.name}` ); assert.dom('[data-test-namespace-editor]').exists(); assert.dom('[data-test-title]').includesText(firstNamespace.name); @@ -148,7 +148,7 @@ module('Acceptance | namespaces', function (hooks) { assert.dom('.flash-message.alert-success').exists(); assert.equal( currentURL(), - `/access-control/namespaces/${firstNamespace.name}`, + `/administration/namespaces/${firstNamespace.name}`, 'remain on page after save' ); // Reset Token @@ -159,7 +159,7 @@ module('Acceptance | namespaces', function (hooks) { assert.expect(11); allScenarios.namespacesTestCluster(server); window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId; - await visit('/access-control/namespaces'); + await visit('/administration/namespaces'); // Default namespace hides delete button const defaultNamespaceLink = [ @@ -167,14 +167,14 @@ module('Acceptance | namespaces', function (hooks) { ].filter((row) => row.textContent.includes('default'))[0]; await click(defaultNamespaceLink); - assert.equal(currentURL(), `/access-control/namespaces/default`); + assert.equal(currentURL(), `/administration/namespaces/default`); let deleteButton = find('[data-test-delete-namespace] button'); assert .dom(deleteButton) .doesNotExist('delete button is not present for default'); // Standard namespace properly deletes - await visit('/access-control/namespaces'); + await visit('/administration/namespaces'); let nonDefaultNamespace = server.db.namespaces.findBy( (ns) => ns.name != 'default' @@ -185,7 +185,7 @@ module('Acceptance | namespaces', function (hooks) { await click(nonDefaultNsLink); assert.equal( currentURL(), - `/access-control/namespaces/${nonDefaultNamespace.name}` + `/administration/namespaces/${nonDefaultNamespace.name}` ); deleteButton = find('[data-test-delete-namespace] button'); assert.dom(deleteButton).exists('delete button is present for non-default'); @@ -195,15 +195,15 @@ module('Acceptance | namespaces', function (hooks) { .exists('confirmation message is present'); await click(find('[data-test-confirm-button]')); assert.dom('.flash-message.alert-success').exists(); - assert.equal(currentURL(), '/access-control/namespaces'); + assert.equal(currentURL(), '/administration/namespaces'); assert .dom(`[data-test-namespace-name="${nonDefaultNamespace.name}"]`) .doesNotExist(); // Namespace with variables errors properly // "with-variables" hard-coded into scenario to be a NS with variables attached - await visit('/access-control/namespaces/with-variables'); - assert.equal(currentURL(), '/access-control/namespaces/with-variables'); + await visit('/administration/namespaces/with-variables'); + assert.equal(currentURL(), '/administration/namespaces/with-variables'); deleteButton = find('[data-test-delete-namespace] button'); await click(deleteButton); await click(find('[data-test-confirm-button]')); @@ -211,7 +211,7 @@ module('Acceptance | namespaces', function (hooks) { .dom('.flash-message.alert-critical') .exists('Doesnt let you delete a namespace with variables'); - assert.equal(currentURL(), '/access-control/namespaces/with-variables'); + assert.equal(currentURL(), '/administration/namespaces/with-variables'); // Reset Token window.localStorage.nomadTokenSecret = null; @@ -228,7 +228,7 @@ module('Acceptance | namespaces', function (hooks) { window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId; // Attempt a delete on an un-deletable namespace - await visit('/access-control/namespaces/with-variables'); + await visit('/administration/namespaces/with-variables'); let deleteButton = find('[data-test-delete-namespace] button'); await click(deleteButton); await click(find('[data-test-confirm-button]')); @@ -236,10 +236,10 @@ module('Acceptance | namespaces', function (hooks) { assert .dom('.flash-message.alert-critical') .exists('Doesnt let you delete a namespace with variables'); - assert.equal(currentURL(), '/access-control/namespaces/with-variables'); + assert.equal(currentURL(), '/administration/namespaces/with-variables'); // Navigate back to the page via the index - await visit('/access-control/namespaces'); + await visit('/administration/namespaces'); // Default namespace hides delete button const notDeletedNSLink = [...findAll('[data-test-namespace-name]')].filter( @@ -247,6 +247,6 @@ module('Acceptance | namespaces', function (hooks) { )[0]; await click(notDeletedNSLink); - assert.equal(currentURL(), `/access-control/namespaces/with-variables`); + assert.equal(currentURL(), `/administration/namespaces/with-variables`); }); }); diff --git a/ui/tests/acceptance/policies-test.js b/ui/tests/acceptance/policies-test.js index e04d01ec041..719224c146c 100644 --- a/ui/tests/acceptance/policies-test.js +++ b/ui/tests/acceptance/policies-test.js @@ -26,9 +26,9 @@ module('Acceptance | policies', function (hooks) { assert.expect(4); allScenarios.policiesTestCluster(server); window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId; - await visit('/access-control/policies'); - assert.dom('[data-test-gutter-link="access-control"]').exists(); - assert.equal(currentURL(), '/access-control/policies'); + await visit('/administration/policies'); + assert.dom('[data-test-gutter-link="administration"]').exists(); + assert.equal(currentURL(), '/administration/policies'); assert .dom('[data-test-policy-row]') .exists({ count: server.db.policies.length }); @@ -41,9 +41,9 @@ module('Acceptance | policies', function (hooks) { test('Prevents policies access if you lack a management token', async function (assert) { allScenarios.policiesTestCluster(server); window.localStorage.nomadTokenSecret = server.db.tokens[1].secretId; - await visit('/access-control/policies'); + await visit('/administration/policies'); assert.equal(currentURL(), '/jobs'); - assert.dom('[data-test-gutter-link="access-control"]').doesNotExist(); + assert.dom('[data-test-gutter-link="administration"]').doesNotExist(); // Reset Token window.localStorage.nomadTokenSecret = null; }); @@ -51,20 +51,20 @@ module('Acceptance | policies', function (hooks) { test('Modifying an existing policy', async function (assert) { allScenarios.policiesTestCluster(server); window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId; - await visit('/access-control/policies'); + await visit('/administration/policies'); await click('[data-test-policy-row]:first-child a'); // Table sorts by name by default let firstPolicy = server.db.policies.sort((a, b) => { return a.name.localeCompare(b.name); })[0]; - assert.equal(currentURL(), `/access-control/policies/${firstPolicy.name}`); + assert.equal(currentURL(), `/administration/policies/${firstPolicy.name}`); assert.dom('[data-test-policy-editor]').exists(); assert.dom('[data-test-title]').includesText(firstPolicy.name); await click('button[data-test-save-policy]'); assert.dom('.flash-message.alert-success').exists(); assert.equal( currentURL(), - `/access-control/policies/${firstPolicy.name}`, + `/administration/policies/${firstPolicy.name}`, 'remain on page after save' ); // Reset Token @@ -74,9 +74,9 @@ module('Acceptance | policies', function (hooks) { test('Creating a test token', async function (assert) { allScenarios.policiesTestCluster(server); window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId; - await visit('/access-control/policies'); + await visit('/administration/policies'); await click('[data-test-policy-name="Variable-Maker"]'); - assert.equal(currentURL(), '/access-control/policies/Variable-Maker'); + assert.equal(currentURL(), '/administration/policies/Variable-Maker'); await click('[data-test-create-test-token]'); assert.dom('.flash-message.alert-success').exists(); assert @@ -100,31 +100,31 @@ module('Acceptance | policies', function (hooks) { assert.expect(7); allScenarios.policiesTestCluster(server); window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId; - await visit('/access-control/policies'); + await visit('/administration/policies'); await click('[data-test-create-policy]'); - assert.equal(currentURL(), '/access-control/policies/new'); + assert.equal(currentURL(), '/administration/policies/new'); await typeIn('[data-test-policy-name-input]', 'My Fun Policy'); await click('button[data-test-save-policy]'); assert .dom('.flash-message.alert-critical') .exists('Doesnt let you save a bad name'); - assert.equal(currentURL(), '/access-control/policies/new'); + assert.equal(currentURL(), '/administration/policies/new'); document.querySelector('[data-test-policy-name-input]').value = ''; // clear await typeIn('[data-test-policy-name-input]', 'My-Fun-Policy'); await click('button[data-test-save-policy]'); assert.dom('.flash-message.alert-success').exists(); assert.equal( currentURL(), - '/access-control/policies/My-Fun-Policy', + '/administration/policies/My-Fun-Policy', 'redirected to the now-created policy' ); - await visit('/access-control/policies'); + await visit('/administration/policies'); const newPolicy = [...findAll('[data-test-policy-name]')].filter((a) => a.textContent.includes('My-Fun-Policy') )[0]; assert.ok(newPolicy, 'Policy is in the list'); await click(newPolicy); - assert.equal(currentURL(), '/access-control/policies/My-Fun-Policy'); + assert.equal(currentURL(), '/administration/policies/My-Fun-Policy'); await percySnapshot(assert); // Reset Token window.localStorage.nomadTokenSecret = null; @@ -133,7 +133,7 @@ module('Acceptance | policies', function (hooks) { test('Deleting a policy', async function (assert) { allScenarios.policiesTestCluster(server); window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId; - await visit('/access-control/policies'); + await visit('/administration/policies'); let firstPolicy = server.db.policies.sort((a, b) => { return a.name.localeCompare(b.name); })[0]; @@ -143,7 +143,7 @@ module('Acceptance | policies', function (hooks) { (row) => row.textContent.includes(firstPolicyName) )[0]; await click(firstPolicyLink); - assert.equal(currentURL(), `/access-control/policies/${firstPolicyName}`); + assert.equal(currentURL(), `/administration/policies/${firstPolicyName}`); const deleteButton = find('[data-test-delete-policy] button'); assert.dom(deleteButton).exists('delete button is present'); @@ -154,7 +154,7 @@ module('Acceptance | policies', function (hooks) { await click(find('[data-test-confirm-button]')); assert.dom('.flash-message.alert-success').exists(); - assert.equal(currentURL(), '/access-control/policies'); + assert.equal(currentURL(), '/administration/policies'); assert.dom(`[data-test-policy-name="${firstPolicyName}"]`).doesNotExist(); // Reset Token window.localStorage.nomadTokenSecret = null; @@ -163,7 +163,7 @@ module('Acceptance | policies', function (hooks) { test('Policies Index', async function (assert) { allScenarios.policiesTestCluster(server); window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId; - await visit('/access-control/policies'); + await visit('/administration/policies'); // Table contains every policy in db assert .dom('[data-test-policy-row]') diff --git a/ui/tests/acceptance/roles-test.js b/ui/tests/acceptance/roles-test.js index e08e4a29f34..abe6f784a80 100644 --- a/ui/tests/acceptance/roles-test.js +++ b/ui/tests/acceptance/roles-test.js @@ -10,7 +10,7 @@ import { setupMirage } from 'ember-cli-mirage/test-support'; import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit'; import { allScenarios } from '../../mirage/scenarios/default'; import Tokens from 'nomad-ui/tests/pages/settings/tokens'; -import AccessControl from 'nomad-ui/tests/pages/access-control'; +import Administration from 'nomad-ui/tests/pages/administration'; import percySnapshot from '@percy/ember'; module('Acceptance | roles', function (hooks) { @@ -27,7 +27,7 @@ module('Acceptance | roles', function (hooks) { ); const { secretId } = managementToken; await Tokens.secret(secretId).submit(); - await AccessControl.visitRoles(); + await Administration.visitRoles(); }); hooks.afterEach(async function () { @@ -39,7 +39,7 @@ module('Acceptance | roles', function (hooks) { assert.expect(3); await a11yAudit(assert); - assert.equal(currentURL(), '/access-control/roles'); + assert.equal(currentURL(), '/administration/roles'); assert .dom('[data-test-role-row]') @@ -78,7 +78,7 @@ module('Acceptance | roles', function (hooks) { assert.equal(policiesCellTags[1].textContent.trim(), 'job-reader'); await click(policiesCellTags[0].querySelector('a')); - assert.equal(currentURL(), '/access-control/policies/client-reader'); + assert.equal(currentURL(), '/administration/policies/client-reader'); assert.dom('[data-test-title]').containsText('client-reader'); }); @@ -86,7 +86,7 @@ module('Acceptance | roles', function (hooks) { assert.expect(8); const role = server.db.roles.findBy((r) => r.name === 'reader'); await click('[data-test-role-name="reader"] a'); - assert.equal(currentURL(), `/access-control/roles/${role.id}`); + assert.equal(currentURL(), `/administration/roles/${role.id}`); assert.dom('[data-test-role-name-input]').hasValue(role.name); assert.dom('[data-test-role-description-input]').hasValue(role.description); @@ -99,13 +99,13 @@ module('Acceptance | roles', function (hooks) { assert.dom('.flash-message.alert-success').exists(); assert.equal( currentURL(), - `/access-control/roles/${role.name}`, + `/administration/roles/${role.name}`, 'remain on page after save' ); await percySnapshot(assert); // Go back to the roles index - await AccessControl.visitRoles(); + await Administration.visitRoles(); let readerRoleRow = find('[data-test-role-row="reader-edited"]'); assert.dom(readerRoleRow).exists(); assert.equal( @@ -119,7 +119,7 @@ module('Acceptance | roles', function (hooks) { test('Edit Role: Policies', async function (assert) { const role = server.db.roles.findBy((r) => r.name === 'reader'); await click('[data-test-role-name="reader"] a'); - assert.equal(currentURL(), `/access-control/roles/${role.id}`); + assert.equal(currentURL(), `/administration/roles/${role.id}`); // Policies table is sortable @@ -202,7 +202,7 @@ module('Acceptance | roles', function (hooks) { await click('button[data-test-save-role]'); assert.dom('.flash-message.alert-success').exists(); - await AccessControl.visitRoles(); + await Administration.visitRoles(); const readerRoleRow = find('[data-test-role-row="reader"]'); const readerRolePolicies = readerRoleRow .querySelector('[data-test-role-policies]') @@ -219,7 +219,7 @@ module('Acceptance | roles', function (hooks) { const role = server.db.roles.findBy((r) => r.name === 'reader'); await click('[data-test-role-name="reader"] a'); - assert.equal(currentURL(), `/access-control/roles/${role.id}`); + assert.equal(currentURL(), `/administration/roles/${role.id}`); assert.dom('table.tokens').exists(); // "Reader" role has a single token with it applied by default @@ -243,7 +243,7 @@ module('Acceptance | roles', function (hooks) { await percySnapshot(assert); - await AccessControl.visitTokens(); + await Administration.visitTokens(); assert .dom('[data-test-token-name="Example Token for reader"]') .exists( @@ -254,7 +254,7 @@ module('Acceptance | roles', function (hooks) { test('Edit Role: Deletion', async function (assert) { const role = server.db.roles.findBy((r) => r.name === 'reader'); await click('[data-test-role-name="reader"] a'); - assert.equal(currentURL(), `/access-control/roles/${role.id}`); + assert.equal(currentURL(), `/administration/roles/${role.id}`); const deleteButton = find('[data-test-delete-role] button'); assert.dom(deleteButton).exists('delete button is present'); await click(deleteButton); @@ -263,12 +263,12 @@ module('Acceptance | roles', function (hooks) { .exists('confirmation message is present'); await click(find('[data-test-confirm-button]')); assert.dom('.flash-message.alert-success').exists(); - assert.equal(currentURL(), '/access-control/roles'); + assert.equal(currentURL(), '/administration/roles'); assert.dom('[data-test-role-row="reader"]').doesNotExist(); }); test('New Role', async function (assert) { await click('[data-test-create-role]'); - assert.equal(currentURL(), '/access-control/roles/new'); + assert.equal(currentURL(), '/administration/roles/new'); await fillIn('[data-test-role-name-input]', 'test-role'); await click('button[data-test-save-role]'); assert @@ -279,19 +279,19 @@ module('Acceptance | roles', function (hooks) { await click('[data-test-role-policies] tbody tr input'); await click('button[data-test-save-role]'); assert.dom('.flash-message.alert-success').exists(); - assert.equal(currentURL(), '/access-control/roles/1'); // default id created via mirage - await AccessControl.visitRoles(); + assert.equal(currentURL(), '/administration/roles/1'); // default id created via mirage + await Administration.visitRoles(); assert.dom('[data-test-role-row="test-role"]').exists(); // Now, try deleting all policies then doing this again. There'll be a warning on the roles/new page. - await AccessControl.visitPolicies(); + await Administration.visitPolicies(); const policyRows = findAll('[data-test-policy-row]'); for (const row of policyRows) { const deleteButton = row.querySelector('[data-test-delete-policy]'); await click(deleteButton); } assert.dom('[data-test-empty-policies-list-headline]').exists(); - await AccessControl.visitRoles(); + await Administration.visitRoles(); await click('[data-test-create-role]'); assert.dom('.empty-message').exists(); assert diff --git a/ui/tests/acceptance/token-test.js b/ui/tests/acceptance/token-test.js index 258a783f9be..a167232938c 100644 --- a/ui/tests/acceptance/token-test.js +++ b/ui/tests/acceptance/token-test.js @@ -21,7 +21,7 @@ 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'; -import AccessControl from 'nomad-ui/tests/pages/access-control'; +import Administration from 'nomad-ui/tests/pages/administration'; import percySnapshot from '@percy/ember'; import faker from 'nomad-ui/mirage/faker'; import moment from 'moment'; @@ -621,7 +621,7 @@ module('Acceptance | tokens', function (hooks) { }); window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId; - await visit('/access-control/policies'); + await visit('/administration/policies'); assert.dom('[data-test-policy-total-tokens]').exists(); const expectedFirstPolicyTokens = server.db.tokens.filter((token) => { return token.policyIds.includes(firstPolicy.name); @@ -648,9 +648,9 @@ module('Acceptance | tokens', function (hooks) { }); window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId; - await visit('/access-control/policies'); + await visit('/administration/policies'); await click('[data-test-policy-name]'); - assert.equal(currentURL(), `/access-control/policies/${firstPolicy.name}`); + assert.equal(currentURL(), `/administration/policies/${firstPolicy.name}`); const expectedFirstPolicyTokens = server.db.tokens.filter((token) => { return token.policyIds.includes(firstPolicy.name); @@ -692,10 +692,10 @@ module('Acceptance | tokens', function (hooks) { }); window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId; - await visit('/access-control/policies'); + await visit('/administration/policies'); await click('[data-test-policy-name]:first-child'); - assert.equal(currentURL(), `/access-control/policies/${testPolicy.name}`); + assert.equal(currentURL(), `/administration/policies/${testPolicy.name}`); assert .dom('[data-test-policy-token-row]') .exists( @@ -730,10 +730,10 @@ module('Acceptance | tokens', function (hooks) { ); window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId; - await visit('/access-control/policies'); + await visit('/administration/policies'); await click('[data-test-policy-name]'); - assert.equal(currentURL(), `/access-control/policies/${testPolicy.name}`); + assert.equal(currentURL(), `/administration/policies/${testPolicy.name}`); assert .dom('[data-test-policy-token-row]') @@ -885,7 +885,7 @@ module('Acceptance | tokens', function (hooks) { ); const { secretId } = managementToken; await Tokens.secret(secretId).submit(); - await AccessControl.visitTokens(); + await Administration.visitTokens(); }); hooks.afterEach(async function () { @@ -894,7 +894,7 @@ module('Acceptance | tokens', function (hooks) { }); test('Tokens index, general', async function (assert) { - assert.equal(currentURL(), '/access-control/tokens'); + assert.equal(currentURL(), '/administration/tokens'); // Number of token rows equivalent to number in db assert .dom('[data-test-token-row]') @@ -1002,7 +1002,7 @@ module('Acceptance | tokens', function (hooks) { (row) => row.textContent.includes(tokenToClick.name) ); await click(tokenRowToClick.querySelector('[data-test-token-name] a')); - assert.equal(currentURL(), `/access-control/tokens/${tokenToClick.id}`); + assert.equal(currentURL(), `/administration/tokens/${tokenToClick.id}`); assert.dom('[data-test-token-name-input]').hasValue(tokenToClick.name); }); @@ -1059,7 +1059,7 @@ module('Acceptance | tokens', function (hooks) { test('Token page, general', async function (assert) { const token = server.db.tokens.findBy((t) => t.id === 'cl4y-t0k3n'); - await visit(`/access-control/tokens/${token.id}`); + await visit(`/administration/tokens/${token.id}`); assert.dom('[data-test-token-name-input]').hasValue(token.name); assert.dom('[data-test-token-accessor]').hasValue(token.accessorId); assert.dom('[data-test-token-secret]').hasValue(token.secretId); @@ -1136,18 +1136,18 @@ module('Acceptance | tokens', function (hooks) { }); test('Token name can be edited', async function (assert) { const token = server.db.tokens.findBy((t) => t.id === 'cl4y-t0k3n'); - await visit(`/access-control/tokens/${token.id}`); + await visit(`/administration/tokens/${token.id}`); assert.dom('[data-test-token-name-input]').hasValue(token.name); await fillIn('[data-test-token-name-input]', 'Mud-Token'); await click('[data-test-token-save]'); assert.dom('.flash-message.alert-success').exists(); - await AccessControl.visitTokens(); + await Administration.visitTokens(); assert.dom('[data-test-token-name="Mud-Token"]').exists({ count: 1 }); }); test('Token policies and roles can be edited', async function (assert) { const token = server.db.tokens.findBy((t) => t.id === 'cl4y-t0k3n'); - await visit(`/access-control/tokens/${token.id}`); + await visit(`/administration/tokens/${token.id}`); // The policies/roles belonging to this token are checked const tokenPolicies = token.policyIds; @@ -1199,7 +1199,7 @@ module('Acceptance | tokens', function (hooks) { await percySnapshot(assert); - await AccessControl.visitTokens(); + await Administration.visitTokens(); // Policies cell for our clay token should read "No Policies" const clayToken = server.db.tokens.findBy((t) => t.id === 'cl4y-t0k3n'); const clayTokenRow = [...findAll('[data-test-token-row]')].find((row) => @@ -1220,7 +1220,7 @@ module('Acceptance | tokens', function (hooks) { }); test('Token can be deleted', async function (assert) { const token = server.db.tokens.findBy((t) => t.id === 'cl4y-t0k3n'); - await visit(`/access-control/tokens/${token.id}`); + await visit(`/administration/tokens/${token.id}`); const deleteButton = find('[data-test-delete-token] button'); assert.dom(deleteButton).exists('delete button is present'); @@ -1231,16 +1231,16 @@ module('Acceptance | tokens', function (hooks) { await click(find('[data-test-confirm-button]')); assert.dom('.flash-message.alert-success').exists(); - await AccessControl.visitTokens(); + await Administration.visitTokens(); assert.dom('[data-test-token-name="cl4y-t0k3n"]').doesNotExist(); }); test('New Token creation', async function (assert) { await click('[data-test-create-token]'); - assert.equal(currentURL(), '/access-control/tokens/new'); + assert.equal(currentURL(), '/administration/tokens/new'); await fillIn('[data-test-token-name-input]', 'Timeless Token'); await click('[data-test-token-save]'); assert.dom('.flash-message.alert-success').exists(); - await AccessControl.visitTokens(); + await Administration.visitTokens(); assert .dom('[data-test-token-name="Timeless Token"]') .exists({ count: 1 }); @@ -1254,13 +1254,13 @@ module('Acceptance | tokens', function (hooks) { // Now create one with a TTL await click('[data-test-create-token]'); - assert.equal(currentURL(), '/access-control/tokens/new'); + assert.equal(currentURL(), '/administration/tokens/new'); await fillIn('[data-test-token-name-input]', 'TTL Token'); // Select the "8 hours" radio within the .expiration-time div await click('.expiration-time input[value="8h"]'); await click('[data-test-token-save]'); assert.dom('.flash-message.alert-success').exists(); - await AccessControl.visitTokens(); + await Administration.visitTokens(); assert.dom('[data-test-token-name="TTL Token"]').exists({ count: 1 }); const ttlTokenRow = [...findAll('[data-test-token-row]')].find((row) => row.textContent.includes('TTL Token') @@ -1272,7 +1272,7 @@ module('Acceptance | tokens', function (hooks) { // Now create one with an expiration time await click('[data-test-create-token]'); - assert.equal(currentURL(), '/access-control/tokens/new'); + assert.equal(currentURL(), '/administration/tokens/new'); await fillIn('[data-test-token-name-input]', 'Expiring Token'); // select the Custom radio button await click('.expiration-time input[value="custom"]'); @@ -1288,7 +1288,7 @@ module('Acceptance | tokens', function (hooks) { await fillIn('[data-test-token-expiration-time-input]', soonString); await click('[data-test-token-save]'); assert.dom('.flash-message.alert-success').exists(); - await AccessControl.visitTokens(); + await Administration.visitTokens(); assert .dom('[data-test-token-name="Expiring Token"]') .exists({ count: 1 }); diff --git a/ui/tests/pages/access-control.js b/ui/tests/pages/access-control.js deleted file mode 100644 index 9db1bd3cd9c..00000000000 --- a/ui/tests/pages/access-control.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { create, visitable } from 'ember-cli-page-object'; - -export default create({ - visit: visitable('/access-control'), - visitTokens: visitable('/access-control/tokens'), - visitPolicies: visitable('/access-control/policies'), - visitRoles: visitable('/access-control/roles'), - visitNamespaces: visitable('/access-control/namespaces'), -}); diff --git a/ui/tests/pages/administration.js b/ui/tests/pages/administration.js new file mode 100644 index 00000000000..cc382561b1b --- /dev/null +++ b/ui/tests/pages/administration.js @@ -0,0 +1,14 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { create, visitable } from 'ember-cli-page-object'; + +export default create({ + visit: visitable('/administration'), + visitTokens: visitable('/administration/tokens'), + visitPolicies: visitable('/administration/policies'), + visitRoles: visitable('/administration/roles'), + visitNamespaces: visitable('/administration/namespaces'), +});