mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
[ui] Sentinel Policies CRUD UI (#20483)
* Gallery allows picking stuff * Small fixes * added sentinel templates * Can set enforcement level on policies * Working on the interactive sentinel dev mode * Very rough development flow on FE * Changed position in gutter menu * More sentinel stuff * PR cleanup: removed testmode, removed unneeded mixins and deps * Heliosification * Index-level sentinel policy deletion and page title fixes * Makes the Canaries sentinel policy real and then comments out the unfinished ones * rename Access Control to Administration in prep for moving Sentinel Policies and Node Pool admin there * Sentinel policies moved within the Administration section * Mirage fixture for sentinel policy endpoints * Description length check and 500 prevention * Sync review PR feedback addressed, implied butons on radio cards * Cull un-used sentinel policies --------- Co-authored-by: Mike Nomitch <mail@mikenomitch.com>
This commit is contained in:
3
.changelog/20483.txt
Normal file
3
.changelog/20483.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
```release-note:improvement
|
||||||
|
ui: Added a UI for creating, editing and deleting Sentinel Policies
|
||||||
|
```
|
||||||
27
ui/app/abilities/sentinel-policy.js
Normal file
27
ui/app/abilities/sentinel-policy.js
Normal file
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
25
ui/app/adapters/sentinel-policy.js
Normal file
25
ui/app/adapters/sentinel-policy.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,6 @@ import { tagName } from '@ember-decorators/component';
|
|||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
@tagName('')
|
@tagName('')
|
||||||
export default class AccessControlSubnav extends Component {
|
export default class AdministrationSubnav extends Component {
|
||||||
@service keyboard;
|
@service keyboard;
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,7 @@ export default class NamespaceEditorComponent extends Component {
|
|||||||
|
|
||||||
if (shouldRedirectAfterSave) {
|
if (shouldRedirectAfterSave) {
|
||||||
this.router.transitionTo(
|
this.router.transitionTo(
|
||||||
'access-control.namespaces.acl-namespace',
|
'administration.namespaces.acl-namespace',
|
||||||
this.namespace.name
|
this.namespace.name
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,12 +26,12 @@
|
|||||||
class="policy-editor"
|
class="policy-editor"
|
||||||
data-test-policy-editor
|
data-test-policy-editor
|
||||||
{{code-mirror
|
{{code-mirror
|
||||||
screenReaderLabel="Policy definition"
|
screenReaderLabel="Policy definition"
|
||||||
theme="hashi"
|
theme="hashi"
|
||||||
mode="ruby"
|
mode="ruby"
|
||||||
content=@policy.rules
|
content=@policy.rules
|
||||||
onUpdate=this.updatePolicyRules
|
onUpdate=this.updatePolicyRules
|
||||||
autofocus=false
|
autofocus=false
|
||||||
extraKeys=(hash Cmd-Enter=this.save)
|
extraKeys=(hash Cmd-Enter=this.save)
|
||||||
}} />
|
}} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export default class PolicyEditorComponent extends Component {
|
|||||||
|
|
||||||
if (shouldRedirectAfterSave) {
|
if (shouldRedirectAfterSave) {
|
||||||
this.router.transitionTo(
|
this.router.transitionTo(
|
||||||
'access-control.policies.policy',
|
'administration.policies.policy',
|
||||||
this.policy.id
|
this.policy.id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
<B.Td data-test-policy-name>{{B.data.name}}</B.Td>
|
<B.Td data-test-policy-name>{{B.data.name}}</B.Td>
|
||||||
<B.Td>{{B.data.description}}</B.Td>
|
<B.Td>{{B.data.description}}</B.Td>
|
||||||
<B.Td>
|
<B.Td>
|
||||||
<LinkTo @route="access-control.policies.policy" @model={{B.data.name}}>
|
<LinkTo @route="administration.policies.policy" @model={{B.data.name}}>
|
||||||
View Policy Definition
|
View Policy Definition
|
||||||
</LinkTo>
|
</LinkTo>
|
||||||
</B.Td>
|
</B.Td>
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export default class RoleEditorComponent extends Component {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (shouldRedirectAfterSave) {
|
if (shouldRedirectAfterSave) {
|
||||||
this.router.transitionTo('access-control.roles.role', this.role.id);
|
this.router.transitionTo('administration.roles.role', this.role.id);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
let message = err.errors?.length
|
let message = err.errors?.length
|
||||||
|
|||||||
90
ui/app/components/sentinel-policy-editor.hbs
Normal file
90
ui/app/components/sentinel-policy-editor.hbs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
{{!
|
||||||
|
Copyright (c) HashiCorp, Inc.
|
||||||
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
|
~}}
|
||||||
|
|
||||||
|
<form class="acl-form" autocomplete="off" {{on "submit" this.save}}>
|
||||||
|
{{#if @policy.isNew }}
|
||||||
|
<Hds::Form::TextInput::Field
|
||||||
|
@isRequired={{true}}
|
||||||
|
data-test-policy-name-input
|
||||||
|
@value={{@policy.name}}
|
||||||
|
{{on "input" this.updatePolicyName}}
|
||||||
|
{{autofocus}}
|
||||||
|
as |F|>
|
||||||
|
<F.Label>Policy Name</F.Label>
|
||||||
|
</Hds::Form::TextInput::Field>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<div class="boxed-section">
|
||||||
|
<div class="boxed-section-head">
|
||||||
|
Policy Definition
|
||||||
|
</div>
|
||||||
|
<div class="boxed-section-body is-full-bleed">
|
||||||
|
<div
|
||||||
|
class="policy-editor"
|
||||||
|
data-test-policy-editor
|
||||||
|
{{code-mirror
|
||||||
|
screenReaderLabel="Policy definition"
|
||||||
|
theme="hashi"
|
||||||
|
mode="ruby"
|
||||||
|
content=@policy.policy
|
||||||
|
onUpdate=this.updatePolicy
|
||||||
|
autofocus=false
|
||||||
|
extraKeys=(hash Cmd-Enter=this.save)
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
<span>
|
||||||
|
Description (optional)
|
||||||
|
</span>
|
||||||
|
<Input
|
||||||
|
data-test-policy-description
|
||||||
|
@value={{@policy.description}}
|
||||||
|
class="input"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Hds::Form::Radio::Group @layout="horizontal" @name="method-demo1" {{on "change" this.updatePolicyEnforcementLevel}} as |G|>
|
||||||
|
<G.Legend>Enforcement Level</G.Legend>
|
||||||
|
<G.HelperText>See <Hds::Link::Inline @href="https://developer.hashicorp.com/nomad/tutorials/access-control/access-control-tokens#token-types">Sentinel Policy documentation</Hds::Link::Inline> for more information.</G.HelperText>
|
||||||
|
<G.Radio::Field
|
||||||
|
@id="advisory"
|
||||||
|
checked={{eq @policy.enforcementLevel "advisory"}}
|
||||||
|
data-test-token-type="client"
|
||||||
|
as |F|>
|
||||||
|
<F.Label>Advisory</F.Label>
|
||||||
|
</G.Radio::Field>
|
||||||
|
<G.Radio::Field
|
||||||
|
@id="soft-mandatory"
|
||||||
|
checked={{eq @policy.enforcementLevel "soft-mandatory"}}
|
||||||
|
data-test-token-type="soft-mandatory"
|
||||||
|
as |F|>
|
||||||
|
<F.Label>Soft Mandatory</F.Label>
|
||||||
|
</G.Radio::Field>
|
||||||
|
<G.Radio::Field
|
||||||
|
@id="hard-mandatory"
|
||||||
|
checked={{eq @policy.enforcementLevel "hard-mandatory"}}
|
||||||
|
data-test-token-type="hard-mandatory"
|
||||||
|
as |F|>
|
||||||
|
<F.Label>Hard Mandatory</F.Label>
|
||||||
|
</G.Radio::Field>
|
||||||
|
</Hds::Form::Radio::Group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
{{#if (can "update sentinel-policy")}}
|
||||||
|
<Hds::Button
|
||||||
|
@text="Save Policy"
|
||||||
|
@type="submit"
|
||||||
|
data-test-save-policy
|
||||||
|
{{on "click" this.save}}
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
92
ui/app/components/sentinel-policy-editor.js
Normal file
92
ui/app/components/sentinel-policy-editor.js
Normal file
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
<B.Td data-test-policy-name>{{B.data.name}}</B.Td>
|
<B.Td data-test-policy-name>{{B.data.name}}</B.Td>
|
||||||
<B.Td>{{B.data.description}}</B.Td>
|
<B.Td>{{B.data.description}}</B.Td>
|
||||||
<B.Td>
|
<B.Td>
|
||||||
<LinkTo @route="access-control.policies.policy" @model={{B.data.name}}>
|
<LinkTo @route="administration.policies.policy" @model={{B.data.name}}>
|
||||||
View Policy Definition
|
View Policy Definition
|
||||||
</LinkTo>
|
</LinkTo>
|
||||||
</B.Td>
|
</B.Td>
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
No Policies
|
No Policies
|
||||||
</h3>
|
</h3>
|
||||||
<p class="empty-message-body">
|
<p class="empty-message-body">
|
||||||
Get started by <LinkTo @route="access-control.policies.new">creating a new policy</LinkTo>
|
Get started by <LinkTo @route="administration.policies.new">creating a new policy</LinkTo>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
@@ -196,7 +196,7 @@
|
|||||||
<div class="tag-group">
|
<div class="tag-group">
|
||||||
{{#each B.data.policies as |policy|}}
|
{{#each B.data.policies as |policy|}}
|
||||||
{{#if policy.name}}
|
{{#if policy.name}}
|
||||||
<Hds::Tag @color="primary" @text="{{policy.name}}" @route="access-control.policies.policy" @model="{{policy.name}}" />
|
<Hds::Tag @color="primary" @text="{{policy.name}}" @route="administration.policies.policy" @model="{{policy.name}}" />
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{else}}
|
{{else}}
|
||||||
Role contains no policies
|
Role contains no policies
|
||||||
@@ -204,7 +204,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</B.Td>
|
</B.Td>
|
||||||
<B.Td>
|
<B.Td>
|
||||||
<LinkTo @route="access-control.roles.role" @model={{B.data.id}}>
|
<LinkTo @route="administration.roles.role" @model={{B.data.id}}>
|
||||||
View Role Info
|
View Role Info
|
||||||
</LinkTo>
|
</LinkTo>
|
||||||
</B.Td>
|
</B.Td>
|
||||||
@@ -217,7 +217,7 @@
|
|||||||
No Roles
|
No Roles
|
||||||
</h3>
|
</h3>
|
||||||
<p class="empty-message-body">
|
<p class="empty-message-body">
|
||||||
Get started by <LinkTo @route="access-control.roles.new">creating a new role</LinkTo>
|
Get started by <LinkTo @route="administration.roles.new">creating a new role</LinkTo>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export default class TokenEditorComponent extends Component {
|
|||||||
|
|
||||||
if (shouldRedirectAfterSave) {
|
if (shouldRedirectAfterSave) {
|
||||||
this.router.transitionTo(
|
this.router.transitionTo(
|
||||||
'access-control.tokens.token',
|
'administration.tokens.token',
|
||||||
this.activeToken.id
|
this.activeToken.id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,6 @@
|
|||||||
job <Hds::Link::Inline @route="jobs.job" @model={{concat @job "@" @namespace}} @icon="external-link">{{@job}}</Hds::Link::Inline>
|
job <Hds::Link::Inline @route="jobs.job" @model={{concat @job "@" @namespace}} @icon="external-link">{{@job}}</Hds::Link::Inline>
|
||||||
{{else}}
|
{{else}}
|
||||||
all nomad jobs in this namespace
|
all nomad jobs in this namespace
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</A.Description>
|
</A.Description>
|
||||||
</Hds::Alert>
|
</Hds::Alert>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default class AccessControlNamespacesAclNamespaceController extends Contr
|
|||||||
type: `success`,
|
type: `success`,
|
||||||
destroyOnClick: false,
|
destroyOnClick: false,
|
||||||
});
|
});
|
||||||
this.router.transitionTo('access-control.namespaces');
|
this.router.transitionTo('administration.namespaces');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// A failed delete resulted in errors when you then navigated away and back
|
// A failed delete resulted in errors when you then navigated away and back
|
||||||
// to the show page rollbackWithoutChangedAttrs fixes it, but there might
|
// to the show page rollbackWithoutChangedAttrs fixes it, but there might
|
||||||
@@ -14,13 +14,13 @@ export default class AccessControlNamespacesIndexController extends Controller {
|
|||||||
|
|
||||||
@action openNamespace(namespace) {
|
@action openNamespace(namespace) {
|
||||||
this.router.transitionTo(
|
this.router.transitionTo(
|
||||||
'access-control.namespaces.acl-namespace',
|
'administration.namespaces.acl-namespace',
|
||||||
namespace.name
|
namespace.name
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action goToNewNamespace() {
|
@action goToNewNamespace() {
|
||||||
this.router.transitionTo('access-control.namespaces.new');
|
this.router.transitionTo('administration.namespaces.new');
|
||||||
}
|
}
|
||||||
|
|
||||||
get columns() {
|
get columns() {
|
||||||
@@ -54,11 +54,11 @@ export default class AccessControlPoliciesIndexController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action openPolicy(policy) {
|
@action openPolicy(policy) {
|
||||||
this.router.transitionTo('access-control.policies.policy', policy.name);
|
this.router.transitionTo('administration.policies.policy', policy.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action goToNewPolicy() {
|
@action goToNewPolicy() {
|
||||||
this.router.transitionTo('access-control.policies.new');
|
this.router.transitionTo('administration.policies.new');
|
||||||
}
|
}
|
||||||
|
|
||||||
@task(function* (policy) {
|
@task(function* (policy) {
|
||||||
@@ -42,7 +42,7 @@ export default class AccessControlPoliciesPolicyController extends Controller {
|
|||||||
type: `success`,
|
type: `success`,
|
||||||
destroyOnClick: false,
|
destroyOnClick: false,
|
||||||
});
|
});
|
||||||
this.router.transitionTo('access-control.policies');
|
this.router.transitionTo('administration.policies');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.notifications.add({
|
this.notifications.add({
|
||||||
title: `Error deleting Policy ${this.policy.name}`,
|
title: `Error deleting Policy ${this.policy.name}`,
|
||||||
@@ -60,11 +60,11 @@ export default class AccessControlRolesIndexController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action openRole(role) {
|
@action openRole(role) {
|
||||||
this.router.transitionTo('access-control.roles.role', role.id);
|
this.router.transitionTo('administration.roles.role', role.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action goToNewRole() {
|
@action goToNewRole() {
|
||||||
this.router.transitionTo('access-control.roles.new');
|
this.router.transitionTo('administration.roles.new');
|
||||||
}
|
}
|
||||||
|
|
||||||
@task(function* (role) {
|
@task(function* (role) {
|
||||||
@@ -32,7 +32,7 @@ export default class AccessControlRolesRoleController extends Controller {
|
|||||||
type: `success`,
|
type: `success`,
|
||||||
destroyOnClick: false,
|
destroyOnClick: false,
|
||||||
});
|
});
|
||||||
this.router.transitionTo('access-control.roles');
|
this.router.transitionTo('administration.roles');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.notifications.add({
|
this.notifications.add({
|
||||||
title: `Error deleting Role ${this.role.name}`,
|
title: `Error deleting Role ${this.role.name}`,
|
||||||
8
ui/app/controllers/administration/sentinel-policies.js
Normal file
8
ui/app/controllers/administration/sentinel-policies.js
Normal file
@@ -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 {}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
78
ui/app/controllers/administration/sentinel-policies/index.js
Normal file
78
ui/app/controllers/administration/sentinel-policies/index.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
11
ui/app/controllers/administration/sentinel-policies/new.js
Normal file
11
ui/app/controllers/administration/sentinel-policies/new.js
Normal file
@@ -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'];
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -38,10 +38,10 @@ export default class AccessControlTokensIndexController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action openToken(token) {
|
@action openToken(token) {
|
||||||
this.router.transitionTo('access-control.tokens.token', token.id);
|
this.router.transitionTo('administration.tokens.token', token.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action goToNewToken() {
|
@action goToNewToken() {
|
||||||
this.router.transitionTo('access-control.tokens.new');
|
this.router.transitionTo('administration.tokens.new');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,7 +28,7 @@ export default class AccessControlTokensTokenController extends Controller {
|
|||||||
type: `success`,
|
type: `success`,
|
||||||
destroyOnClick: false,
|
destroyOnClick: false,
|
||||||
});
|
});
|
||||||
this.router.transitionTo('access-control.tokens');
|
this.router.transitionTo('administration.tokens');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.notifications.add({
|
this.notifications.add({
|
||||||
title: `Error deleting Token ${this.activeToken.name}`,
|
title: `Error deleting Token ${this.activeToken.name}`,
|
||||||
18
ui/app/models/sentinel-policy.js
Normal file
18
ui/app/models/sentinel-policy.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -112,7 +112,7 @@ Router.map(function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.route('access-control', function () {
|
this.route('administration', function () {
|
||||||
this.route('policies', function () {
|
this.route('policies', function () {
|
||||||
this.route('new');
|
this.route('new');
|
||||||
this.route('policy', {
|
this.route('policy', {
|
||||||
@@ -139,6 +139,11 @@ Router.map(function () {
|
|||||||
path: '/:name',
|
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
|
// Mirage-only route for testing OIDC flow
|
||||||
if (config['ember-cli-mirage']) {
|
if (config['ember-cli-mirage']) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import WithModelErrorHandling from 'nomad-ui/mixins/with-model-error-handling';
|
|||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import RSVP from 'rsvp';
|
import RSVP from 'rsvp';
|
||||||
|
|
||||||
export default class AccessControlRoute extends Route.extend(
|
export default class AdministrationRoute extends Route.extend(
|
||||||
withForbiddenState,
|
withForbiddenState,
|
||||||
WithModelErrorHandling
|
WithModelErrorHandling
|
||||||
) {
|
) {
|
||||||
@@ -35,6 +35,9 @@ export default class AccessControlRoute extends Route.extend(
|
|||||||
roles: this.store.findAll('role', { reload: true }),
|
roles: this.store.findAll('role', { reload: true }),
|
||||||
tokens: this.store.findAll('token', { reload: true }),
|
tokens: this.store.findAll('token', { reload: true }),
|
||||||
namespaces: this.store.findAll('namespace', { reload: true }),
|
namespaces: this.store.findAll('namespace', { reload: true }),
|
||||||
|
sentinelPolicies: this.can.can('list sentinel-policy')
|
||||||
|
? this.store.findAll('sentinel-policy', { reload: true })
|
||||||
|
: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ export default class AccessControlNamespacesNewRoute extends Route {
|
|||||||
|
|
||||||
beforeModel() {
|
beforeModel() {
|
||||||
if (this.can.cannot('write namespace')) {
|
if (this.can.cannot('write namespace')) {
|
||||||
this.router.transitionTo('/access-control/namespaces');
|
this.router.transitionTo('/administration/namespaces');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ export default class AccessControlPoliciesNewRoute extends Route {
|
|||||||
|
|
||||||
beforeModel() {
|
beforeModel() {
|
||||||
if (this.can.cannot('write policy')) {
|
if (this.can.cannot('write policy')) {
|
||||||
this.router.transitionTo('/access-control/policies');
|
this.router.transitionTo('/administration/policies');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ export default class AccessControlRolesNewRoute extends Route {
|
|||||||
|
|
||||||
beforeModel() {
|
beforeModel() {
|
||||||
if (this.can.cannot('write role')) {
|
if (this.can.cannot('write role')) {
|
||||||
this.router.transitionTo('/access-control/roles');
|
this.router.transitionTo('/administration/roles');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
17
ui/app/routes/administration/sentinel-policies.js
Normal file
17
ui/app/routes/administration/sentinel-policies.js
Normal file
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
51
ui/app/routes/administration/sentinel-policies/new.js
Normal file
51
ui/app/routes/administration/sentinel-policies/new.js
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
ui/app/routes/administration/sentinel-policies/policy.js
Normal file
21
ui/app/routes/administration/sentinel-policies/policy.js
Normal file
@@ -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,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ export default class AccessControlTokensNewRoute extends Route {
|
|||||||
|
|
||||||
beforeModel() {
|
beforeModel() {
|
||||||
if (this.can.cannot('write token')) {
|
if (this.can.cannot('write token')) {
|
||||||
this.router.transitionTo('/access-control/tokens');
|
this.router.transitionTo('/administration/tokens');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,9 +19,9 @@ export default class AccessControlTokensTokenRoute extends Route.extend(
|
|||||||
|
|
||||||
// Route guard to prevent you from wrecking your current token
|
// Route guard to prevent you from wrecking your current token
|
||||||
beforeModel() {
|
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) {
|
if (this.token.selfToken && this.token.selfToken.id === id) {
|
||||||
this.transitionTo('/access-control/tokens');
|
this.transitionTo('/administration/tokens');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
12
ui/app/serializers/sentinel-policy.js
Normal file
12
ui/app/serializers/sentinel-policy.js
Normal file
@@ -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';
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
.section-cards {
|
.section-cards {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
& > div {
|
& > div {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
@@ -83,7 +83,6 @@
|
|||||||
|
|
||||||
.policy-editor {
|
.policy-editor {
|
||||||
max-height: 600px;
|
max-height: 600px;
|
||||||
overflow: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.namespace-editor-wrapper {
|
.namespace-editor-wrapper {
|
||||||
|
|||||||
@@ -96,6 +96,16 @@
|
|||||||
|
|
||||||
.radio-group {
|
.radio-group {
|
||||||
padding: 16px 0px;
|
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 {
|
.button-group {
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
{{!
|
|
||||||
Copyright (c) HashiCorp, Inc.
|
|
||||||
SPDX-License-Identifier: BUSL-1.1
|
|
||||||
~}}
|
|
||||||
|
|
||||||
{{page-title "Access Control"}}
|
|
||||||
|
|
||||||
<Breadcrumb @crumb={{hash label="Access Control" args=(array "access-control")}} />
|
|
||||||
<PageLayout>
|
|
||||||
<AccessControlSubnav @client={{this.model}} />
|
|
||||||
{{outlet}}
|
|
||||||
</PageLayout>
|
|
||||||
12
ui/app/templates/administration.hbs
Normal file
12
ui/app/templates/administration.hbs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{{!
|
||||||
|
Copyright (c) HashiCorp, Inc.
|
||||||
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
|
~}}
|
||||||
|
|
||||||
|
{{page-title "Administration"}}
|
||||||
|
|
||||||
|
<Breadcrumb @crumb={{hash label="Administration" args=(array "administration")}} />
|
||||||
|
<PageLayout>
|
||||||
|
<AdministrationSubnav @client={{this.model}} />
|
||||||
|
{{outlet}}
|
||||||
|
</PageLayout>
|
||||||
@@ -14,40 +14,51 @@
|
|||||||
<div class="section-cards">
|
<div class="section-cards">
|
||||||
<Hds::Card::Container @level="mid" @hasBorder={{true}} data-test-tokens-card>
|
<Hds::Card::Container @level="mid" @hasBorder={{true}} data-test-tokens-card>
|
||||||
<LinkTo
|
<LinkTo
|
||||||
@route="access-control.tokens"
|
@route="administration.tokens"
|
||||||
>
|
>
|
||||||
{{this.model.tokens.length}} {{pluralize "Token" this.model.tokens.length}}
|
{{this.model.tokens.length}} {{pluralize "Token" this.model.tokens.length}}
|
||||||
</LinkTo>
|
</LinkTo>
|
||||||
<p>User access tokens are associated with one or more policies or roles to grant specific capabilities.</p>
|
<p>User access tokens are associated with one or more policies or roles to grant specific capabilities.</p>
|
||||||
<Hds::Button @text="Create Token" @color="secondary" @iconPosition="leading" @icon="plus" @route="access-control.tokens.new" />
|
<Hds::Button @text="Create Token" @color="secondary" @iconPosition="leading" @icon="plus" @route="administration.tokens.new" />
|
||||||
</Hds::Card::Container>
|
</Hds::Card::Container>
|
||||||
<Hds::Card::Container @level="mid" @hasBorder={{true}} data-test-roles-card>
|
<Hds::Card::Container @level="mid" @hasBorder={{true}} data-test-roles-card>
|
||||||
<LinkTo
|
<LinkTo
|
||||||
@route="access-control.roles"
|
@route="administration.roles"
|
||||||
>
|
>
|
||||||
{{this.model.roles.length}} {{pluralize "Role" this.model.roles.length}}
|
{{this.model.roles.length}} {{pluralize "Role" this.model.roles.length}}
|
||||||
</LinkTo>
|
</LinkTo>
|
||||||
<p>Roles group one or more Policies into higher-level sets of permissions.</p>
|
<p>Roles group one or more Policies into higher-level sets of permissions.</p>
|
||||||
<Hds::Button @text="Create Role" @color="secondary" @iconPosition="leading" @icon="plus" @route="access-control.roles.new" />
|
<Hds::Button @text="Create Role" @color="secondary" @iconPosition="leading" @icon="plus" @route="administration.roles.new" />
|
||||||
</Hds::Card::Container>
|
</Hds::Card::Container>
|
||||||
<Hds::Card::Container @level="mid" @hasBorder={{true}} data-test-policies-card>
|
<Hds::Card::Container @level="mid" @hasBorder={{true}} data-test-policies-card>
|
||||||
<LinkTo
|
<LinkTo
|
||||||
@route="access-control.policies"
|
@route="administration.policies"
|
||||||
>
|
>
|
||||||
{{this.model.policies.length}} {{pluralize "Policy" this.model.policies.length}}
|
{{this.model.policies.length}} {{pluralize "Policy" this.model.policies.length}}
|
||||||
</LinkTo>
|
</LinkTo>
|
||||||
<p>Sets of rules defining the capabilities granted to adhering tokens.</p>
|
<p>Sets of rules defining the capabilities granted to adhering tokens.</p>
|
||||||
<Hds::Button @text="Create Policy" @color="secondary" @iconPosition="leading" @icon="plus" @route="access-control.policies.new" />
|
<Hds::Button @text="Create Policy" @color="secondary" @iconPosition="leading" @icon="plus" @route="administration.policies.new" />
|
||||||
</Hds::Card::Container>
|
</Hds::Card::Container>
|
||||||
<Hds::Card::Container @level="mid" @hasBorder={{true}} data-test-namespaces-card>
|
<Hds::Card::Container @level="mid" @hasBorder={{true}} data-test-namespaces-card>
|
||||||
<LinkTo
|
<LinkTo
|
||||||
@route="access-control.namespaces"
|
@route="administration.namespaces"
|
||||||
>
|
>
|
||||||
{{this.model.namespaces.length}} {{pluralize "Namespace" this.model.namespaces.length}}
|
{{this.model.namespaces.length}} {{pluralize "Namespace" this.model.namespaces.length}}
|
||||||
</LinkTo>
|
</LinkTo>
|
||||||
<p>Namespaces allow jobs and other objects to be segmented from each other.</p>
|
<p>Namespaces allow jobs and other objects to be segmented from each other.</p>
|
||||||
<Hds::Button @text="Create Namespace" @color="secondary" @iconPosition="leading" @icon="plus" @route="access-control.namespaces.new" />
|
<Hds::Button @text="Create Namespace" @color="secondary" @iconPosition="leading" @icon="plus" @route="administration.namespaces.new" />
|
||||||
</Hds::Card::Container>
|
</Hds::Card::Container>
|
||||||
|
{{#if (can "read sentinel-policy")}}
|
||||||
|
<Hds::Card::Container @level="mid" @hasBorder={{true}} data-test-sentinel-policies-card>
|
||||||
|
<LinkTo
|
||||||
|
@route="administration.sentinel-policies"
|
||||||
|
>
|
||||||
|
{{this.model.sentinelPolicies.length}} {{pluralize "Sentinel Policy" this.model.sentinelPolicies.length}}
|
||||||
|
</LinkTo>
|
||||||
|
<p>Sentinel Policies allow operators to express rules as code and have those rules automatically enforced when jobs are planned.</p>
|
||||||
|
<Hds::Button @text="Create Sentinel Policy" @color="secondary" @iconPosition="leading" @icon="plus" @route="administration.sentinel-policies.new" />
|
||||||
|
</Hds::Card::Container>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{{outlet}}
|
{{outlet}}
|
||||||
@@ -4,5 +4,5 @@
|
|||||||
~}}
|
~}}
|
||||||
|
|
||||||
{{page-title "Namespaces"}}
|
{{page-title "Namespaces"}}
|
||||||
<Breadcrumb @crumb={{hash label="Namespaces" args=(array "access-control.namespaces")}} />
|
<Breadcrumb @crumb={{hash label="Namespaces" args=(array "administration.namespaces")}} />
|
||||||
{{outlet}}
|
{{outlet}}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
Copyright (c) HashiCorp, Inc.
|
Copyright (c) HashiCorp, Inc.
|
||||||
SPDX-License-Identifier: BUSL-1.1
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
~}}
|
~}}
|
||||||
<Breadcrumb @crumb={{hash label=this.model.name args=(array "access-control.namespaces.acl-namespace" this.model.name)}} />
|
<Breadcrumb @crumb={{hash label=this.model.name args=(array "administration.namespaces.acl-namespace" this.model.name)}} />
|
||||||
{{page-title "Namespace"}}
|
{{page-title "Namespace"}}
|
||||||
|
|
||||||
<section class="section">
|
<section class="section">
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<Hds::Button
|
<Hds::Button
|
||||||
@text="Create Namespace"
|
@text="Create Namespace"
|
||||||
@icon="plus"
|
@icon="plus"
|
||||||
@route="access-control.namespaces.new"
|
@route="administration.namespaces.new"
|
||||||
{{keyboard-shortcut
|
{{keyboard-shortcut
|
||||||
pattern=(array "n" "n")
|
pattern=(array "n" "n")
|
||||||
action=(action this.goToNewNamespace)
|
action=(action this.goToNewNamespace)
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
data-test-namespace-row
|
data-test-namespace-row
|
||||||
>
|
>
|
||||||
<B.Td>
|
<B.Td>
|
||||||
<LinkTo data-test-namespace-name={{B.data.name}} @route="access-control.namespaces.acl-namespace" @model={{B.data.name}}>{{B.data.name}}</LinkTo>
|
<LinkTo data-test-namespace-name={{B.data.name}} @route="administration.namespaces.acl-namespace" @model={{B.data.name}}>{{B.data.name}}</LinkTo>
|
||||||
</B.Td>
|
</B.Td>
|
||||||
<B.Td>{{B.data.description}}</B.Td>
|
<B.Td>{{B.data.description}}</B.Td>
|
||||||
</B.Tr>
|
</B.Tr>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
SPDX-License-Identifier: BUSL-1.1
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
~}}
|
~}}
|
||||||
|
|
||||||
<Breadcrumb @crumb={{hash label="New" args=(array "access-control.namespaces.new")}} />
|
<Breadcrumb @crumb={{hash label="New" args=(array "administration.namespaces.new")}} />
|
||||||
{{page-title "Create Namespace"}}
|
{{page-title "Create Namespace"}}
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<h1 class="title with-flex" data-test-title>
|
<h1 class="title with-flex" data-test-title>
|
||||||
@@ -4,5 +4,5 @@
|
|||||||
~}}
|
~}}
|
||||||
|
|
||||||
{{page-title "Policies"}}
|
{{page-title "Policies"}}
|
||||||
<Breadcrumb @crumb={{hash label="Policies" args=(array "access-control.policies")}} />
|
<Breadcrumb @crumb={{hash label="Policies" args=(array "administration.policies")}} />
|
||||||
{{outlet}}
|
{{outlet}}
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<Hds::Button
|
<Hds::Button
|
||||||
@text="Create Policy"
|
@text="Create Policy"
|
||||||
@icon="plus"
|
@icon="plus"
|
||||||
@route="access-control.policies.new"
|
@route="administration.policies.new"
|
||||||
{{keyboard-shortcut
|
{{keyboard-shortcut
|
||||||
pattern=(array "n" "p")
|
pattern=(array "n" "p")
|
||||||
action=(action this.goToNewPolicy)
|
action=(action this.goToNewPolicy)
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
data-test-policy-row
|
data-test-policy-row
|
||||||
>
|
>
|
||||||
<B.Td>
|
<B.Td>
|
||||||
<LinkTo data-test-policy-name={{B.data.name}} @route="access-control.policies.policy" @model={{B.data.name}}>{{B.data.name}}</LinkTo>
|
<LinkTo data-test-policy-name={{B.data.name}} @route="administration.policies.policy" @model={{B.data.name}}>{{B.data.name}}</LinkTo>
|
||||||
</B.Td>
|
</B.Td>
|
||||||
<B.Td>{{B.data.description}}</B.Td>
|
<B.Td>{{B.data.description}}</B.Td>
|
||||||
{{#if (can "list token")}}
|
{{#if (can "list token")}}
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
No Policies
|
No Policies
|
||||||
</h3>
|
</h3>
|
||||||
<p class="empty-message-body">
|
<p class="empty-message-body">
|
||||||
Get started by <LinkTo @route="access-control.policies.new">creating a new policy</LinkTo>
|
Get started by <LinkTo @route="administration.policies.new">creating a new policy</LinkTo>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
SPDX-License-Identifier: BUSL-1.1
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
~}}
|
~}}
|
||||||
|
|
||||||
<Breadcrumb @crumb={{hash label="New" args=(array "access-control.policies.new")}} />
|
<Breadcrumb @crumb={{hash label="New" args=(array "administration.policies.new")}} />
|
||||||
{{page-title "Create Policy"}}
|
{{page-title "Create Policy"}}
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<h1 class="title with-flex" data-test-title>
|
<h1 class="title with-flex" data-test-title>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
SPDX-License-Identifier: BUSL-1.1
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
~}}
|
~}}
|
||||||
|
|
||||||
<Breadcrumb @crumb={{hash label=this.policy.name args=(array "access-control.policies.policy" this.policy.name)}} />
|
<Breadcrumb @crumb={{hash label=this.policy.name args=(array "administration.policies.policy" this.policy.name)}} />
|
||||||
{{page-title "Policy"}}
|
{{page-title "Policy"}}
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<h1 class="title with-flex" data-test-title>
|
<h1 class="title with-flex" data-test-title>
|
||||||
@@ -4,5 +4,5 @@
|
|||||||
~}}
|
~}}
|
||||||
|
|
||||||
{{page-title "Roles"}}
|
{{page-title "Roles"}}
|
||||||
<Breadcrumb @crumb={{hash label="Roles" args=(array "access-control.roles")}} />
|
<Breadcrumb @crumb={{hash label="Roles" args=(array "administration.roles")}} />
|
||||||
{{outlet}}
|
{{outlet}}
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<Hds::Button
|
<Hds::Button
|
||||||
@text="Create Role"
|
@text="Create Role"
|
||||||
@icon="plus"
|
@icon="plus"
|
||||||
@route="access-control.roles.new"
|
@route="administration.roles.new"
|
||||||
{{keyboard-shortcut
|
{{keyboard-shortcut
|
||||||
pattern=(array "n" "r")
|
pattern=(array "n" "r")
|
||||||
action=(action this.goToNewRole)
|
action=(action this.goToNewRole)
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
data-test-role-row={{B.data.name}}
|
data-test-role-row={{B.data.name}}
|
||||||
>
|
>
|
||||||
<B.Td data-test-role-name={{B.data.name}}>
|
<B.Td data-test-role-name={{B.data.name}}>
|
||||||
<LinkTo @route="access-control.roles.role" @model={{B.data.id}}>{{B.data.name}}</LinkTo></B.Td>
|
<LinkTo @route="administration.roles.role" @model={{B.data.id}}>{{B.data.name}}</LinkTo></B.Td>
|
||||||
<B.Td data-test-role-description>{{B.data.description}}</B.Td>
|
<B.Td data-test-role-description>{{B.data.description}}</B.Td>
|
||||||
{{#if (can "list token")}}
|
{{#if (can "list token")}}
|
||||||
<B.Td>
|
<B.Td>
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
{{#each B.data.policyNames as |policyName|}}
|
{{#each B.data.policyNames as |policyName|}}
|
||||||
{{#let (find-by "name" policyName this.model.policies) as |policy|}}
|
{{#let (find-by "name" policyName this.model.policies) as |policy|}}
|
||||||
{{#if policy}}
|
{{#if policy}}
|
||||||
<Hds::Tag @color="primary" @text="{{policy.name}}" @route="access-control.policies.policy" @model="{{policy.name}}" />
|
<Hds::Tag @color="primary" @text="{{policy.name}}" @route="administration.policies.policy" @model="{{policy.name}}" />
|
||||||
{{else}}
|
{{else}}
|
||||||
<Hds::Tag
|
<Hds::Tag
|
||||||
{{hds-tooltip "This policy has been deleted"}}
|
{{hds-tooltip "This policy has been deleted"}}
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
No Roles
|
No Roles
|
||||||
</h3>
|
</h3>
|
||||||
<p class="empty-message-body">
|
<p class="empty-message-body">
|
||||||
Get started by <LinkTo @route="access-control.roles.new">creating a new role</LinkTo>
|
Get started by <LinkTo @route="administration.roles.new">creating a new role</LinkTo>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
SPDX-License-Identifier: BUSL-1.1
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
~}}
|
~}}
|
||||||
|
|
||||||
<Breadcrumb @crumb={{hash label="New" args=(array "access-control.roles.new")}} />
|
<Breadcrumb @crumb={{hash label="New" args=(array "administration.roles.new")}} />
|
||||||
{{page-title "Create Role"}}
|
{{page-title "Create Role"}}
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<h1 class="title with-flex" data-test-title>
|
<h1 class="title with-flex" data-test-title>
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
No Policies
|
No Policies
|
||||||
</h3>
|
</h3>
|
||||||
<p class="empty-message-body">
|
<p class="empty-message-body">
|
||||||
At least one Policy is required to create a Role; <LinkTo @route="access-control.policies.new">create a new policy</LinkTo>
|
At least one Policy is required to create a Role; <LinkTo @route="administration.policies.new">create a new policy</LinkTo>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
Copyright (c) HashiCorp, Inc.
|
Copyright (c) HashiCorp, Inc.
|
||||||
SPDX-License-Identifier: BUSL-1.1
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
~}}
|
~}}
|
||||||
<Breadcrumb @crumb={{hash label=this.role.name args=(array "access-control.roles.role" this.role.id)}} />
|
<Breadcrumb @crumb={{hash label=this.role.name args=(array "administration.roles.role" this.role.id)}} />
|
||||||
{{page-title "Role"}}
|
{{page-title "Role"}}
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<h1 class="title with-flex" data-test-title>
|
<h1 class="title with-flex" data-test-title>
|
||||||
8
ui/app/templates/administration/sentinel-policies.hbs
Normal file
8
ui/app/templates/administration/sentinel-policies.hbs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{{!
|
||||||
|
Copyright (c) HashiCorp, Inc.
|
||||||
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
|
~}}
|
||||||
|
|
||||||
|
<Breadcrumb @crumb={{hash label="Sentinel Policies" args=(array "administration.sentinel-policies.index")}} />
|
||||||
|
{{page-title "Sentinel Policies"}}
|
||||||
|
{{outlet}}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{{!
|
||||||
|
Copyright (c) HashiCorp, Inc.
|
||||||
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
|
~}}
|
||||||
|
|
||||||
|
<Breadcrumb @crumb={{hash label="Gallery" args=(array "administration.sentinel-policies.gallery" )}} />
|
||||||
|
{{page-title "Sentinel Policy Gallery"}}
|
||||||
|
<section class="section">
|
||||||
|
<Hds::PageHeader class="variable-title" as |PH|>
|
||||||
|
<PH.Title>Choose a Template</PH.Title>
|
||||||
|
<PH.Description>
|
||||||
|
Select a policy template below. You will have an opportunity to modify the policy before it is submitted.
|
||||||
|
</PH.Description>
|
||||||
|
</Hds::PageHeader>
|
||||||
|
<main class="radio-group" data-test-template-list>
|
||||||
|
<Hds::Form::RadioCard::Group as |G|>
|
||||||
|
<G.Legend>Select a Template</G.Legend>
|
||||||
|
{{#each this.templates as |template|}}
|
||||||
|
<G.RadioCard class="form-container" @layout="fixed" @maxWidth="30%" @checked={{eq template.name
|
||||||
|
this.selectedTemplate}} id={{template.name}} data-test-template-card={{template.name}} {{on "change"
|
||||||
|
this.onChange}} as |R|>
|
||||||
|
<R.Label data-test-template-label>{{template.displayName}}</R.Label>
|
||||||
|
<R.Description data-test-template-description>{{template.description}}</R.Description>
|
||||||
|
</G.RadioCard>
|
||||||
|
{{/each}}
|
||||||
|
</Hds::Form::RadioCard::Group>
|
||||||
|
</main>
|
||||||
|
<footer class="buttonset">
|
||||||
|
<Hds::ButtonSet class="button-group">
|
||||||
|
<Hds::Button @text="Apply" @route="administration.sentinel-policies.new" @query={{hash template=this.selectedTemplate}}
|
||||||
|
disabled={{is-equal this.selectedTemplate null}} data-test-apply />
|
||||||
|
<Hds::Button @text="Cancel" @route="administration.sentinel-policies.new" @color="secondary" data-test-cancel />
|
||||||
|
</Hds::ButtonSet>
|
||||||
|
</footer>
|
||||||
|
</section>
|
||||||
78
ui/app/templates/administration/sentinel-policies/index.hbs
Normal file
78
ui/app/templates/administration/sentinel-policies/index.hbs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
{{!
|
||||||
|
Copyright (c) HashiCorp, Inc.
|
||||||
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
|
~}}
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<Hds::PageHeader class="variable-title" as |PH|>
|
||||||
|
<PH.Title>Sentinel Policies</PH.Title>
|
||||||
|
<PH.Description>
|
||||||
|
Nomad integrates with <Hds::Link::Inline @icon="collections" @href="https://developer.hashicorp.com/nomad/tutorials/governance-and-policy/sentinel">HashiCorp Sentinel</Hds::Link::Inline> 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.
|
||||||
|
</PH.Description>
|
||||||
|
<PH.Actions>
|
||||||
|
{{#if (can "write sentinel-policy")}}
|
||||||
|
<span
|
||||||
|
{{keyboard-shortcut
|
||||||
|
pattern=(array "n" "p" )
|
||||||
|
action=(action this.goToNewPolicy)
|
||||||
|
label="Create Policy"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Hds::Button @text="Create from Scratch" @icon="plus" @route="administration.sentinel-policies.new" data-test-create-sentinel-policy />
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
{{keyboard-shortcut
|
||||||
|
pattern=(array "n" "t" "p")
|
||||||
|
action=(action this.goToTemplateGallery)
|
||||||
|
label="Create Policy from Template"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Hds::Button @text="Create from Template" @icon="plus" @route="administration.sentinel-policies.gallery" />
|
||||||
|
</span>
|
||||||
|
{{else}}
|
||||||
|
<Hds::Button @text="Create Policy" @icon="plus" disabled data-test-disabled-create-sentinel-policy />
|
||||||
|
{{/if}}
|
||||||
|
</PH.Actions>
|
||||||
|
</Hds::PageHeader>
|
||||||
|
|
||||||
|
{{#if this.model}}
|
||||||
|
<Hds::Table @caption="A list of policies for this cluster" class="acl-table" @model={{this.model}}
|
||||||
|
@columns={{this.columns}} @sortBy="name">
|
||||||
|
<:body as |B|>
|
||||||
|
<B.Tr {{keyboard-shortcut enumerated=true action=(action "openPolicy" B.data) }} data-test-sentinel-policy-row>
|
||||||
|
<B.Td>
|
||||||
|
<LinkTo data-test-sentinel-policy-name={{B.data.name}} @route="administration.sentinel-policies.policy"
|
||||||
|
@model={{B.data.name}}>{{B.data.name}}</LinkTo>
|
||||||
|
</B.Td>
|
||||||
|
<B.Td>{{B.data.description}}</B.Td>
|
||||||
|
<B.Td>{{B.data.enforcementLevel}}</B.Td>
|
||||||
|
{{#if (can "destroy sentinel-policy")}}
|
||||||
|
<B.Td>
|
||||||
|
<TwoStepButton
|
||||||
|
data-test-delete-policy
|
||||||
|
@idleText="Delete"
|
||||||
|
@inlineText={{true}}
|
||||||
|
@cancelText="Cancel"
|
||||||
|
@confirmText="Yes, Delete Policy"
|
||||||
|
@confirmationMessage="Are you sure?"
|
||||||
|
@awaitingConfirmation={{this.deletePolicy.isRunning}}
|
||||||
|
@disabled={{this.deletePolicy.isRunning}}
|
||||||
|
@onConfirm={{perform this.deletePolicy B.data}}
|
||||||
|
/>
|
||||||
|
</B.Td>
|
||||||
|
{{/if}}
|
||||||
|
</B.Tr>
|
||||||
|
</:body>
|
||||||
|
</Hds::Table>
|
||||||
|
{{else}}
|
||||||
|
<div data-test-empty-jobs-list class="empty-message">
|
||||||
|
<h3 data-test-empty-jobs-list-headline class="empty-message-headline">
|
||||||
|
No Sentinel Policies
|
||||||
|
</h3>
|
||||||
|
<p class="empty-message-body">
|
||||||
|
Get started by <LinkTo @route="administration.sentinel-policies.new">creating a policy from scratch</LinkTo> or
|
||||||
|
by <LinkTo @route="administration.sentinel-policies.gallery">creating one from the policy gallery</LinkTo>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</section>
|
||||||
26
ui/app/templates/administration/sentinel-policies/new.hbs
Normal file
26
ui/app/templates/administration/sentinel-policies/new.hbs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{{!
|
||||||
|
Copyright (c) HashiCorp, Inc.
|
||||||
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
|
~}}
|
||||||
|
|
||||||
|
<Breadcrumb @crumb={{hash label="New" args=(array "administration.sentinel-policies.new")}} />
|
||||||
|
{{page-title "Create a Policy"}}
|
||||||
|
<section class="section">
|
||||||
|
<Hds::PageHeader class="variable-title" as |PH|>
|
||||||
|
<PH.Title>Create Sentinel Policy</PH.Title>
|
||||||
|
<PH.Description>
|
||||||
|
Nomad integrates with <Hds::Link::Inline @icon="collections" @href="https://developer.hashicorp.com/nomad/tutorials/governance-and-policy/sentinel">HashiCorp Sentinel</Hds::Link::Inline> 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.
|
||||||
|
</PH.Description>
|
||||||
|
<PH.Actions>
|
||||||
|
<Hds::Button
|
||||||
|
@text="Start from a template"
|
||||||
|
@color="secondary"
|
||||||
|
@route="administration.sentinel-policies.gallery"
|
||||||
|
data-test-choose-template
|
||||||
|
/>
|
||||||
|
</PH.Actions>
|
||||||
|
</Hds::PageHeader>
|
||||||
|
|
||||||
|
<SentinelPolicyEditor @policy={{this.model}} />
|
||||||
|
|
||||||
|
</section>
|
||||||
32
ui/app/templates/administration/sentinel-policies/policy.hbs
Normal file
32
ui/app/templates/administration/sentinel-policies/policy.hbs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{{!
|
||||||
|
Copyright (c) HashiCorp, Inc.
|
||||||
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
|
~}}
|
||||||
|
|
||||||
|
<Breadcrumb @crumb={{hash label=this.model.name args=(array "administration.sentinel-policies.policy" this.model.name)}} />
|
||||||
|
{{page-title (concat "Sentinel Policy: " this.model.name)}}
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<Hds::PageHeader class="variable-title" as |PH|>
|
||||||
|
<PH.Title>{{this.model.name}}</PH.Title>
|
||||||
|
<PH.Actions>
|
||||||
|
{{#if (can "destroy sentinel-policy")}}
|
||||||
|
<div>
|
||||||
|
<TwoStepButton
|
||||||
|
data-test-delete-policy
|
||||||
|
@alignRight={{true}}
|
||||||
|
@idleText="Delete Sentinel Policy"
|
||||||
|
@cancelText="Cancel"
|
||||||
|
@confirmText="Yes, Delete Policy"
|
||||||
|
@confirmationMessage="Are you sure?"
|
||||||
|
@awaitingConfirmation={{this.deletePolicy.isRunning}}
|
||||||
|
@disabled={{this.deletePolicy.isRunning}}
|
||||||
|
@onConfirm={{perform this.deletePolicy}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</PH.Actions>
|
||||||
|
</Hds::PageHeader>
|
||||||
|
|
||||||
|
<SentinelPolicyEditor @policy={{this.model}} />
|
||||||
|
</section>
|
||||||
@@ -4,5 +4,5 @@
|
|||||||
~}}
|
~}}
|
||||||
|
|
||||||
{{page-title "Tokens"}}
|
{{page-title "Tokens"}}
|
||||||
<Breadcrumb @crumb={{hash label="Tokens" args=(array "access-control.tokens")}} />
|
<Breadcrumb @crumb={{hash label="Tokens" args=(array "administration.tokens")}} />
|
||||||
{{outlet}}
|
{{outlet}}
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<Hds::Button
|
<Hds::Button
|
||||||
@text="Create Token"
|
@text="Create Token"
|
||||||
@icon="plus"
|
@icon="plus"
|
||||||
@route="access-control.tokens.new"
|
@route="administration.tokens.new"
|
||||||
{{keyboard-shortcut
|
{{keyboard-shortcut
|
||||||
pattern=(array "n" "t")
|
pattern=(array "n" "t")
|
||||||
action=(action this.goToNewToken)
|
action=(action this.goToNewToken)
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
{{#if (eq B.data.id this.selfToken.id)}}
|
{{#if (eq B.data.id this.selfToken.id)}}
|
||||||
<strong>{{B.data.name}}</strong>
|
<strong>{{B.data.name}}</strong>
|
||||||
{{else}}
|
{{else}}
|
||||||
<LinkTo @route="access-control.tokens.token" @model={{B.data.id}}>
|
<LinkTo @route="administration.tokens.token" @model={{B.data.id}}>
|
||||||
{{B.data.name}}
|
{{B.data.name}}
|
||||||
</LinkTo>
|
</LinkTo>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
--}}
|
--}}
|
||||||
{{#each B.data.roles as |role|}}
|
{{#each B.data.roles as |role|}}
|
||||||
{{#if role.name}}
|
{{#if role.name}}
|
||||||
<Hds::Tag @color="primary" @text="{{role.name}}" @route="access-control.roles.role" @model="{{role.id}}" />
|
<Hds::Tag @color="primary" @text="{{role.name}}" @route="administration.roles.role" @model="{{role.id}}" />
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#if (eq B.data.type "management")}}
|
{{#if (eq B.data.type "management")}}
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
{{#each B.data.policyNames as |policyName|}}
|
{{#each B.data.policyNames as |policyName|}}
|
||||||
{{#let (find-by "name" policyName this.model.policies) as |policy|}}
|
{{#let (find-by "name" policyName this.model.policies) as |policy|}}
|
||||||
{{#if policy}}
|
{{#if policy}}
|
||||||
<Hds::Tag @color="primary" @text="{{policy.name}}" @route="access-control.policies.policy" @model="{{policy.name}}" />
|
<Hds::Tag @color="primary" @text="{{policy.name}}" @route="administration.policies.policy" @model="{{policy.name}}" />
|
||||||
{{else}}
|
{{else}}
|
||||||
<Hds::Tag
|
<Hds::Tag
|
||||||
{{hds-tooltip "This policy has been deleted"}}
|
{{hds-tooltip "This policy has been deleted"}}
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
No Tokens
|
No Tokens
|
||||||
</h3>
|
</h3>
|
||||||
<p class="empty-message-body">
|
<p class="empty-message-body">
|
||||||
Get started by <LinkTo @route="access-control.policies.new">creating a new policy</LinkTo>
|
Get started by <LinkTo @route="administration.policies.new">creating a new policy</LinkTo>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
SPDX-License-Identifier: BUSL-1.1
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
~}}
|
~}}
|
||||||
|
|
||||||
<Breadcrumb @crumb={{hash label="New" args=(array "access-control.tokens.new")}} />
|
<Breadcrumb @crumb={{hash label="New" args=(array "administration.tokens.new")}} />
|
||||||
{{page-title "Create Token"}}
|
{{page-title "Create Token"}}
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<h1 class="title with-flex" data-test-title>
|
<h1 class="title with-flex" data-test-title>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
Copyright (c) HashiCorp, Inc.
|
Copyright (c) HashiCorp, Inc.
|
||||||
SPDX-License-Identifier: BUSL-1.1
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
~}}
|
~}}
|
||||||
<Breadcrumb @crumb={{hash label=this.activeToken.name args=(array "access-control.tokens.token" this.activeToken.id)}} />
|
<Breadcrumb @crumb={{hash label=this.activeToken.name args=(array "administration.tokens.token" this.activeToken.id)}} />
|
||||||
{{page-title "Token"}}
|
{{page-title "Token"}}
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<h1 class="title with-flex" data-test-title>
|
<h1 class="title with-flex" data-test-title>
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{{!
|
|
||||||
Copyright (c) HashiCorp, Inc.
|
|
||||||
SPDX-License-Identifier: BUSL-1.1
|
|
||||||
~}}
|
|
||||||
|
|
||||||
<div class="tabs is-subnav" {{did-insert this.keyboard.registerNav type="subnav"}} {{will-destroy this.keyboard.unregisterSubnav}}>
|
|
||||||
<ul>
|
|
||||||
<li><LinkTo @route="access-control.index" @activeClass="is-active">Overview</LinkTo></li>
|
|
||||||
<li><LinkTo @route="access-control.tokens" @activeClass="is-active">Tokens</LinkTo></li>
|
|
||||||
<li><LinkTo @route="access-control.roles" @activeClass="is-active">Roles</LinkTo></li>
|
|
||||||
<li><LinkTo @route="access-control.policies" @activeClass="is-active">Policies</LinkTo></li>
|
|
||||||
<li><LinkTo @route="access-control.namespaces" @activeClass="is-active">Namespaces</LinkTo></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
17
ui/app/templates/components/administration-subnav.hbs
Normal file
17
ui/app/templates/components/administration-subnav.hbs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{{!
|
||||||
|
Copyright (c) HashiCorp, Inc.
|
||||||
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
|
~}}
|
||||||
|
|
||||||
|
<div class="tabs is-subnav" {{did-insert this.keyboard.registerNav type="subnav"}} {{will-destroy this.keyboard.unregisterSubnav}}>
|
||||||
|
<ul>
|
||||||
|
<li><LinkTo @route="administration.index" @activeClass="is-active">Overview</LinkTo></li>
|
||||||
|
<li><LinkTo @route="administration.tokens" @activeClass="is-active">Tokens</LinkTo></li>
|
||||||
|
<li><LinkTo @route="administration.roles" @activeClass="is-active">Roles</LinkTo></li>
|
||||||
|
<li><LinkTo @route="administration.policies" @activeClass="is-active">Policies</LinkTo></li>
|
||||||
|
<li><LinkTo @route="administration.namespaces" @activeClass="is-active">Namespaces</LinkTo></li>
|
||||||
|
{{#if (can "list sentinel-policy")}}
|
||||||
|
<li><LinkTo @route="administration.sentinel-policies" @activeClass="is-active">Sentinel Policies</LinkTo></li>
|
||||||
|
{{/if}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
@@ -136,15 +136,15 @@
|
|||||||
{{keyboard-shortcut
|
{{keyboard-shortcut
|
||||||
menuLevel=true
|
menuLevel=true
|
||||||
pattern=(array "g" "a")
|
pattern=(array "g" "a")
|
||||||
action=(action this.transitionTo 'access-control')
|
action=(action this.transitionTo 'administration')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LinkTo
|
<LinkTo
|
||||||
@route="access-control"
|
@route="administration"
|
||||||
@activeClass="is-active"
|
@activeClass="is-active"
|
||||||
data-test-gutter-link="access-control"
|
data-test-gutter-link="administration"
|
||||||
>
|
>
|
||||||
Access Control
|
Administration
|
||||||
</LinkTo>
|
</LinkTo>
|
||||||
</li>
|
</li>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
>
|
>
|
||||||
<div class="hds-button__text">Upload file</div>
|
<div class="hds-button__text">Upload file</div>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
onchange={{action this.uploadJobSpec}}
|
onchange={{action this.uploadJobSpec}}
|
||||||
accept=".hcl,.json,.nomad"
|
accept=".hcl,.json,.nomad"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
<div class="boxed-section-body is-full-bleed">
|
<div class="boxed-section-body is-full-bleed">
|
||||||
{{#if (eq @data.view "job-spec")}}
|
{{#if (eq @data.view "job-spec")}}
|
||||||
<div
|
<div
|
||||||
data-test-job-spec-view
|
data-test-job-spec-view
|
||||||
{{code-mirror
|
{{code-mirror
|
||||||
content=@data.definition
|
content=@data.definition
|
||||||
mode=(if (eq @data.format "json") "javascript" "ruby")
|
mode=(if (eq @data.format "json") "javascript" "ruby")
|
||||||
|
|||||||
50
ui/app/utils/default-sentinel-policy-templates.js
Normal file
50
ui/app/utils/default-sentinel-policy-templates.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import countLimitsPolicy from './sentinel_policy_templates/count-limits';
|
||||||
|
import noFridaysPolicy from './sentinel_policy_templates/no-friday-deploys';
|
||||||
|
import alwaysFailPolicy from './sentinel_policy_templates/always-fail';
|
||||||
|
import canariesOnlyPolicy from './sentinel_policy_templates/canaries-only';
|
||||||
|
import resourceLimitsPolicy from './sentinel_policy_templates/resource-limits';
|
||||||
|
import restictImagesPolicy from './sentinel_policy_templates/restrict-images';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
displayName: 'Count Limits',
|
||||||
|
name: 'count-limits',
|
||||||
|
description: 'Enforces that no task group has a count over 100',
|
||||||
|
policy: countLimitsPolicy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'No Friday Deploys',
|
||||||
|
name: 'no-friday-deploys',
|
||||||
|
description: 'Ensures that no deploys happen on a Friday',
|
||||||
|
policy: noFridaysPolicy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Always Fail',
|
||||||
|
name: 'always-fail',
|
||||||
|
description: 'A test Sentinel Policy that will always fail',
|
||||||
|
policy: alwaysFailPolicy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Canaries Only',
|
||||||
|
name: 'canaries-only',
|
||||||
|
description: 'All deployments must have a canary',
|
||||||
|
policy: canariesOnlyPolicy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Resource Limits',
|
||||||
|
name: 'resource-limits',
|
||||||
|
description: 'Ensures that tasks do not request too much CPU or Memory',
|
||||||
|
policy: resourceLimitsPolicy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Restrict Images',
|
||||||
|
name: 'restrict-images',
|
||||||
|
description: 'Allows only certain Docker images and disables "latest" tags',
|
||||||
|
policy: restictImagesPolicy,
|
||||||
|
},
|
||||||
|
];
|
||||||
9
ui/app/utils/sentinel_policy_templates/always-fail.js
Normal file
9
ui/app/utils/sentinel_policy_templates/always-fail.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default `# This policy will always fail. You can temporarily halt all new job updates using this.
|
||||||
|
|
||||||
|
main = rule { false }
|
||||||
|
`;
|
||||||
14
ui/app/utils/sentinel_policy_templates/canaries-only.js
Normal file
14
ui/app/utils/sentinel_policy_templates/canaries-only.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default `# This policy ensures that all deployments must use canary deployments.
|
||||||
|
|
||||||
|
canary_required = rule {
|
||||||
|
all job.task_groups as tg {
|
||||||
|
tg.update.canary > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main = rule { canary_required }`;
|
||||||
15
ui/app/utils/sentinel_policy_templates/count-limits.js
Normal file
15
ui/app/utils/sentinel_policy_templates/count-limits.js
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
11
ui/app/utils/sentinel_policy_templates/no-friday-deploys.js
Normal file
11
ui/app/utils/sentinel_policy_templates/no-friday-deploys.js
Normal file
@@ -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 }`;
|
||||||
21
ui/app/utils/sentinel_policy_templates/resource-limits.js
Normal file
21
ui/app/utils/sentinel_policy_templates/resource-limits.js
Normal file
@@ -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
|
||||||
|
}`;
|
||||||
32
ui/app/utils/sentinel_policy_templates/restrict-images.js
Normal file
32
ui/app/utils/sentinel_policy_templates/restrict-images.js
Normal file
@@ -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
|
||||||
|
}`;
|
||||||
@@ -881,6 +881,23 @@ export default function () {
|
|||||||
return this.serialize(policies.all());
|
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) {
|
this.delete('/acl/policy/:id', function (schema, request) {
|
||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ function jobsIndexTestCluster(server) {
|
|||||||
function smallCluster(server) {
|
function smallCluster(server) {
|
||||||
faker.seed(1);
|
faker.seed(1);
|
||||||
server.create('feature', { name: 'Dynamic Application Sizing' });
|
server.create('feature', { name: 'Dynamic Application Sizing' });
|
||||||
|
server.create('feature', { name: 'Sentinel Policies' });
|
||||||
server.createList('agent', 3, 'withConsulLink', 'withVaultLink');
|
server.createList('agent', 3, 'withConsulLink', 'withVaultLink');
|
||||||
server.createList('node-pool', 2);
|
server.createList('node-pool', 2);
|
||||||
server.createList('node', 5);
|
server.createList('node', 5);
|
||||||
@@ -623,6 +624,7 @@ function variableTestCluster(server) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function policiesTestCluster(server) {
|
function policiesTestCluster(server) {
|
||||||
|
server.create('feature', { name: 'Sentinel Policies' });
|
||||||
faker.seed(1);
|
faker.seed(1);
|
||||||
createTokens(server);
|
createTokens(server);
|
||||||
server.createList('agent', 3, 'withConsulLink', 'withVaultLink');
|
server.createList('agent', 3, 'withConsulLink', 'withVaultLink');
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { module, test } from 'qunit';
|
|||||||
import { currentURL, triggerKeyEvent } from '@ember/test-helpers';
|
import { currentURL, triggerKeyEvent } from '@ember/test-helpers';
|
||||||
import { setupApplicationTest } from 'ember-qunit';
|
import { setupApplicationTest } from 'ember-qunit';
|
||||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
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 Tokens from 'nomad-ui/tests/pages/settings/tokens';
|
||||||
import { allScenarios } from '../../mirage/scenarios/default';
|
import { allScenarios } from '../../mirage/scenarios/default';
|
||||||
import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
|
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) {
|
test('Access Control is only accessible by a management user', async function (assert) {
|
||||||
assert.expect(7);
|
assert.expect(7);
|
||||||
await AccessControl.visit();
|
await Administration.visit();
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
currentURL(),
|
currentURL(),
|
||||||
'/jobs',
|
'/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(
|
assert.equal(
|
||||||
currentURL(),
|
currentURL(),
|
||||||
'/jobs',
|
'/jobs',
|
||||||
'redirected to the jobs page if a non-management token on /tokens'
|
'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();
|
await Tokens.visit();
|
||||||
const managementToken = server.db.tokens.findBy(
|
const managementToken = server.db.tokens.findBy(
|
||||||
@@ -52,22 +52,22 @@ module('Acceptance | access control', function (hooks) {
|
|||||||
const { secretId } = managementToken;
|
const { secretId } = managementToken;
|
||||||
await Tokens.secret(secretId).submit();
|
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(
|
assert.equal(
|
||||||
currentURL(),
|
currentURL(),
|
||||||
'/access-control',
|
'/administration',
|
||||||
'management token can access /access-control'
|
'management token can access /administration'
|
||||||
);
|
);
|
||||||
|
|
||||||
await a11yAudit(assert);
|
await a11yAudit(assert);
|
||||||
|
|
||||||
await AccessControl.visitTokens();
|
await Administration.visitTokens();
|
||||||
assert.equal(
|
assert.equal(
|
||||||
currentURL(),
|
currentURL(),
|
||||||
'/access-control/tokens',
|
'/administration/tokens',
|
||||||
'management token can access /access-control/tokens'
|
'management token can access /administration/tokens'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ module('Acceptance | access control', function (hooks) {
|
|||||||
const { secretId } = managementToken;
|
const { secretId } = managementToken;
|
||||||
await Tokens.secret(secretId).submit();
|
await Tokens.secret(secretId).submit();
|
||||||
|
|
||||||
await AccessControl.visit();
|
await Administration.visit();
|
||||||
assert.dom('[data-test-tokens-card]').exists();
|
assert.dom('[data-test-tokens-card]').exists();
|
||||||
assert.dom('[data-test-roles-card]').exists();
|
assert.dom('[data-test-roles-card]').exists();
|
||||||
assert.dom('[data-test-policies-card]').exists();
|
assert.dom('[data-test-policies-card]').exists();
|
||||||
@@ -112,16 +112,16 @@ module('Acceptance | access control', function (hooks) {
|
|||||||
const { secretId } = managementToken;
|
const { secretId } = managementToken;
|
||||||
await Tokens.secret(secretId).submit();
|
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', {
|
await triggerKeyEvent('.page-layout', 'keydown', 'ArrowRight', {
|
||||||
shiftKey: true,
|
shiftKey: true,
|
||||||
});
|
});
|
||||||
assert.equal(
|
assert.equal(
|
||||||
currentURL(),
|
currentURL(),
|
||||||
`/access-control/tokens`,
|
`/administration/tokens`,
|
||||||
'Shift+ArrowRight takes you to the next tab (Tokens)'
|
'Shift+ArrowRight takes you to the next tab (Tokens)'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ module('Acceptance | access control', function (hooks) {
|
|||||||
});
|
});
|
||||||
assert.equal(
|
assert.equal(
|
||||||
currentURL(),
|
currentURL(),
|
||||||
`/access-control/roles`,
|
`/administration/roles`,
|
||||||
'Shift+ArrowRight takes you to the next tab (Roles)'
|
'Shift+ArrowRight takes you to the next tab (Roles)'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@ module('Acceptance | access control', function (hooks) {
|
|||||||
});
|
});
|
||||||
assert.equal(
|
assert.equal(
|
||||||
currentURL(),
|
currentURL(),
|
||||||
`/access-control/policies`,
|
`/administration/policies`,
|
||||||
'Shift+ArrowRight takes you to the next tab (Policies)'
|
'Shift+ArrowRight takes you to the next tab (Policies)'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ module('Acceptance | access control', function (hooks) {
|
|||||||
});
|
});
|
||||||
assert.equal(
|
assert.equal(
|
||||||
currentURL(),
|
currentURL(),
|
||||||
`/access-control/namespaces`,
|
`/administration/namespaces`,
|
||||||
'Shift+ArrowRight takes you to the next tab (Namespaces)'
|
'Shift+ArrowRight takes you to the next tab (Namespaces)'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@ module('Acceptance | access control', function (hooks) {
|
|||||||
});
|
});
|
||||||
assert.equal(
|
assert.equal(
|
||||||
currentURL(),
|
currentURL(),
|
||||||
`/access-control`,
|
`/administration`,
|
||||||
'Shift+ArrowLeft takes you back to the Access Control index page'
|
'Shift+ArrowLeft takes you back to the Access Control index page'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ module('Acceptance | namespaces', function (hooks) {
|
|||||||
assert.expect(4);
|
assert.expect(4);
|
||||||
allScenarios.namespacesTestCluster(server);
|
allScenarios.namespacesTestCluster(server);
|
||||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
||||||
await visit('/access-control/namespaces');
|
await visit('/administration/namespaces');
|
||||||
assert.dom('[data-test-gutter-link="access-control"]').exists();
|
assert.dom('[data-test-gutter-link="administration"]').exists();
|
||||||
assert.equal(currentURL(), '/access-control/namespaces');
|
assert.equal(currentURL(), '/administration/namespaces');
|
||||||
assert
|
assert
|
||||||
.dom('[data-test-namespace-row]')
|
.dom('[data-test-namespace-row]')
|
||||||
.exists({ count: server.db.namespaces.length });
|
.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) {
|
test('Prevents namespaes access if you lack a management token', async function (assert) {
|
||||||
allScenarios.namespacesTestCluster(server);
|
allScenarios.namespacesTestCluster(server);
|
||||||
window.localStorage.nomadTokenSecret = server.db.tokens[1].secretId;
|
window.localStorage.nomadTokenSecret = server.db.tokens[1].secretId;
|
||||||
await visit('/access-control/namespaces');
|
await visit('/administration/namespaces');
|
||||||
assert.equal(currentURL(), '/jobs');
|
assert.equal(currentURL(), '/jobs');
|
||||||
assert.dom('[data-test-gutter-link="access-control"]').doesNotExist();
|
assert.dom('[data-test-gutter-link="administration"]').doesNotExist();
|
||||||
// Reset Token
|
// Reset Token
|
||||||
window.localStorage.nomadTokenSecret = null;
|
window.localStorage.nomadTokenSecret = null;
|
||||||
});
|
});
|
||||||
@@ -52,15 +52,15 @@ module('Acceptance | namespaces', function (hooks) {
|
|||||||
assert.expect(7);
|
assert.expect(7);
|
||||||
allScenarios.namespacesTestCluster(server);
|
allScenarios.namespacesTestCluster(server);
|
||||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
||||||
await visit('/access-control/namespaces');
|
await visit('/administration/namespaces');
|
||||||
await click('[data-test-create-namespace]');
|
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 typeIn('[data-test-namespace-name-input]', 'My New Namespace');
|
||||||
await click('button[data-test-save-namespace]');
|
await click('button[data-test-save-namespace]');
|
||||||
assert
|
assert
|
||||||
.dom('.flash-message.alert-critical')
|
.dom('.flash-message.alert-critical')
|
||||||
.exists('Doesnt let you save a bad name');
|
.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
|
document.querySelector('[data-test-namespace-name-input]').value = ''; // clear
|
||||||
await typeIn('[data-test-namespace-name-input]', 'My-New-Namespace');
|
await typeIn('[data-test-namespace-name-input]', 'My-New-Namespace');
|
||||||
await click('button[data-test-save-namespace]');
|
await click('button[data-test-save-namespace]');
|
||||||
@@ -68,16 +68,16 @@ module('Acceptance | namespaces', function (hooks) {
|
|||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
currentURL(),
|
currentURL(),
|
||||||
'/access-control/namespaces/My-New-Namespace',
|
'/administration/namespaces/My-New-Namespace',
|
||||||
'redirected to the now-created 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) =>
|
const newNs = [...findAll('[data-test-namespace-name]')].filter((a) =>
|
||||||
a.textContent.includes('My-New-Namespace')
|
a.textContent.includes('My-New-Namespace')
|
||||||
)[0];
|
)[0];
|
||||||
assert.ok(newNs, 'Namespace is in the list');
|
assert.ok(newNs, 'Namespace is in the list');
|
||||||
await click(newNs);
|
await click(newNs);
|
||||||
assert.equal(currentURL(), '/access-control/namespaces/My-New-Namespace');
|
assert.equal(currentURL(), '/administration/namespaces/My-New-Namespace');
|
||||||
await percySnapshot(assert);
|
await percySnapshot(assert);
|
||||||
// Reset Token
|
// Reset Token
|
||||||
window.localStorage.nomadTokenSecret = null;
|
window.localStorage.nomadTokenSecret = null;
|
||||||
@@ -87,7 +87,7 @@ module('Acceptance | namespaces', function (hooks) {
|
|||||||
assert.expect(2);
|
assert.expect(2);
|
||||||
allScenarios.namespacesTestCluster(server, { enterprise: true });
|
allScenarios.namespacesTestCluster(server, { enterprise: true });
|
||||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
||||||
await visit('/access-control/namespaces');
|
await visit('/administration/namespaces');
|
||||||
await click('[data-test-create-namespace]');
|
await click('[data-test-create-namespace]');
|
||||||
|
|
||||||
// Get the dom node text for the description
|
// Get the dom node text for the description
|
||||||
@@ -114,7 +114,7 @@ module('Acceptance | namespaces', function (hooks) {
|
|||||||
assert.expect(2);
|
assert.expect(2);
|
||||||
allScenarios.namespacesTestCluster(server, { enterprise: false });
|
allScenarios.namespacesTestCluster(server, { enterprise: false });
|
||||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
||||||
await visit('/access-control/namespaces');
|
await visit('/administration/namespaces');
|
||||||
await click('[data-test-create-namespace]');
|
await click('[data-test-create-namespace]');
|
||||||
|
|
||||||
// Get the dom node text for the description
|
// 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) {
|
test('Modifying an existing namespace', async function (assert) {
|
||||||
allScenarios.namespacesTestCluster(server);
|
allScenarios.namespacesTestCluster(server);
|
||||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
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');
|
await click('[data-test-namespace-row]:first-child a');
|
||||||
// Table sorts by name by default
|
// Table sorts by name by default
|
||||||
let firstNamespace = server.db.namespaces.sort((a, b) => {
|
let firstNamespace = server.db.namespaces.sort((a, b) => {
|
||||||
@@ -140,7 +140,7 @@ module('Acceptance | namespaces', function (hooks) {
|
|||||||
})[0];
|
})[0];
|
||||||
assert.equal(
|
assert.equal(
|
||||||
currentURL(),
|
currentURL(),
|
||||||
`/access-control/namespaces/${firstNamespace.name}`
|
`/administration/namespaces/${firstNamespace.name}`
|
||||||
);
|
);
|
||||||
assert.dom('[data-test-namespace-editor]').exists();
|
assert.dom('[data-test-namespace-editor]').exists();
|
||||||
assert.dom('[data-test-title]').includesText(firstNamespace.name);
|
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.dom('.flash-message.alert-success').exists();
|
||||||
assert.equal(
|
assert.equal(
|
||||||
currentURL(),
|
currentURL(),
|
||||||
`/access-control/namespaces/${firstNamespace.name}`,
|
`/administration/namespaces/${firstNamespace.name}`,
|
||||||
'remain on page after save'
|
'remain on page after save'
|
||||||
);
|
);
|
||||||
// Reset Token
|
// Reset Token
|
||||||
@@ -159,7 +159,7 @@ module('Acceptance | namespaces', function (hooks) {
|
|||||||
assert.expect(11);
|
assert.expect(11);
|
||||||
allScenarios.namespacesTestCluster(server);
|
allScenarios.namespacesTestCluster(server);
|
||||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
||||||
await visit('/access-control/namespaces');
|
await visit('/administration/namespaces');
|
||||||
|
|
||||||
// Default namespace hides delete button
|
// Default namespace hides delete button
|
||||||
const defaultNamespaceLink = [
|
const defaultNamespaceLink = [
|
||||||
@@ -167,14 +167,14 @@ module('Acceptance | namespaces', function (hooks) {
|
|||||||
].filter((row) => row.textContent.includes('default'))[0];
|
].filter((row) => row.textContent.includes('default'))[0];
|
||||||
await click(defaultNamespaceLink);
|
await click(defaultNamespaceLink);
|
||||||
|
|
||||||
assert.equal(currentURL(), `/access-control/namespaces/default`);
|
assert.equal(currentURL(), `/administration/namespaces/default`);
|
||||||
let deleteButton = find('[data-test-delete-namespace] button');
|
let deleteButton = find('[data-test-delete-namespace] button');
|
||||||
assert
|
assert
|
||||||
.dom(deleteButton)
|
.dom(deleteButton)
|
||||||
.doesNotExist('delete button is not present for default');
|
.doesNotExist('delete button is not present for default');
|
||||||
|
|
||||||
// Standard namespace properly deletes
|
// Standard namespace properly deletes
|
||||||
await visit('/access-control/namespaces');
|
await visit('/administration/namespaces');
|
||||||
|
|
||||||
let nonDefaultNamespace = server.db.namespaces.findBy(
|
let nonDefaultNamespace = server.db.namespaces.findBy(
|
||||||
(ns) => ns.name != 'default'
|
(ns) => ns.name != 'default'
|
||||||
@@ -185,7 +185,7 @@ module('Acceptance | namespaces', function (hooks) {
|
|||||||
await click(nonDefaultNsLink);
|
await click(nonDefaultNsLink);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
currentURL(),
|
currentURL(),
|
||||||
`/access-control/namespaces/${nonDefaultNamespace.name}`
|
`/administration/namespaces/${nonDefaultNamespace.name}`
|
||||||
);
|
);
|
||||||
deleteButton = find('[data-test-delete-namespace] button');
|
deleteButton = find('[data-test-delete-namespace] button');
|
||||||
assert.dom(deleteButton).exists('delete button is present for non-default');
|
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');
|
.exists('confirmation message is present');
|
||||||
await click(find('[data-test-confirm-button]'));
|
await click(find('[data-test-confirm-button]'));
|
||||||
assert.dom('.flash-message.alert-success').exists();
|
assert.dom('.flash-message.alert-success').exists();
|
||||||
assert.equal(currentURL(), '/access-control/namespaces');
|
assert.equal(currentURL(), '/administration/namespaces');
|
||||||
assert
|
assert
|
||||||
.dom(`[data-test-namespace-name="${nonDefaultNamespace.name}"]`)
|
.dom(`[data-test-namespace-name="${nonDefaultNamespace.name}"]`)
|
||||||
.doesNotExist();
|
.doesNotExist();
|
||||||
|
|
||||||
// Namespace with variables errors properly
|
// Namespace with variables errors properly
|
||||||
// "with-variables" hard-coded into scenario to be a NS with variables attached
|
// "with-variables" hard-coded into scenario to be a NS with variables attached
|
||||||
await visit('/access-control/namespaces/with-variables');
|
await visit('/administration/namespaces/with-variables');
|
||||||
assert.equal(currentURL(), '/access-control/namespaces/with-variables');
|
assert.equal(currentURL(), '/administration/namespaces/with-variables');
|
||||||
deleteButton = find('[data-test-delete-namespace] button');
|
deleteButton = find('[data-test-delete-namespace] button');
|
||||||
await click(deleteButton);
|
await click(deleteButton);
|
||||||
await click(find('[data-test-confirm-button]'));
|
await click(find('[data-test-confirm-button]'));
|
||||||
@@ -211,7 +211,7 @@ module('Acceptance | namespaces', function (hooks) {
|
|||||||
.dom('.flash-message.alert-critical')
|
.dom('.flash-message.alert-critical')
|
||||||
.exists('Doesnt let you delete a namespace with variables');
|
.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
|
// Reset Token
|
||||||
window.localStorage.nomadTokenSecret = null;
|
window.localStorage.nomadTokenSecret = null;
|
||||||
@@ -228,7 +228,7 @@ module('Acceptance | namespaces', function (hooks) {
|
|||||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
||||||
|
|
||||||
// Attempt a delete on an un-deletable namespace
|
// 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');
|
let deleteButton = find('[data-test-delete-namespace] button');
|
||||||
await click(deleteButton);
|
await click(deleteButton);
|
||||||
await click(find('[data-test-confirm-button]'));
|
await click(find('[data-test-confirm-button]'));
|
||||||
@@ -236,10 +236,10 @@ module('Acceptance | namespaces', function (hooks) {
|
|||||||
assert
|
assert
|
||||||
.dom('.flash-message.alert-critical')
|
.dom('.flash-message.alert-critical')
|
||||||
.exists('Doesnt let you delete a namespace with variables');
|
.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
|
// Navigate back to the page via the index
|
||||||
await visit('/access-control/namespaces');
|
await visit('/administration/namespaces');
|
||||||
|
|
||||||
// Default namespace hides delete button
|
// Default namespace hides delete button
|
||||||
const notDeletedNSLink = [...findAll('[data-test-namespace-name]')].filter(
|
const notDeletedNSLink = [...findAll('[data-test-namespace-name]')].filter(
|
||||||
@@ -247,6 +247,6 @@ module('Acceptance | namespaces', function (hooks) {
|
|||||||
)[0];
|
)[0];
|
||||||
await click(notDeletedNSLink);
|
await click(notDeletedNSLink);
|
||||||
|
|
||||||
assert.equal(currentURL(), `/access-control/namespaces/with-variables`);
|
assert.equal(currentURL(), `/administration/namespaces/with-variables`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ module('Acceptance | policies', function (hooks) {
|
|||||||
assert.expect(4);
|
assert.expect(4);
|
||||||
allScenarios.policiesTestCluster(server);
|
allScenarios.policiesTestCluster(server);
|
||||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
||||||
await visit('/access-control/policies');
|
await visit('/administration/policies');
|
||||||
assert.dom('[data-test-gutter-link="access-control"]').exists();
|
assert.dom('[data-test-gutter-link="administration"]').exists();
|
||||||
assert.equal(currentURL(), '/access-control/policies');
|
assert.equal(currentURL(), '/administration/policies');
|
||||||
assert
|
assert
|
||||||
.dom('[data-test-policy-row]')
|
.dom('[data-test-policy-row]')
|
||||||
.exists({ count: server.db.policies.length });
|
.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) {
|
test('Prevents policies access if you lack a management token', async function (assert) {
|
||||||
allScenarios.policiesTestCluster(server);
|
allScenarios.policiesTestCluster(server);
|
||||||
window.localStorage.nomadTokenSecret = server.db.tokens[1].secretId;
|
window.localStorage.nomadTokenSecret = server.db.tokens[1].secretId;
|
||||||
await visit('/access-control/policies');
|
await visit('/administration/policies');
|
||||||
assert.equal(currentURL(), '/jobs');
|
assert.equal(currentURL(), '/jobs');
|
||||||
assert.dom('[data-test-gutter-link="access-control"]').doesNotExist();
|
assert.dom('[data-test-gutter-link="administration"]').doesNotExist();
|
||||||
// Reset Token
|
// Reset Token
|
||||||
window.localStorage.nomadTokenSecret = null;
|
window.localStorage.nomadTokenSecret = null;
|
||||||
});
|
});
|
||||||
@@ -51,20 +51,20 @@ module('Acceptance | policies', function (hooks) {
|
|||||||
test('Modifying an existing policy', async function (assert) {
|
test('Modifying an existing policy', async function (assert) {
|
||||||
allScenarios.policiesTestCluster(server);
|
allScenarios.policiesTestCluster(server);
|
||||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
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');
|
await click('[data-test-policy-row]:first-child a');
|
||||||
// Table sorts by name by default
|
// Table sorts by name by default
|
||||||
let firstPolicy = server.db.policies.sort((a, b) => {
|
let firstPolicy = server.db.policies.sort((a, b) => {
|
||||||
return a.name.localeCompare(b.name);
|
return a.name.localeCompare(b.name);
|
||||||
})[0];
|
})[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-policy-editor]').exists();
|
||||||
assert.dom('[data-test-title]').includesText(firstPolicy.name);
|
assert.dom('[data-test-title]').includesText(firstPolicy.name);
|
||||||
await click('button[data-test-save-policy]');
|
await click('button[data-test-save-policy]');
|
||||||
assert.dom('.flash-message.alert-success').exists();
|
assert.dom('.flash-message.alert-success').exists();
|
||||||
assert.equal(
|
assert.equal(
|
||||||
currentURL(),
|
currentURL(),
|
||||||
`/access-control/policies/${firstPolicy.name}`,
|
`/administration/policies/${firstPolicy.name}`,
|
||||||
'remain on page after save'
|
'remain on page after save'
|
||||||
);
|
);
|
||||||
// Reset Token
|
// Reset Token
|
||||||
@@ -74,9 +74,9 @@ module('Acceptance | policies', function (hooks) {
|
|||||||
test('Creating a test token', async function (assert) {
|
test('Creating a test token', async function (assert) {
|
||||||
allScenarios.policiesTestCluster(server);
|
allScenarios.policiesTestCluster(server);
|
||||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
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"]');
|
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]');
|
await click('[data-test-create-test-token]');
|
||||||
assert.dom('.flash-message.alert-success').exists();
|
assert.dom('.flash-message.alert-success').exists();
|
||||||
assert
|
assert
|
||||||
@@ -100,31 +100,31 @@ module('Acceptance | policies', function (hooks) {
|
|||||||
assert.expect(7);
|
assert.expect(7);
|
||||||
allScenarios.policiesTestCluster(server);
|
allScenarios.policiesTestCluster(server);
|
||||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
||||||
await visit('/access-control/policies');
|
await visit('/administration/policies');
|
||||||
await click('[data-test-create-policy]');
|
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 typeIn('[data-test-policy-name-input]', 'My Fun Policy');
|
||||||
await click('button[data-test-save-policy]');
|
await click('button[data-test-save-policy]');
|
||||||
assert
|
assert
|
||||||
.dom('.flash-message.alert-critical')
|
.dom('.flash-message.alert-critical')
|
||||||
.exists('Doesnt let you save a bad name');
|
.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
|
document.querySelector('[data-test-policy-name-input]').value = ''; // clear
|
||||||
await typeIn('[data-test-policy-name-input]', 'My-Fun-Policy');
|
await typeIn('[data-test-policy-name-input]', 'My-Fun-Policy');
|
||||||
await click('button[data-test-save-policy]');
|
await click('button[data-test-save-policy]');
|
||||||
assert.dom('.flash-message.alert-success').exists();
|
assert.dom('.flash-message.alert-success').exists();
|
||||||
assert.equal(
|
assert.equal(
|
||||||
currentURL(),
|
currentURL(),
|
||||||
'/access-control/policies/My-Fun-Policy',
|
'/administration/policies/My-Fun-Policy',
|
||||||
'redirected to the now-created 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) =>
|
const newPolicy = [...findAll('[data-test-policy-name]')].filter((a) =>
|
||||||
a.textContent.includes('My-Fun-Policy')
|
a.textContent.includes('My-Fun-Policy')
|
||||||
)[0];
|
)[0];
|
||||||
assert.ok(newPolicy, 'Policy is in the list');
|
assert.ok(newPolicy, 'Policy is in the list');
|
||||||
await click(newPolicy);
|
await click(newPolicy);
|
||||||
assert.equal(currentURL(), '/access-control/policies/My-Fun-Policy');
|
assert.equal(currentURL(), '/administration/policies/My-Fun-Policy');
|
||||||
await percySnapshot(assert);
|
await percySnapshot(assert);
|
||||||
// Reset Token
|
// Reset Token
|
||||||
window.localStorage.nomadTokenSecret = null;
|
window.localStorage.nomadTokenSecret = null;
|
||||||
@@ -133,7 +133,7 @@ module('Acceptance | policies', function (hooks) {
|
|||||||
test('Deleting a policy', async function (assert) {
|
test('Deleting a policy', async function (assert) {
|
||||||
allScenarios.policiesTestCluster(server);
|
allScenarios.policiesTestCluster(server);
|
||||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
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) => {
|
let firstPolicy = server.db.policies.sort((a, b) => {
|
||||||
return a.name.localeCompare(b.name);
|
return a.name.localeCompare(b.name);
|
||||||
})[0];
|
})[0];
|
||||||
@@ -143,7 +143,7 @@ module('Acceptance | policies', function (hooks) {
|
|||||||
(row) => row.textContent.includes(firstPolicyName)
|
(row) => row.textContent.includes(firstPolicyName)
|
||||||
)[0];
|
)[0];
|
||||||
await click(firstPolicyLink);
|
await click(firstPolicyLink);
|
||||||
assert.equal(currentURL(), `/access-control/policies/${firstPolicyName}`);
|
assert.equal(currentURL(), `/administration/policies/${firstPolicyName}`);
|
||||||
|
|
||||||
const deleteButton = find('[data-test-delete-policy] button');
|
const deleteButton = find('[data-test-delete-policy] button');
|
||||||
assert.dom(deleteButton).exists('delete button is present');
|
assert.dom(deleteButton).exists('delete button is present');
|
||||||
@@ -154,7 +154,7 @@ module('Acceptance | policies', function (hooks) {
|
|||||||
await click(find('[data-test-confirm-button]'));
|
await click(find('[data-test-confirm-button]'));
|
||||||
|
|
||||||
assert.dom('.flash-message.alert-success').exists();
|
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();
|
assert.dom(`[data-test-policy-name="${firstPolicyName}"]`).doesNotExist();
|
||||||
// Reset Token
|
// Reset Token
|
||||||
window.localStorage.nomadTokenSecret = null;
|
window.localStorage.nomadTokenSecret = null;
|
||||||
@@ -163,7 +163,7 @@ module('Acceptance | policies', function (hooks) {
|
|||||||
test('Policies Index', async function (assert) {
|
test('Policies Index', async function (assert) {
|
||||||
allScenarios.policiesTestCluster(server);
|
allScenarios.policiesTestCluster(server);
|
||||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
||||||
await visit('/access-control/policies');
|
await visit('/administration/policies');
|
||||||
// Table contains every policy in db
|
// Table contains every policy in db
|
||||||
assert
|
assert
|
||||||
.dom('[data-test-policy-row]')
|
.dom('[data-test-policy-row]')
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { setupMirage } from 'ember-cli-mirage/test-support';
|
|||||||
import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
|
import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
|
||||||
import { allScenarios } from '../../mirage/scenarios/default';
|
import { allScenarios } from '../../mirage/scenarios/default';
|
||||||
import Tokens from 'nomad-ui/tests/pages/settings/tokens';
|
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';
|
import percySnapshot from '@percy/ember';
|
||||||
|
|
||||||
module('Acceptance | roles', function (hooks) {
|
module('Acceptance | roles', function (hooks) {
|
||||||
@@ -27,7 +27,7 @@ module('Acceptance | roles', function (hooks) {
|
|||||||
);
|
);
|
||||||
const { secretId } = managementToken;
|
const { secretId } = managementToken;
|
||||||
await Tokens.secret(secretId).submit();
|
await Tokens.secret(secretId).submit();
|
||||||
await AccessControl.visitRoles();
|
await Administration.visitRoles();
|
||||||
});
|
});
|
||||||
|
|
||||||
hooks.afterEach(async function () {
|
hooks.afterEach(async function () {
|
||||||
@@ -39,7 +39,7 @@ module('Acceptance | roles', function (hooks) {
|
|||||||
assert.expect(3);
|
assert.expect(3);
|
||||||
await a11yAudit(assert);
|
await a11yAudit(assert);
|
||||||
|
|
||||||
assert.equal(currentURL(), '/access-control/roles');
|
assert.equal(currentURL(), '/administration/roles');
|
||||||
|
|
||||||
assert
|
assert
|
||||||
.dom('[data-test-role-row]')
|
.dom('[data-test-role-row]')
|
||||||
@@ -78,7 +78,7 @@ module('Acceptance | roles', function (hooks) {
|
|||||||
assert.equal(policiesCellTags[1].textContent.trim(), 'job-reader');
|
assert.equal(policiesCellTags[1].textContent.trim(), 'job-reader');
|
||||||
|
|
||||||
await click(policiesCellTags[0].querySelector('a'));
|
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');
|
assert.dom('[data-test-title]').containsText('client-reader');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ module('Acceptance | roles', function (hooks) {
|
|||||||
assert.expect(8);
|
assert.expect(8);
|
||||||
const role = server.db.roles.findBy((r) => r.name === 'reader');
|
const role = server.db.roles.findBy((r) => r.name === 'reader');
|
||||||
await click('[data-test-role-name="reader"] a');
|
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-name-input]').hasValue(role.name);
|
||||||
assert.dom('[data-test-role-description-input]').hasValue(role.description);
|
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.dom('.flash-message.alert-success').exists();
|
||||||
assert.equal(
|
assert.equal(
|
||||||
currentURL(),
|
currentURL(),
|
||||||
`/access-control/roles/${role.name}`,
|
`/administration/roles/${role.name}`,
|
||||||
'remain on page after save'
|
'remain on page after save'
|
||||||
);
|
);
|
||||||
await percySnapshot(assert);
|
await percySnapshot(assert);
|
||||||
|
|
||||||
// Go back to the roles index
|
// Go back to the roles index
|
||||||
await AccessControl.visitRoles();
|
await Administration.visitRoles();
|
||||||
let readerRoleRow = find('[data-test-role-row="reader-edited"]');
|
let readerRoleRow = find('[data-test-role-row="reader-edited"]');
|
||||||
assert.dom(readerRoleRow).exists();
|
assert.dom(readerRoleRow).exists();
|
||||||
assert.equal(
|
assert.equal(
|
||||||
@@ -119,7 +119,7 @@ module('Acceptance | roles', function (hooks) {
|
|||||||
test('Edit Role: Policies', async function (assert) {
|
test('Edit Role: Policies', async function (assert) {
|
||||||
const role = server.db.roles.findBy((r) => r.name === 'reader');
|
const role = server.db.roles.findBy((r) => r.name === 'reader');
|
||||||
await click('[data-test-role-name="reader"] a');
|
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
|
// Policies table is sortable
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ module('Acceptance | roles', function (hooks) {
|
|||||||
await click('button[data-test-save-role]');
|
await click('button[data-test-save-role]');
|
||||||
assert.dom('.flash-message.alert-success').exists();
|
assert.dom('.flash-message.alert-success').exists();
|
||||||
|
|
||||||
await AccessControl.visitRoles();
|
await Administration.visitRoles();
|
||||||
const readerRoleRow = find('[data-test-role-row="reader"]');
|
const readerRoleRow = find('[data-test-role-row="reader"]');
|
||||||
const readerRolePolicies = readerRoleRow
|
const readerRolePolicies = readerRoleRow
|
||||||
.querySelector('[data-test-role-policies]')
|
.querySelector('[data-test-role-policies]')
|
||||||
@@ -219,7 +219,7 @@ module('Acceptance | roles', function (hooks) {
|
|||||||
const role = server.db.roles.findBy((r) => r.name === 'reader');
|
const role = server.db.roles.findBy((r) => r.name === 'reader');
|
||||||
|
|
||||||
await click('[data-test-role-name="reader"] a');
|
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();
|
assert.dom('table.tokens').exists();
|
||||||
|
|
||||||
// "Reader" role has a single token with it applied by default
|
// "Reader" role has a single token with it applied by default
|
||||||
@@ -243,7 +243,7 @@ module('Acceptance | roles', function (hooks) {
|
|||||||
|
|
||||||
await percySnapshot(assert);
|
await percySnapshot(assert);
|
||||||
|
|
||||||
await AccessControl.visitTokens();
|
await Administration.visitTokens();
|
||||||
assert
|
assert
|
||||||
.dom('[data-test-token-name="Example Token for reader"]')
|
.dom('[data-test-token-name="Example Token for reader"]')
|
||||||
.exists(
|
.exists(
|
||||||
@@ -254,7 +254,7 @@ module('Acceptance | roles', function (hooks) {
|
|||||||
test('Edit Role: Deletion', async function (assert) {
|
test('Edit Role: Deletion', async function (assert) {
|
||||||
const role = server.db.roles.findBy((r) => r.name === 'reader');
|
const role = server.db.roles.findBy((r) => r.name === 'reader');
|
||||||
await click('[data-test-role-name="reader"] a');
|
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');
|
const deleteButton = find('[data-test-delete-role] button');
|
||||||
assert.dom(deleteButton).exists('delete button is present');
|
assert.dom(deleteButton).exists('delete button is present');
|
||||||
await click(deleteButton);
|
await click(deleteButton);
|
||||||
@@ -263,12 +263,12 @@ module('Acceptance | roles', function (hooks) {
|
|||||||
.exists('confirmation message is present');
|
.exists('confirmation message is present');
|
||||||
await click(find('[data-test-confirm-button]'));
|
await click(find('[data-test-confirm-button]'));
|
||||||
assert.dom('.flash-message.alert-success').exists();
|
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();
|
assert.dom('[data-test-role-row="reader"]').doesNotExist();
|
||||||
});
|
});
|
||||||
test('New Role', async function (assert) {
|
test('New Role', async function (assert) {
|
||||||
await click('[data-test-create-role]');
|
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 fillIn('[data-test-role-name-input]', 'test-role');
|
||||||
await click('button[data-test-save-role]');
|
await click('button[data-test-save-role]');
|
||||||
assert
|
assert
|
||||||
@@ -279,19 +279,19 @@ module('Acceptance | roles', function (hooks) {
|
|||||||
await click('[data-test-role-policies] tbody tr input');
|
await click('[data-test-role-policies] tbody tr input');
|
||||||
await click('button[data-test-save-role]');
|
await click('button[data-test-save-role]');
|
||||||
assert.dom('.flash-message.alert-success').exists();
|
assert.dom('.flash-message.alert-success').exists();
|
||||||
assert.equal(currentURL(), '/access-control/roles/1'); // default id created via mirage
|
assert.equal(currentURL(), '/administration/roles/1'); // default id created via mirage
|
||||||
await AccessControl.visitRoles();
|
await Administration.visitRoles();
|
||||||
assert.dom('[data-test-role-row="test-role"]').exists();
|
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.
|
// 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]');
|
const policyRows = findAll('[data-test-policy-row]');
|
||||||
for (const row of policyRows) {
|
for (const row of policyRows) {
|
||||||
const deleteButton = row.querySelector('[data-test-delete-policy]');
|
const deleteButton = row.querySelector('[data-test-delete-policy]');
|
||||||
await click(deleteButton);
|
await click(deleteButton);
|
||||||
}
|
}
|
||||||
assert.dom('[data-test-empty-policies-list-headline]').exists();
|
assert.dom('[data-test-empty-policies-list-headline]').exists();
|
||||||
await AccessControl.visitRoles();
|
await Administration.visitRoles();
|
||||||
await click('[data-test-create-role]');
|
await click('[data-test-create-role]');
|
||||||
assert.dom('.empty-message').exists();
|
assert.dom('.empty-message').exists();
|
||||||
assert
|
assert
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import Jobs from 'nomad-ui/tests/pages/jobs/list';
|
|||||||
import JobDetail from 'nomad-ui/tests/pages/jobs/detail';
|
import JobDetail from 'nomad-ui/tests/pages/jobs/detail';
|
||||||
import ClientDetail from 'nomad-ui/tests/pages/clients/detail';
|
import ClientDetail from 'nomad-ui/tests/pages/clients/detail';
|
||||||
import Layout from 'nomad-ui/tests/pages/layout';
|
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 percySnapshot from '@percy/ember';
|
||||||
import faker from 'nomad-ui/mirage/faker';
|
import faker from 'nomad-ui/mirage/faker';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
@@ -621,7 +621,7 @@ module('Acceptance | tokens', function (hooks) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
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();
|
assert.dom('[data-test-policy-total-tokens]').exists();
|
||||||
const expectedFirstPolicyTokens = server.db.tokens.filter((token) => {
|
const expectedFirstPolicyTokens = server.db.tokens.filter((token) => {
|
||||||
return token.policyIds.includes(firstPolicy.name);
|
return token.policyIds.includes(firstPolicy.name);
|
||||||
@@ -648,9 +648,9 @@ module('Acceptance | tokens', function (hooks) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
||||||
await visit('/access-control/policies');
|
await visit('/administration/policies');
|
||||||
await click('[data-test-policy-name]');
|
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) => {
|
const expectedFirstPolicyTokens = server.db.tokens.filter((token) => {
|
||||||
return token.policyIds.includes(firstPolicy.name);
|
return token.policyIds.includes(firstPolicy.name);
|
||||||
@@ -692,10 +692,10 @@ module('Acceptance | tokens', function (hooks) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
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');
|
await click('[data-test-policy-name]:first-child');
|
||||||
assert.equal(currentURL(), `/access-control/policies/${testPolicy.name}`);
|
assert.equal(currentURL(), `/administration/policies/${testPolicy.name}`);
|
||||||
assert
|
assert
|
||||||
.dom('[data-test-policy-token-row]')
|
.dom('[data-test-policy-token-row]')
|
||||||
.exists(
|
.exists(
|
||||||
@@ -730,10 +730,10 @@ module('Acceptance | tokens', function (hooks) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
||||||
await visit('/access-control/policies');
|
await visit('/administration/policies');
|
||||||
|
|
||||||
await click('[data-test-policy-name]');
|
await click('[data-test-policy-name]');
|
||||||
assert.equal(currentURL(), `/access-control/policies/${testPolicy.name}`);
|
assert.equal(currentURL(), `/administration/policies/${testPolicy.name}`);
|
||||||
|
|
||||||
assert
|
assert
|
||||||
.dom('[data-test-policy-token-row]')
|
.dom('[data-test-policy-token-row]')
|
||||||
@@ -885,7 +885,7 @@ module('Acceptance | tokens', function (hooks) {
|
|||||||
);
|
);
|
||||||
const { secretId } = managementToken;
|
const { secretId } = managementToken;
|
||||||
await Tokens.secret(secretId).submit();
|
await Tokens.secret(secretId).submit();
|
||||||
await AccessControl.visitTokens();
|
await Administration.visitTokens();
|
||||||
});
|
});
|
||||||
|
|
||||||
hooks.afterEach(async function () {
|
hooks.afterEach(async function () {
|
||||||
@@ -894,7 +894,7 @@ module('Acceptance | tokens', function (hooks) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Tokens index, general', async function (assert) {
|
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
|
// Number of token rows equivalent to number in db
|
||||||
assert
|
assert
|
||||||
.dom('[data-test-token-row]')
|
.dom('[data-test-token-row]')
|
||||||
@@ -1002,7 +1002,7 @@ module('Acceptance | tokens', function (hooks) {
|
|||||||
(row) => row.textContent.includes(tokenToClick.name)
|
(row) => row.textContent.includes(tokenToClick.name)
|
||||||
);
|
);
|
||||||
await click(tokenRowToClick.querySelector('[data-test-token-name] a'));
|
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);
|
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) {
|
test('Token page, general', async function (assert) {
|
||||||
const token = server.db.tokens.findBy((t) => t.id === 'cl4y-t0k3n');
|
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-name-input]').hasValue(token.name);
|
||||||
assert.dom('[data-test-token-accessor]').hasValue(token.accessorId);
|
assert.dom('[data-test-token-accessor]').hasValue(token.accessorId);
|
||||||
assert.dom('[data-test-token-secret]').hasValue(token.secretId);
|
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) {
|
test('Token name can be edited', async function (assert) {
|
||||||
const token = server.db.tokens.findBy((t) => t.id === 'cl4y-t0k3n');
|
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-name-input]').hasValue(token.name);
|
||||||
await fillIn('[data-test-token-name-input]', 'Mud-Token');
|
await fillIn('[data-test-token-name-input]', 'Mud-Token');
|
||||||
await click('[data-test-token-save]');
|
await click('[data-test-token-save]');
|
||||||
assert.dom('.flash-message.alert-success').exists();
|
assert.dom('.flash-message.alert-success').exists();
|
||||||
await AccessControl.visitTokens();
|
await Administration.visitTokens();
|
||||||
assert.dom('[data-test-token-name="Mud-Token"]').exists({ count: 1 });
|
assert.dom('[data-test-token-name="Mud-Token"]').exists({ count: 1 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Token policies and roles can be edited', async function (assert) {
|
test('Token policies and roles can be edited', async function (assert) {
|
||||||
const token = server.db.tokens.findBy((t) => t.id === 'cl4y-t0k3n');
|
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
|
// The policies/roles belonging to this token are checked
|
||||||
const tokenPolicies = token.policyIds;
|
const tokenPolicies = token.policyIds;
|
||||||
@@ -1199,7 +1199,7 @@ module('Acceptance | tokens', function (hooks) {
|
|||||||
|
|
||||||
await percySnapshot(assert);
|
await percySnapshot(assert);
|
||||||
|
|
||||||
await AccessControl.visitTokens();
|
await Administration.visitTokens();
|
||||||
// Policies cell for our clay token should read "No Policies"
|
// Policies cell for our clay token should read "No Policies"
|
||||||
const clayToken = server.db.tokens.findBy((t) => t.id === 'cl4y-t0k3n');
|
const clayToken = server.db.tokens.findBy((t) => t.id === 'cl4y-t0k3n');
|
||||||
const clayTokenRow = [...findAll('[data-test-token-row]')].find((row) =>
|
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) {
|
test('Token can be deleted', async function (assert) {
|
||||||
const token = server.db.tokens.findBy((t) => t.id === 'cl4y-t0k3n');
|
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');
|
const deleteButton = find('[data-test-delete-token] button');
|
||||||
assert.dom(deleteButton).exists('delete button is present');
|
assert.dom(deleteButton).exists('delete button is present');
|
||||||
@@ -1231,16 +1231,16 @@ module('Acceptance | tokens', function (hooks) {
|
|||||||
await click(find('[data-test-confirm-button]'));
|
await click(find('[data-test-confirm-button]'));
|
||||||
|
|
||||||
assert.dom('.flash-message.alert-success').exists();
|
assert.dom('.flash-message.alert-success').exists();
|
||||||
await AccessControl.visitTokens();
|
await Administration.visitTokens();
|
||||||
assert.dom('[data-test-token-name="cl4y-t0k3n"]').doesNotExist();
|
assert.dom('[data-test-token-name="cl4y-t0k3n"]').doesNotExist();
|
||||||
});
|
});
|
||||||
test('New Token creation', async function (assert) {
|
test('New Token creation', async function (assert) {
|
||||||
await click('[data-test-create-token]');
|
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 fillIn('[data-test-token-name-input]', 'Timeless Token');
|
||||||
await click('[data-test-token-save]');
|
await click('[data-test-token-save]');
|
||||||
assert.dom('.flash-message.alert-success').exists();
|
assert.dom('.flash-message.alert-success').exists();
|
||||||
await AccessControl.visitTokens();
|
await Administration.visitTokens();
|
||||||
assert
|
assert
|
||||||
.dom('[data-test-token-name="Timeless Token"]')
|
.dom('[data-test-token-name="Timeless Token"]')
|
||||||
.exists({ count: 1 });
|
.exists({ count: 1 });
|
||||||
@@ -1254,13 +1254,13 @@ module('Acceptance | tokens', function (hooks) {
|
|||||||
|
|
||||||
// Now create one with a TTL
|
// Now create one with a TTL
|
||||||
await click('[data-test-create-token]');
|
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');
|
await fillIn('[data-test-token-name-input]', 'TTL Token');
|
||||||
// Select the "8 hours" radio within the .expiration-time div
|
// Select the "8 hours" radio within the .expiration-time div
|
||||||
await click('.expiration-time input[value="8h"]');
|
await click('.expiration-time input[value="8h"]');
|
||||||
await click('[data-test-token-save]');
|
await click('[data-test-token-save]');
|
||||||
assert.dom('.flash-message.alert-success').exists();
|
assert.dom('.flash-message.alert-success').exists();
|
||||||
await AccessControl.visitTokens();
|
await Administration.visitTokens();
|
||||||
assert.dom('[data-test-token-name="TTL Token"]').exists({ count: 1 });
|
assert.dom('[data-test-token-name="TTL Token"]').exists({ count: 1 });
|
||||||
const ttlTokenRow = [...findAll('[data-test-token-row]')].find((row) =>
|
const ttlTokenRow = [...findAll('[data-test-token-row]')].find((row) =>
|
||||||
row.textContent.includes('TTL Token')
|
row.textContent.includes('TTL Token')
|
||||||
@@ -1272,7 +1272,7 @@ module('Acceptance | tokens', function (hooks) {
|
|||||||
|
|
||||||
// Now create one with an expiration time
|
// Now create one with an expiration time
|
||||||
await click('[data-test-create-token]');
|
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');
|
await fillIn('[data-test-token-name-input]', 'Expiring Token');
|
||||||
// select the Custom radio button
|
// select the Custom radio button
|
||||||
await click('.expiration-time input[value="custom"]');
|
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 fillIn('[data-test-token-expiration-time-input]', soonString);
|
||||||
await click('[data-test-token-save]');
|
await click('[data-test-token-save]');
|
||||||
assert.dom('.flash-message.alert-success').exists();
|
assert.dom('.flash-message.alert-success').exists();
|
||||||
await AccessControl.visitTokens();
|
await Administration.visitTokens();
|
||||||
assert
|
assert
|
||||||
.dom('[data-test-token-name="Expiring Token"]')
|
.dom('[data-test-token-name="Expiring Token"]')
|
||||||
.exists({ count: 1 });
|
.exists({ count: 1 });
|
||||||
|
|||||||
@@ -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'),
|
|
||||||
});
|
|
||||||
14
ui/tests/pages/administration.js
Normal file
14
ui/tests/pages/administration.js
Normal file
@@ -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'),
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user