* Rename pages to include roles * Models and adapters * [ui] Any policy checks in the UI now check for roles' policies as well as token policies (#18346) * combinedPolicies as a concept * Classic decorator on role adapter * We added a new request for roles, so the test based on a specific order of requests got fickle fast * Mirage roles cluster scaffolded * Acceptance test for roles and policies on the login page * Update mirage mock for nodes fetch to account for role policies / empty token.policies * Roles-derived policies checks * [ui] Access Control with Roles and Tokens (#18413) * top level policies routes moved into access control * A few more routes and name cleanup * Delog and test fixes to account for new url prefix and document titles * Overview page * Tokens and Roles routes * Tokens helios table * Add a role * Hacky role page and deletion * New policy keyboard shortcut and roles breadcrumb nav * If you leave New Role but havent made any changes, remove the newly-created record from store * Roles index list and general role route crud * Roles index actually links to roles now * Helios button styles for new roles and policies * Handle when you try to create a new role without having any policies * Token editing generally * Create Token functionality * Cant delete self-token but management token editing and deleting is fine * Upgrading helios caused codemirror to explode, shimmed * Policies table fix * without bang-element condition, modifier would refire over and over * Token TTL or Time setting * time will take you on * Mirage hooks for create and list roles * Ensure policy names only use allow characters in mirage mocks * Mirage mocked roles and policies in the default cluster * log and lintfix * chromedriver to 2.1.2 * unused unit tests removed * Nice profile dropdown * With the HDS accordion, rename our internal component scss ref * design revisions after discussion * Tooltip on deleted-policy tokens * Two-step button peripheral isDeleting gcode removed * Never to null on token save * copywrite headers added and empty routefiles removed * acceptance test fixes for policies endpoint * Route for updating a token * Policies testfixes * Ember on-click-outside modifier upgraded with general ember-modifier upgrade * Test adjustments to account for new profile header dropdown * Test adjustments for tokens via policy pages * Removed an unused route * Access Control index page tests * a11y tests * Tokens index acceptance tests generally * Lintfix * Token edit page tests * Token editing tests * New token expiration tests * Roles Index tests * Role editing policies tests * A complete set of Access Control Roles tests * Policies test * Be more specific about which row to check for expiration time * Nil check on expirationTime equality * Management tokens shouldnt show No Roles/Policies, give them their own designation * Route guard on selftoken, conditional columns, and afterModel at parent to prevent orphaned policies on tokens/roles from stopping a new save * Policy unloading on delete and other todos plus autofocus conditionally re-enabled * Invalid policies non-links now a concept for Roles index * HDS style links to make job.variables.alert links look like links again * Mirage finding looks weird so making model async in hash even though redundant * Drop rsvp * RSVP wasnt the problem, cached lookups were * remove old todo comments * de-log
This commit is contained in:
parent
3cc387749e
commit
bfba4f5e13
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:improvement
|
||||||
|
ui: observe a token's roles' rules in the UI and add an interface for managing tokens, roles, and policies
|
||||||
|
```
|
|
@ -363,9 +363,12 @@ func (a *ACLToken) Validate(minTTL, maxTTL time.Duration, existing *ACLToken) er
|
||||||
if existing.ExpirationTTL != a.ExpirationTTL {
|
if existing.ExpirationTTL != a.ExpirationTTL {
|
||||||
mErr.Errors = append(mErr.Errors, errors.New("cannot update expiration TTL"))
|
mErr.Errors = append(mErr.Errors, errors.New("cannot update expiration TTL"))
|
||||||
}
|
}
|
||||||
if existing.ExpirationTime != a.ExpirationTime {
|
if a.ExpirationTime != nil {
|
||||||
mErr.Errors = append(mErr.Errors, errors.New("cannot update expiration time"))
|
if !existing.ExpirationTime.Equal(*a.ExpirationTime) {
|
||||||
|
mErr.Errors = append(mErr.Errors, errors.New("cannot update expiration time"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return mErr.ErrorOrNil()
|
return mErr.ErrorOrNil()
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import AbstractAbility from './abstract';
|
||||||
|
import { alias } from '@ember/object/computed';
|
||||||
|
import classic from 'ember-classic-decorator';
|
||||||
|
|
||||||
|
@classic
|
||||||
|
export default class Role extends AbstractAbility {
|
||||||
|
@alias('selfTokenIsManagement') canRead;
|
||||||
|
@alias('selfTokenIsManagement') canList;
|
||||||
|
@alias('selfTokenIsManagement') canWrite;
|
||||||
|
@alias('selfTokenIsManagement') canUpdate;
|
||||||
|
@alias('selfTokenIsManagement') canDestroy;
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: MPL-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
// @ts-check
|
||||||
|
import { default as ApplicationAdapter, namespace } from './application';
|
||||||
|
import classic from 'ember-classic-decorator';
|
||||||
|
import { singularize } from 'ember-inflector';
|
||||||
|
@classic
|
||||||
|
export default class RoleAdapter extends ApplicationAdapter {
|
||||||
|
namespace = namespace + '/acl';
|
||||||
|
|
||||||
|
urlForCreateRecord(modelName) {
|
||||||
|
let baseUrl = this.buildURL(modelName);
|
||||||
|
return singularize(baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
urlForDeleteRecord(id) {
|
||||||
|
return this.urlForUpdateRecord(id, 'role');
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,9 +15,22 @@ export default class TokenAdapter extends ApplicationAdapter {
|
||||||
|
|
||||||
namespace = namespace + '/acl';
|
namespace = namespace + '/acl';
|
||||||
|
|
||||||
|
methodForRequest(params) {
|
||||||
|
if (params.requestType === 'updateRecord') {
|
||||||
|
return 'POST';
|
||||||
|
}
|
||||||
|
return super.methodForRequest(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRecord(store, type, snapshot) {
|
||||||
|
let data = this.serialize(snapshot);
|
||||||
|
return this.ajax(`${this.buildURL()}/token/${snapshot.id}`, 'POST', {
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
createRecord(_store, type, snapshot) {
|
createRecord(_store, type, snapshot) {
|
||||||
let data = this.serialize(snapshot);
|
let data = this.serialize(snapshot);
|
||||||
data.Policies = data.PolicyIDs;
|
|
||||||
return this.ajax(`${this.buildURL()}/token`, 'POST', { data });
|
return this.ajax(`${this.buildURL()}/token`, 'POST', { data });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Component from '@ember/component';
|
||||||
|
import { tagName } from '@ember-decorators/component';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
|
@tagName('')
|
||||||
|
export default class AccessControlSubnav extends Component {
|
||||||
|
@service keyboard;
|
||||||
|
}
|
|
@ -7,9 +7,9 @@
|
||||||
{{#if (can "write variable")}}
|
{{#if (can "write variable")}}
|
||||||
{{#with (editable-variable-link @path existingPaths=@existingPaths namespace=@namespace) as |link|}}
|
{{#with (editable-variable-link @path existingPaths=@existingPaths namespace=@namespace) as |link|}}
|
||||||
{{#if link.model}}
|
{{#if link.model}}
|
||||||
<LinkTo @route={{link.route}} @model={{link.model}} @query={{link.query}}>{{@path}}</LinkTo>
|
<Hds::Link::Inline @route={{link.route}} @model={{link.model}} @query={{link.query}}>{{@path}}</Hds::Link::Inline>
|
||||||
{{else}}
|
{{else}}
|
||||||
<LinkTo @route={{link.route}} @query={{link.query}}>{{@path}}</LinkTo>
|
<Hds::Link::Inline @route={{link.route}} @query={{link.query}}>{{@path}}</Hds::Link::Inline>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/with}}
|
{{/with}}
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
@type="text"
|
@type="text"
|
||||||
@value={{@policy.name}}
|
@value={{@policy.name}}
|
||||||
class="input"
|
class="input"
|
||||||
{{autofocus}}
|
{{autofocus}}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
@ -34,7 +34,7 @@
|
||||||
mode="ruby"
|
mode="ruby"
|
||||||
content=@policy.rules
|
content=@policy.rules
|
||||||
onUpdate=this.updatePolicyRules
|
onUpdate=this.updatePolicyRules
|
||||||
autofocus=(not @policy.isNew)
|
autofocus=false
|
||||||
extraKeys=(hash Cmd-Enter=this.save)
|
extraKeys=(hash Cmd-Enter=this.save)
|
||||||
}} />
|
}} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -55,12 +55,12 @@
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
{{#if (can "update policy")}}
|
{{#if (can "update policy")}}
|
||||||
<button
|
<Hds::Button
|
||||||
class="button is-primary"
|
@text="Save Policy"
|
||||||
type="submit"
|
@type="submit"
|
||||||
>
|
data-test-save-policy
|
||||||
Save Policy
|
{{on "click" this.save}}
|
||||||
</button>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</footer>
|
</footer>
|
||||||
</form>
|
</form>
|
|
@ -30,20 +30,22 @@ export default class PolicyEditorComponent extends Component {
|
||||||
`Policy name must be 1-128 characters long and can only contain letters, numbers, and dashes.`
|
`Policy name must be 1-128 characters long and can only contain letters, numbers, and dashes.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldRedirectAfterSave = this.policy.isNew;
|
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 (
|
if (
|
||||||
this.policy.isNew &&
|
this.policy.isNew &&
|
||||||
this.store.peekRecord('policy', this.policy.name)
|
this.store
|
||||||
|
.peekAll('policy')
|
||||||
|
.filter((policy) => policy !== this.policy)
|
||||||
|
.findBy('name', this.policy.name)
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`A policy with name ${this.policy.name} already exists.`
|
`A policy with name ${this.policy.name} already exists.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
this.policy.set('id', this.policy.name);
|
||||||
this.policy.id = this.policy.name;
|
|
||||||
|
|
||||||
await this.policy.save();
|
await this.policy.save();
|
||||||
|
|
||||||
this.notifications.add({
|
this.notifications.add({
|
||||||
|
@ -52,7 +54,10 @@ export default class PolicyEditorComponent extends Component {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (shouldRedirectAfterSave) {
|
if (shouldRedirectAfterSave) {
|
||||||
this.router.transitionTo('policies.policy', this.policy.id);
|
this.router.transitionTo(
|
||||||
|
'access-control.policies.policy',
|
||||||
|
this.policy.id
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.notifications.add({
|
this.notifications.add({
|
||||||
|
|
|
@ -4,22 +4,16 @@
|
||||||
~}}
|
~}}
|
||||||
|
|
||||||
{{#if this.token.selfToken}}
|
{{#if this.token.selfToken}}
|
||||||
<PowerSelect
|
<Hds::Dropdown @color="secondary" class="profile-dropdown"
|
||||||
data-test-header-profile-dropdown
|
{{keyboard-shortcut menuLevel=true pattern=(array "g" "p") }}
|
||||||
{{keyboard-shortcut menuLevel=true pattern=(array "g" "p") }}
|
as |dd|>
|
||||||
@options={{this.profileOptions}}
|
<dd.ToggleIcon @color="secondary" @icon="user-circle" @text="user menu" @size="small" data-test-header-profile-dropdown />
|
||||||
@onChange={{action (queue
|
<dd.Title @text="Signed In" />
|
||||||
(fn (mut this.profileSelection))
|
<dd.Description @text={{this.token.selfToken.name}} />
|
||||||
this.profileSelection.action
|
<dd.Separator />
|
||||||
)}}
|
<dd.Interactive @route="settings.tokens" @text="Profile" data-test-profile-dropdown-profile-link />
|
||||||
@dropdownClass="dropdown-options"
|
<dd.Interactive {{on "click" this.signOut}} @text="Sign Out" @color="critical" data-test-profile-dropdown-sign-out-link />
|
||||||
@matchTriggerWidth={{false}}
|
</Hds::Dropdown>
|
||||||
@selected={{get this.profileSelection "key"}}
|
|
||||||
class="profile-dropdown navbar-item"
|
|
||||||
as |option|>
|
|
||||||
<span class="ember-power-select-prefix">Profile</span>
|
|
||||||
<span class="dropdown-label" data-test-dropdown-option={{option.key}}>{{option.label}}</span>
|
|
||||||
</PowerSelect>
|
|
||||||
{{else}}
|
{{else}}
|
||||||
<LinkTo data-test-header-signin-link @route="settings.tokens" class="navbar-item" {{keyboard-shortcut menuLevel=true pattern=(array "g" "p") }}>
|
<LinkTo data-test-header-signin-link @route="settings.tokens" class="navbar-item" {{keyboard-shortcut menuLevel=true pattern=(array "g" "p") }}>
|
||||||
Sign In
|
Sign In
|
||||||
|
|
|
@ -4,38 +4,24 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
import Component from '@glimmer/component';
|
import Component from '@glimmer/component';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
|
||||||
export default class ProfileNavbarItemComponent extends Component {
|
export default class ProfileNavbarItemComponent extends Component {
|
||||||
@service token;
|
@service token;
|
||||||
@service router;
|
@service router;
|
||||||
@service store;
|
@service store;
|
||||||
|
|
||||||
profileOptions = [
|
@action
|
||||||
{
|
signOut() {
|
||||||
label: 'Authorization',
|
this.token.setProperties({
|
||||||
key: 'authorization',
|
secret: undefined,
|
||||||
action: () => {
|
});
|
||||||
this.router.transitionTo('settings.tokens');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Sign Out',
|
|
||||||
key: 'sign-out',
|
|
||||||
action: () => {
|
|
||||||
this.token.setProperties({
|
|
||||||
secret: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear out all data to ensure only data the anonymous token is privileged to see is shown
|
// Clear out all data to ensure only data the anonymous token is privileged to see is shown
|
||||||
this.store.unloadAll();
|
this.store.unloadAll();
|
||||||
this.token.reset();
|
this.token.reset();
|
||||||
this.router.transitionTo('jobs.index');
|
this.router.transitionTo('jobs.index');
|
||||||
},
|
}
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
profileSelection = this.profileOptions[0];
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
{{!
|
||||||
|
Copyright (c) HashiCorp, Inc.
|
||||||
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
|
~}}
|
||||||
|
|
||||||
|
<form class="acl-form" autocomplete="off" {{on "submit" this.save}}>
|
||||||
|
<label>
|
||||||
|
<span>
|
||||||
|
Role Name
|
||||||
|
</span>
|
||||||
|
<Input
|
||||||
|
data-test-role-name-input
|
||||||
|
@type="text"
|
||||||
|
@value={{@role.name}}
|
||||||
|
class="input"
|
||||||
|
{{autofocus ignore=(not @role.isNew)}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
<span>
|
||||||
|
Description (optional)
|
||||||
|
</span>
|
||||||
|
<Input
|
||||||
|
data-test-role-description-input
|
||||||
|
@value={{@role.description}}
|
||||||
|
class="input"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
Policies
|
||||||
|
</label>
|
||||||
|
<Hds::Table @caption="A list of policies available to this role" class="acl-table"
|
||||||
|
@model={{@policies}}
|
||||||
|
@columns={{array
|
||||||
|
(hash key="selected" width="80px")
|
||||||
|
(hash key="name" label="Name" isSortable=true)
|
||||||
|
(hash key="description" label="Description")
|
||||||
|
(hash key="definition" label="View Policy Definition")
|
||||||
|
}}
|
||||||
|
@sortBy="name"
|
||||||
|
data-test-role-policies
|
||||||
|
>
|
||||||
|
<:body as |B|>
|
||||||
|
<B.Tr>
|
||||||
|
<B.Td class="selection-checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox"
|
||||||
|
checked={{find-by "name" B.data.name @role.policies}}
|
||||||
|
{{on "change" (action this.updateRolePolicies B.data)}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</B.Td>
|
||||||
|
<B.Td data-test-policy-name>{{B.data.name}}</B.Td>
|
||||||
|
<B.Td>{{B.data.description}}</B.Td>
|
||||||
|
<B.Td>
|
||||||
|
<LinkTo @route="access-control.policies.policy" @model={{B.data.name}}>
|
||||||
|
View Policy Definition
|
||||||
|
</LinkTo>
|
||||||
|
</B.Td>
|
||||||
|
</B.Tr>
|
||||||
|
</:body>
|
||||||
|
</Hds::Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
{{#if (can "update role")}}
|
||||||
|
<Hds::Button @text="Save Role" @color="primary"
|
||||||
|
{{on "click" this.save}}
|
||||||
|
data-test-save-role
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
</footer>
|
||||||
|
</form>
|
|
@ -0,0 +1,77 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { alias } from '@ember/object/computed';
|
||||||
|
import Component from '@glimmer/component';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
|
||||||
|
export default class RoleEditorComponent extends Component {
|
||||||
|
@service notifications;
|
||||||
|
@service router;
|
||||||
|
@service store;
|
||||||
|
|
||||||
|
@alias('args.role') role;
|
||||||
|
|
||||||
|
@tracked rolePolicies = [];
|
||||||
|
|
||||||
|
// when this renders, set up rolePOlicies
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.rolePolicies = this.role.policies.toArray() || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
@action updateRolePolicies(policy, event) {
|
||||||
|
let { checked } = event.target;
|
||||||
|
if (checked) {
|
||||||
|
this.rolePolicies.push(policy);
|
||||||
|
} else {
|
||||||
|
this.rolePolicies = this.rolePolicies.filter((p) => p !== policy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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.role.name?.match(nameRegex)) {
|
||||||
|
throw new Error(
|
||||||
|
`Role name must be 1-128 characters long and can only contain letters, numbers, and dashes.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldRedirectAfterSave = this.role.isNew;
|
||||||
|
|
||||||
|
if (this.role.isNew && this.store.peekRecord('role', this.role.name)) {
|
||||||
|
throw new Error(`A role with name ${this.role.name} already exists.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.role.policies = this.rolePolicies;
|
||||||
|
|
||||||
|
await this.role.save();
|
||||||
|
|
||||||
|
this.notifications.add({
|
||||||
|
title: 'Role Saved',
|
||||||
|
color: 'success',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (shouldRedirectAfterSave) {
|
||||||
|
this.router.transitionTo('access-control.roles.role', this.role.id);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.notifications.add({
|
||||||
|
title: `Error creating Role ${this.role.name}`,
|
||||||
|
message: error,
|
||||||
|
color: 'critical',
|
||||||
|
sticky: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,239 @@
|
||||||
|
{{!
|
||||||
|
Copyright (c) HashiCorp, Inc.
|
||||||
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
|
~}}
|
||||||
|
|
||||||
|
<form class="acl-form" autocomplete="off" {{on "submit" this.save}}>
|
||||||
|
<label>
|
||||||
|
<Hds::Form::Label>
|
||||||
|
Token Name
|
||||||
|
</Hds::Form::Label>
|
||||||
|
<Input
|
||||||
|
data-test-token-name-input
|
||||||
|
@type="text"
|
||||||
|
@value={{@token.name}}
|
||||||
|
class="input"
|
||||||
|
{{autofocus ignore=(not @token.isNew)}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="expiration-time">
|
||||||
|
{{#if @token.isNew}}
|
||||||
|
<Hds::Form::Label>
|
||||||
|
Expiration time
|
||||||
|
</Hds::Form::Label>
|
||||||
|
|
||||||
|
{{!-- Radio to select between 1, 4, 8, 24, or never --}}
|
||||||
|
<Hds::Form::Radio::Group @layout="horizontal" @name="expiration-time" {{on "change" this.updateTokenExpirationTTL}} as |G|>
|
||||||
|
<G.Radio::Field
|
||||||
|
@id="10m"
|
||||||
|
@value="10m"
|
||||||
|
as |F|>
|
||||||
|
<F.Label>10 minutes</F.Label>
|
||||||
|
</G.Radio::Field>
|
||||||
|
<G.Radio::Field
|
||||||
|
@id="8h"
|
||||||
|
@value="8h"
|
||||||
|
as |F|>
|
||||||
|
<F.Label>8 hours</F.Label>
|
||||||
|
</G.Radio::Field>
|
||||||
|
<G.Radio::Field
|
||||||
|
@id="24h"
|
||||||
|
@value="24h"
|
||||||
|
as |F|>
|
||||||
|
<F.Label>24 hours</F.Label>
|
||||||
|
</G.Radio::Field>
|
||||||
|
<G.Radio::Field
|
||||||
|
@id="never"
|
||||||
|
@value="never"
|
||||||
|
checked={{eq @token.expirationTTL "never"}}
|
||||||
|
as |F|>
|
||||||
|
<F.Label>Never</F.Label>
|
||||||
|
</G.Radio::Field>
|
||||||
|
<G.Radio::Field
|
||||||
|
@id="custom"
|
||||||
|
@value="custom"
|
||||||
|
as |F|>
|
||||||
|
<F.Label>Custom</F.Label>
|
||||||
|
</G.Radio::Field>
|
||||||
|
</Hds::Form::Radio::Group>
|
||||||
|
|
||||||
|
{{#if @token.expirationTime}}
|
||||||
|
<Hds::Form::TextInput::Field
|
||||||
|
data-test-token-expiration-time-input
|
||||||
|
@type="datetime-local"
|
||||||
|
@id="token-expiration-time"
|
||||||
|
{{on "change" this.updateTokenExpirationTime}}>
|
||||||
|
</Hds::Form::TextInput::Field>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{else}}
|
||||||
|
<Hds::Form::Label>
|
||||||
|
{{#if @token.expirationTime}}
|
||||||
|
Token {{#if @token.isExpired}}expired{{else}}expires{{/if}}
|
||||||
|
<Tooltip @text={{@token.expirationTime}} @isFullText={{true}}>
|
||||||
|
<span data-test-token-expiration-time class="{{if @token.isExpired "has-text-danger"}}">{{moment-from-now @token.expirationTime interval=1000}}</span>
|
||||||
|
</Tooltip>
|
||||||
|
{{else}}
|
||||||
|
Token never expires
|
||||||
|
{{/if}}
|
||||||
|
</Hds::Form::Label>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#unless @token.isNew}}
|
||||||
|
<div>
|
||||||
|
<Hds::Form::MaskedInput::Field @isMasked={{false}} @hasCopyButton={{true}} @value={{@token.accessor}} readonly data-test-token-accessor as |F|>
|
||||||
|
<F.Label>Token Accessor</F.Label>
|
||||||
|
</Hds::Form::MaskedInput::Field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Hds::Form::MaskedInput::Field @hasCopyButton={{true}} @value={{@token.secret}} readonly data-test-token-secret as |F|>
|
||||||
|
<F.Label>Token Secret</F.Label>
|
||||||
|
</Hds::Form::MaskedInput::Field>
|
||||||
|
</div>
|
||||||
|
{{/unless}}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Hds::Form::Radio::Group @layout="horizontal" @name="method-demo1" {{on "change" this.updateTokenType}} as |G|>
|
||||||
|
<G.Legend>Client or Management token?</G.Legend>
|
||||||
|
<G.HelperText>See <Hds::Link::Inline @href="https://developer.hashicorp.com/nomad/tutorials/access-control/access-control-tokens#token-types">Token types documentation</Hds::Link::Inline> for more information.</G.HelperText>
|
||||||
|
<G.Radio::Field
|
||||||
|
@id="client"
|
||||||
|
checked={{eq @token.type "client"}}
|
||||||
|
data-test-token-type="client"
|
||||||
|
as |F|>
|
||||||
|
<F.Label>Client</F.Label>
|
||||||
|
</G.Radio::Field>
|
||||||
|
<G.Radio::Field
|
||||||
|
@id="management"
|
||||||
|
checked={{eq @token.type "management"}}
|
||||||
|
data-test-token-type="management"
|
||||||
|
as |F|>
|
||||||
|
<F.Label>Management</F.Label>
|
||||||
|
</G.Radio::Field>
|
||||||
|
</Hds::Form::Radio::Group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if (eq @token.type "client")}}
|
||||||
|
<div data-test-token-policies>
|
||||||
|
<label>
|
||||||
|
Policies
|
||||||
|
</label>
|
||||||
|
{{#if @policies.length}}
|
||||||
|
<Hds::Table @caption="A list of policies available to this token" class="acl-table"
|
||||||
|
@model={{@policies}}
|
||||||
|
@columns={{array
|
||||||
|
(hash key="selected" width="80px")
|
||||||
|
(hash key="name" label="Name" isSortable=true)
|
||||||
|
(hash key="description" label="Description")
|
||||||
|
(hash key="definition" label="View Policy Definition")
|
||||||
|
}}
|
||||||
|
@sortBy="name"
|
||||||
|
>
|
||||||
|
<:body as |B|>
|
||||||
|
<B.Tr>
|
||||||
|
<B.Td class="selection-checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox"
|
||||||
|
checked={{find-by "name" B.data.name @token.policies}}
|
||||||
|
{{on "change" (action this.updateTokenPolicies B.data)}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</B.Td>
|
||||||
|
<B.Td data-test-policy-name>{{B.data.name}}</B.Td>
|
||||||
|
<B.Td>{{B.data.description}}</B.Td>
|
||||||
|
<B.Td>
|
||||||
|
<LinkTo @route="access-control.policies.policy" @model={{B.data.name}}>
|
||||||
|
View Policy Definition
|
||||||
|
</LinkTo>
|
||||||
|
</B.Td>
|
||||||
|
</B.Tr>
|
||||||
|
</:body>
|
||||||
|
</Hds::Table>
|
||||||
|
{{else}}
|
||||||
|
<div class="empty-message">
|
||||||
|
<h3 data-test-empty-role-list-headline class="empty-message-headline">
|
||||||
|
No Policies
|
||||||
|
</h3>
|
||||||
|
<p class="empty-message-body">
|
||||||
|
Get started by <LinkTo @route="access-control.policies.new">creating a new policy</LinkTo>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-test-token-roles>
|
||||||
|
<label>
|
||||||
|
Roles
|
||||||
|
</label>
|
||||||
|
{{#if @roles.length}}
|
||||||
|
<Hds::Table @caption="A list of roles available to this token" class="acl-table"
|
||||||
|
@model={{@roles}}
|
||||||
|
@columns={{array
|
||||||
|
(hash key="selected" width="80px")
|
||||||
|
(hash key="name" label="Name" isSortable=true)
|
||||||
|
(hash key="description" label="Description")
|
||||||
|
(hash key="policies" label="Policies")
|
||||||
|
(hash key="definition" label="View Role Info")
|
||||||
|
}}
|
||||||
|
@sortBy="name"
|
||||||
|
>
|
||||||
|
<:body as |B|>
|
||||||
|
<B.Tr>
|
||||||
|
<B.Td class="selection-checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox"
|
||||||
|
checked={{find-by "name" B.data.name @token.roles}}
|
||||||
|
{{on "change" (action this.updateTokenRoles B.data)}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</B.Td>
|
||||||
|
<B.Td data-test-role-name>{{B.data.name}}</B.Td>
|
||||||
|
<B.Td>{{B.data.description}}</B.Td>
|
||||||
|
<B.Td>
|
||||||
|
<div class="tag-group">
|
||||||
|
{{#each B.data.policies as |policy|}}
|
||||||
|
{{#if policy.name}}
|
||||||
|
<Hds::Tag @color="primary" @text="{{policy.name}}" @route="access-control.policies.policy" @model="{{policy.name}}" />
|
||||||
|
{{/if}}
|
||||||
|
{{else}}
|
||||||
|
Role contains no policies
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</B.Td>
|
||||||
|
<B.Td>
|
||||||
|
<LinkTo @route="access-control.roles.role" @model={{B.data.id}}>
|
||||||
|
View Role Info
|
||||||
|
</LinkTo>
|
||||||
|
</B.Td>
|
||||||
|
</B.Tr>
|
||||||
|
</:body>
|
||||||
|
</Hds::Table>
|
||||||
|
{{else}}
|
||||||
|
<div class="empty-message">
|
||||||
|
<h3 data-test-empty-role-list-headline class="empty-message-headline">
|
||||||
|
No Roles
|
||||||
|
</h3>
|
||||||
|
<p class="empty-message-body">
|
||||||
|
Get started by <LinkTo @route="access-control.roles.new">creating a new role</LinkTo>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{{else}}
|
||||||
|
<p>Management-type tokens have access to all permissions.</p>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
{{#if (can "update token")}}
|
||||||
|
<Hds::Button @text="Save Token" @color="primary"
|
||||||
|
data-test-token-save
|
||||||
|
{{on "click" this.save}}
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
</footer>
|
||||||
|
</form>
|
|
@ -0,0 +1,120 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
// @ts-check
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { alias } from '@ember/object/computed';
|
||||||
|
import Component from '@glimmer/component';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
|
||||||
|
export default class TokenEditorComponent extends Component {
|
||||||
|
@service notifications;
|
||||||
|
@service router;
|
||||||
|
@service store;
|
||||||
|
|
||||||
|
@alias('args.roles') roles;
|
||||||
|
@alias('args.token') activeToken;
|
||||||
|
@alias('args.policies') policies;
|
||||||
|
|
||||||
|
@tracked tokenPolicies = [];
|
||||||
|
@tracked tokenRoles = [];
|
||||||
|
|
||||||
|
// when this renders, set up tokenPolicies
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.tokenPolicies = this.activeToken.policies.toArray() || [];
|
||||||
|
this.tokenRoles = this.activeToken.roles.toArray() || [];
|
||||||
|
if (this.activeToken.isNew) {
|
||||||
|
this.activeToken.expirationTTL = 'never';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action updateTokenPolicies(policy, event) {
|
||||||
|
let { checked } = event.target;
|
||||||
|
if (checked) {
|
||||||
|
this.tokenPolicies.push(policy);
|
||||||
|
} else {
|
||||||
|
this.tokenPolicies = this.tokenPolicies.filter((p) => p !== policy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action updateTokenRoles(role, event) {
|
||||||
|
let { checked } = event.target;
|
||||||
|
if (checked) {
|
||||||
|
this.tokenRoles.push(role);
|
||||||
|
} else {
|
||||||
|
this.tokenRoles = this.tokenRoles.filter((p) => p !== role);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action updateTokenType(event) {
|
||||||
|
let tokenType = event.target.id;
|
||||||
|
this.activeToken.type = tokenType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action updateTokenExpirationTime(event) {
|
||||||
|
// Override expirationTTL if user selects a time
|
||||||
|
this.activeToken.expirationTTL = null;
|
||||||
|
this.activeToken.expirationTime = new Date(event.target.value);
|
||||||
|
}
|
||||||
|
@action updateTokenExpirationTTL(event) {
|
||||||
|
// Override expirationTime if user selects a TTL
|
||||||
|
this.activeToken.expirationTime = null;
|
||||||
|
if (event.target.value === 'never') {
|
||||||
|
this.activeToken.expirationTTL = null;
|
||||||
|
} else if (event.target.value === 'custom') {
|
||||||
|
this.activeToken.expirationTime = new Date();
|
||||||
|
} else {
|
||||||
|
this.activeToken.expirationTTL = event.target.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action async save() {
|
||||||
|
try {
|
||||||
|
const shouldRedirectAfterSave = this.activeToken.isNew;
|
||||||
|
|
||||||
|
this.activeToken.policies = this.tokenPolicies;
|
||||||
|
this.activeToken.roles = this.tokenRoles;
|
||||||
|
|
||||||
|
if (this.activeToken.type === 'management') {
|
||||||
|
// Management tokens cannot have policies or roles
|
||||||
|
this.activeToken.policyIDs = [];
|
||||||
|
this.activeToken.policyNames = [];
|
||||||
|
this.activeToken.policies = [];
|
||||||
|
this.activeToken.roles = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets to "never" for auto-selecting the radio button;
|
||||||
|
// if it gets updated by the user, will fall back to "" to represent
|
||||||
|
// no expiration. However, if the user never updates it,
|
||||||
|
// it stays as the string "never", where the API expects a null value.
|
||||||
|
if (this.activeToken.expirationTTL === 'never') {
|
||||||
|
this.activeToken.expirationTTL = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.activeToken.save();
|
||||||
|
|
||||||
|
this.notifications.add({
|
||||||
|
title: 'Token Saved',
|
||||||
|
color: 'success',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (shouldRedirectAfterSave) {
|
||||||
|
this.router.transitionTo(
|
||||||
|
'access-control.tokens.token',
|
||||||
|
this.activeToken.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.notifications.add({
|
||||||
|
title: `Error creating Token ${this.activeToken.name}`,
|
||||||
|
message: error,
|
||||||
|
color: 'critical',
|
||||||
|
sticky: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Controller from '@ember/controller';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
import { task } from 'ember-concurrency';
|
||||||
|
|
||||||
|
export default class AccessControlPoliciesIndexController extends Controller {
|
||||||
|
@service router;
|
||||||
|
@service notifications;
|
||||||
|
@service can;
|
||||||
|
|
||||||
|
get columns() {
|
||||||
|
const defaultColumns = [
|
||||||
|
{
|
||||||
|
key: 'name',
|
||||||
|
label: 'Name',
|
||||||
|
isSortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'description',
|
||||||
|
label: 'Description',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const tokensColumn = {
|
||||||
|
key: 'tokens',
|
||||||
|
label: 'Tokens',
|
||||||
|
isSortable: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteColumn = {
|
||||||
|
key: 'delete',
|
||||||
|
label: 'Delete',
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
...defaultColumns,
|
||||||
|
...(this.can.can('list token') ? [tokensColumn] : []),
|
||||||
|
...(this.can.can('destroy policy') ? [deleteColumn] : []),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
get policies() {
|
||||||
|
return this.model.policies.map((policy) => {
|
||||||
|
policy.tokens = (this.model.tokens || []).filter((token) => {
|
||||||
|
return token.policies.includes(policy);
|
||||||
|
});
|
||||||
|
return policy;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action openPolicy(policy) {
|
||||||
|
this.router.transitionTo('access-control.policies.policy', policy.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action goToNewPolicy() {
|
||||||
|
this.router.transitionTo('access-control.policies.new');
|
||||||
|
}
|
||||||
|
|
||||||
|
@task(function* (policy) {
|
||||||
|
try {
|
||||||
|
yield policy.deleteRecord();
|
||||||
|
yield policy.save();
|
||||||
|
|
||||||
|
// Cleanup: Remove references from roles and tokens
|
||||||
|
this.store.peekAll('role').forEach((role) => {
|
||||||
|
role.policies.removeObject(policy);
|
||||||
|
});
|
||||||
|
this.store.peekAll('token').forEach((token) => {
|
||||||
|
token.policies.removeObject(policy);
|
||||||
|
});
|
||||||
|
if (this.store.peekRecord('policy', policy.id)) {
|
||||||
|
this.store.unloadRecord(policy);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.notifications.add({
|
||||||
|
title: `Policy ${policy.name} successfully deleted`,
|
||||||
|
color: 'success',
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.error = {
|
||||||
|
title: 'Error deleting policy',
|
||||||
|
description: err,
|
||||||
|
};
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
deletePolicy;
|
||||||
|
}
|
|
@ -5,13 +5,11 @@
|
||||||
|
|
||||||
// @ts-check
|
// @ts-check
|
||||||
import Controller from '@ember/controller';
|
import Controller from '@ember/controller';
|
||||||
import { action } from '@ember/object';
|
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import { tracked } from '@glimmer/tracking';
|
|
||||||
import { alias } from '@ember/object/computed';
|
import { alias } from '@ember/object/computed';
|
||||||
import { task } from 'ember-concurrency';
|
import { task } from 'ember-concurrency';
|
||||||
|
|
||||||
export default class PoliciesPolicyController extends Controller {
|
export default class AccessControlPoliciesPolicyController extends Controller {
|
||||||
@service notifications;
|
@service notifications;
|
||||||
@service router;
|
@service router;
|
||||||
@service store;
|
@service store;
|
||||||
|
@ -19,36 +17,32 @@ export default class PoliciesPolicyController extends Controller {
|
||||||
@alias('model.policy') policy;
|
@alias('model.policy') policy;
|
||||||
@alias('model.tokens') tokens;
|
@alias('model.tokens') tokens;
|
||||||
|
|
||||||
@tracked
|
|
||||||
error = null;
|
|
||||||
|
|
||||||
@tracked isDeleting = false;
|
|
||||||
|
|
||||||
get newTokenString() {
|
get newTokenString() {
|
||||||
return `nomad acl token create -name="<TOKEN_NAME>" -policy="${this.policy.name}" -type=client -ttl=<8h>`;
|
return `nomad acl token create -name="<TOKEN_NAME>" -policy="${this.policy.name}" -type=client -ttl=8h`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
onDeletePrompt() {
|
|
||||||
this.isDeleting = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
onDeleteCancel() {
|
|
||||||
this.isDeleting = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@task(function* () {
|
@task(function* () {
|
||||||
try {
|
try {
|
||||||
yield this.policy.deleteRecord();
|
yield this.policy.deleteRecord();
|
||||||
yield this.policy.save();
|
yield this.policy.save();
|
||||||
|
|
||||||
|
// Cleanup: Remove references from roles and tokens
|
||||||
|
this.store.peekAll('role').forEach((role) => {
|
||||||
|
role.policies.removeObject(this.policy);
|
||||||
|
});
|
||||||
|
this.store.peekAll('token').forEach((token) => {
|
||||||
|
token.policies.removeObject(this.policy);
|
||||||
|
});
|
||||||
|
if (this.store.peekRecord('policy', this.policy.id)) {
|
||||||
|
this.store.unloadRecord(this.policy);
|
||||||
|
}
|
||||||
|
|
||||||
this.notifications.add({
|
this.notifications.add({
|
||||||
title: 'Policy Deleted',
|
title: 'Policy Deleted',
|
||||||
color: 'success',
|
color: 'success',
|
||||||
type: `success`,
|
type: `success`,
|
||||||
destroyOnClick: false,
|
destroyOnClick: false,
|
||||||
});
|
});
|
||||||
this.router.transitionTo('policies');
|
this.router.transitionTo('access-control.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}`,
|
||||||
|
@ -92,12 +86,12 @@ export default class PoliciesPolicyController extends Controller {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.error = {
|
this.notifications.add({
|
||||||
title: 'Error creating new token',
|
title: 'Error creating test token',
|
||||||
description: err,
|
message: err,
|
||||||
};
|
color: 'critical',
|
||||||
|
sticky: true,
|
||||||
throw err;
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
createTestToken;
|
createTestToken;
|
||||||
|
@ -112,12 +106,12 @@ export default class PoliciesPolicyController extends Controller {
|
||||||
color: 'success',
|
color: 'success',
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.error = {
|
this.notifications.add({
|
||||||
title: 'Error deleting token',
|
title: 'Error deleting token',
|
||||||
description: err,
|
message: err,
|
||||||
};
|
color: 'critical',
|
||||||
|
sticky: true,
|
||||||
throw err;
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
deleteToken;
|
deleteToken;
|
|
@ -0,0 +1,88 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Controller from '@ember/controller';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
import { task } from 'ember-concurrency';
|
||||||
|
|
||||||
|
export default class AccessControlRolesIndexController extends Controller {
|
||||||
|
@service router;
|
||||||
|
@service notifications;
|
||||||
|
@service can;
|
||||||
|
|
||||||
|
get columns() {
|
||||||
|
const defaultColumns = [
|
||||||
|
{
|
||||||
|
key: 'name',
|
||||||
|
label: 'Name',
|
||||||
|
isSortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'description',
|
||||||
|
label: 'Description',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const policiesColumn = {
|
||||||
|
key: 'policies',
|
||||||
|
label: 'Policies',
|
||||||
|
};
|
||||||
|
|
||||||
|
const tokensColumn = {
|
||||||
|
key: 'tokens',
|
||||||
|
label: 'Tokens',
|
||||||
|
isSortable: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteColumn = {
|
||||||
|
key: 'delete',
|
||||||
|
label: 'Delete',
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
...defaultColumns,
|
||||||
|
...(this.can.can('list token') ? [tokensColumn] : []),
|
||||||
|
...(this.can.can('list policy') ? [policiesColumn] : []),
|
||||||
|
...(this.can.can('destroy role') ? [deleteColumn] : []),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
get roles() {
|
||||||
|
return this.model.roles.map((role) => {
|
||||||
|
role.tokens = (this.model.tokens || []).filter((token) => {
|
||||||
|
return token.roles.includes(role);
|
||||||
|
});
|
||||||
|
return role;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action openRole(role) {
|
||||||
|
this.router.transitionTo('access-control.roles.role', role.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action goToNewRole() {
|
||||||
|
this.router.transitionTo('access-control.roles.new');
|
||||||
|
}
|
||||||
|
|
||||||
|
@task(function* (role) {
|
||||||
|
try {
|
||||||
|
yield role.deleteRecord();
|
||||||
|
yield role.save();
|
||||||
|
this.notifications.add({
|
||||||
|
title: `Role ${role.name} successfully deleted`,
|
||||||
|
color: 'success',
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.error = {
|
||||||
|
title: 'Error deleting role',
|
||||||
|
description: err,
|
||||||
|
};
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
deleteRole;
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
/**
|
||||||
|
* 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 { alias } from '@ember/object/computed';
|
||||||
|
import { task } from 'ember-concurrency';
|
||||||
|
|
||||||
|
export default class AccessControlRolesRoleController extends Controller {
|
||||||
|
@service notifications;
|
||||||
|
@service router;
|
||||||
|
@service store;
|
||||||
|
|
||||||
|
@alias('model.role') role;
|
||||||
|
@alias('model.tokens') tokens;
|
||||||
|
@alias('model.policies') policies;
|
||||||
|
|
||||||
|
get newTokenString() {
|
||||||
|
return `nomad acl token create -name="<TOKEN_NAME>" -role-name="${this.role.name}" -type=client -ttl=8h`;
|
||||||
|
}
|
||||||
|
|
||||||
|
@task(function* () {
|
||||||
|
try {
|
||||||
|
yield this.role.deleteRecord();
|
||||||
|
yield this.role.save();
|
||||||
|
this.notifications.add({
|
||||||
|
title: 'Role Deleted',
|
||||||
|
color: 'success',
|
||||||
|
type: `success`,
|
||||||
|
destroyOnClick: false,
|
||||||
|
});
|
||||||
|
this.router.transitionTo('access-control.roles');
|
||||||
|
} catch (err) {
|
||||||
|
this.notifications.add({
|
||||||
|
title: `Error deleting Role ${this.role.name}`,
|
||||||
|
message: err,
|
||||||
|
color: 'critical',
|
||||||
|
sticky: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
deleteRole;
|
||||||
|
|
||||||
|
async refreshTokens() {
|
||||||
|
this.tokens = this.store.peekAll('token').filter((token) =>
|
||||||
|
token.roles.any((role) => {
|
||||||
|
return role.id === decodeURIComponent(this.role.id);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@task(function* () {
|
||||||
|
try {
|
||||||
|
const newToken = this.store.createRecord('token', {
|
||||||
|
name: `Example Token for ${this.role.name}`,
|
||||||
|
roles: [this.role],
|
||||||
|
// New date 10 minutes into the future
|
||||||
|
expirationTime: new Date(Date.now() + 10 * 60 * 1000),
|
||||||
|
type: 'client',
|
||||||
|
});
|
||||||
|
yield newToken.save();
|
||||||
|
yield this.refreshTokens();
|
||||||
|
this.notifications.add({
|
||||||
|
title: 'Example Token Created',
|
||||||
|
message: `${newToken.secret}`,
|
||||||
|
color: 'success',
|
||||||
|
timeout: 30000,
|
||||||
|
customAction: {
|
||||||
|
label: 'Copy to Clipboard',
|
||||||
|
action: () => {
|
||||||
|
navigator.clipboard.writeText(newToken.secret);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.notifications.add({
|
||||||
|
title: 'Error creating test token',
|
||||||
|
message: err,
|
||||||
|
color: 'critical',
|
||||||
|
sticky: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
createTestToken;
|
||||||
|
|
||||||
|
@task(function* (token) {
|
||||||
|
try {
|
||||||
|
yield token.deleteRecord();
|
||||||
|
yield token.save();
|
||||||
|
yield this.refreshTokens();
|
||||||
|
this.notifications.add({
|
||||||
|
title: 'Token successfully deleted',
|
||||||
|
color: 'success',
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.notifications.add({
|
||||||
|
title: 'Error deleting token',
|
||||||
|
message: err,
|
||||||
|
color: 'critical',
|
||||||
|
sticky: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
deleteToken;
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
// @ts-check
|
||||||
|
import Controller from '@ember/controller';
|
||||||
|
import { task } from 'ember-concurrency';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
|
||||||
|
export default class AccessControlTokensIndexController extends Controller {
|
||||||
|
@service notifications;
|
||||||
|
@service router;
|
||||||
|
@service token;
|
||||||
|
|
||||||
|
@task(function* (token) {
|
||||||
|
try {
|
||||||
|
yield token.deleteRecord();
|
||||||
|
yield token.save();
|
||||||
|
this.notifications.add({
|
||||||
|
title: `Token ${token.name} successfully deleted`,
|
||||||
|
color: 'success',
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.error = {
|
||||||
|
title: 'Error deleting token',
|
||||||
|
description: err,
|
||||||
|
};
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
deleteToken;
|
||||||
|
|
||||||
|
get selfToken() {
|
||||||
|
return this.token.selfToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action openToken(token) {
|
||||||
|
this.router.transitionTo('access-control.tokens.token', token.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action goToNewToken() {
|
||||||
|
this.router.transitionTo('access-control.tokens.new');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
/**
|
||||||
|
* 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 { alias } from '@ember/object/computed';
|
||||||
|
import { task } from 'ember-concurrency';
|
||||||
|
|
||||||
|
export default class AccessControlTokensTokenController extends Controller {
|
||||||
|
@service notifications;
|
||||||
|
@service router;
|
||||||
|
@service store;
|
||||||
|
|
||||||
|
@alias('model.roles') roles;
|
||||||
|
@alias('model.token') activeToken; // looks like .token is an Ember reserved name?
|
||||||
|
@alias('model.policies') policies;
|
||||||
|
|
||||||
|
@task(function* () {
|
||||||
|
try {
|
||||||
|
yield this.activeToken.deleteRecord();
|
||||||
|
yield this.activeToken.save();
|
||||||
|
this.notifications.add({
|
||||||
|
title: 'Token Deleted',
|
||||||
|
color: 'success',
|
||||||
|
type: `success`,
|
||||||
|
destroyOnClick: false,
|
||||||
|
});
|
||||||
|
this.router.transitionTo('access-control.tokens');
|
||||||
|
} catch (err) {
|
||||||
|
this.notifications.add({
|
||||||
|
title: `Error deleting Token ${this.activeToken.name}`,
|
||||||
|
message: err,
|
||||||
|
color: 'critical',
|
||||||
|
sticky: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
deleteToken;
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: MPL-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
// @ts-check
|
||||||
|
import Model from '@ember-data/model';
|
||||||
|
import { attr, hasMany } from '@ember-data/model';
|
||||||
|
|
||||||
|
export default class Role extends Model {
|
||||||
|
@attr('string') name;
|
||||||
|
@attr('string') description;
|
||||||
|
@hasMany('policy', { defaultValue: () => [] }) policies;
|
||||||
|
@attr() policyNames;
|
||||||
|
}
|
|
@ -15,12 +15,31 @@ export default class Token extends Model {
|
||||||
@attr('date') createTime;
|
@attr('date') createTime;
|
||||||
@attr('string') type;
|
@attr('string') type;
|
||||||
@hasMany('policy') policies;
|
@hasMany('policy') policies;
|
||||||
|
@hasMany('role') roles;
|
||||||
@attr() policyNames;
|
@attr() policyNames;
|
||||||
@attr('date') expirationTime;
|
@attr('date') expirationTime;
|
||||||
|
|
||||||
|
// Note on verbatim: updating a token requires passing in its expiration time, where
|
||||||
|
// the API performs an equality check. However, we want to display it as a nicely
|
||||||
|
// formatted date in the UI. @attr('date') does this for us, but it strips the
|
||||||
|
// nanoseconds. Thus, our serializer retains the original value in a separate field
|
||||||
|
// that gets used on PUT requests when needed.
|
||||||
|
@attr() expirationTimeVerbatim;
|
||||||
|
@attr() expirationTTL;
|
||||||
|
|
||||||
@alias('id') accessor;
|
@alias('id') accessor;
|
||||||
|
|
||||||
get isExpired() {
|
get isExpired() {
|
||||||
return this.expirationTime && this.expirationTime < new Date();
|
return this.expirationTime && this.expirationTime < new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combined policies directly on the token, and policies inferred from token's role[s]
|
||||||
|
*/
|
||||||
|
get combinedPolicies() {
|
||||||
|
return [
|
||||||
|
...this.policies.toArray(),
|
||||||
|
...this.roles.map((role) => role.policies.toArray()).flat(),
|
||||||
|
].uniq();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,8 +25,15 @@ export default class CodeMirrorModifier extends Modifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
didInstall() {
|
element = null;
|
||||||
this._setup();
|
args = {};
|
||||||
|
|
||||||
|
modify(element, positional, named) {
|
||||||
|
if (!this.element) {
|
||||||
|
this.element = element;
|
||||||
|
this.args = { positional, named };
|
||||||
|
this._setup();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
didUpdateArguments() {
|
didUpdateArguments() {
|
||||||
|
|
|
@ -111,11 +111,24 @@ Router.map(function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.route('policies', function () {
|
this.route('access-control', function () {
|
||||||
this.route('new');
|
this.route('policies', function () {
|
||||||
|
this.route('new');
|
||||||
this.route('policy', {
|
this.route('policy', {
|
||||||
path: '/:name',
|
path: '/:name',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.route('roles', function () {
|
||||||
|
this.route('new');
|
||||||
|
this.route('role', {
|
||||||
|
path: '/:id',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.route('tokens', function () {
|
||||||
|
this.route('new');
|
||||||
|
this.route('token', {
|
||||||
|
path: '/:id',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// Mirage-only route for testing OIDC flow
|
// Mirage-only route for testing OIDC flow
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Route from '@ember/routing/route';
|
||||||
|
import withForbiddenState from 'nomad-ui/mixins/with-forbidden-state';
|
||||||
|
import WithModelErrorHandling from 'nomad-ui/mixins/with-model-error-handling';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import RSVP from 'rsvp';
|
||||||
|
|
||||||
|
export default class AccessControlRoute extends Route.extend(
|
||||||
|
withForbiddenState,
|
||||||
|
WithModelErrorHandling
|
||||||
|
) {
|
||||||
|
@service can;
|
||||||
|
@service store;
|
||||||
|
@service router;
|
||||||
|
|
||||||
|
beforeModel() {
|
||||||
|
if (
|
||||||
|
this.can.cannot('list policies') ||
|
||||||
|
this.can.cannot('list roles') ||
|
||||||
|
this.can.cannot('list tokens')
|
||||||
|
) {
|
||||||
|
this.router.transitionTo('/jobs');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load our tokens, roles, and policies
|
||||||
|
model() {
|
||||||
|
return RSVP.hash({
|
||||||
|
policies: this.store.findAll('policy', { reload: true }),
|
||||||
|
roles: this.store.findAll('role', { reload: true }),
|
||||||
|
tokens: this.store.findAll('token', { reload: true }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// After model: check for all tokens[].policies and roles[].policies to see if any of them are listed
|
||||||
|
// that aren't also in the policies list.
|
||||||
|
// If any of them are, unload them from the store — they are orphans.
|
||||||
|
afterModel(model) {
|
||||||
|
let policies = model.policies;
|
||||||
|
let roles = model.roles;
|
||||||
|
let tokens = model.tokens;
|
||||||
|
|
||||||
|
roles.forEach((role) => {
|
||||||
|
let orphanedPolicies = [];
|
||||||
|
role.policies.forEach((policy) => {
|
||||||
|
if (policy && !policies.includes(policy)) {
|
||||||
|
orphanedPolicies.push(policy);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
orphanedPolicies.forEach((policy) => {
|
||||||
|
role.policies.removeObject(policy);
|
||||||
|
if (this.store.peekRecord('policy', policy.id)) {
|
||||||
|
this.store.unloadRecord(policy);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tokens.forEach((token) => {
|
||||||
|
let orphanedPolicies = [];
|
||||||
|
token.policies.forEach((policy) => {
|
||||||
|
if (policy && !policies.includes(policy)) {
|
||||||
|
orphanedPolicies.push(policy);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
orphanedPolicies.forEach((policy) => {
|
||||||
|
token.policies.removeObject(policy);
|
||||||
|
if (this.store.peekRecord('policy', policy.id)) {
|
||||||
|
this.store.unloadRecord(policy);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -84,13 +84,13 @@ operator {
|
||||||
# * write
|
# * write
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default class PoliciesNewRoute extends Route {
|
export default class AccessControlPoliciesNewRoute extends Route {
|
||||||
@service can;
|
@service can;
|
||||||
@service router;
|
@service router;
|
||||||
|
|
||||||
beforeModel() {
|
beforeModel() {
|
||||||
if (this.can.cannot('write policy')) {
|
if (this.can.cannot('write policy')) {
|
||||||
this.router.transitionTo('/policies');
|
this.router.transitionTo('/access-control/policies');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,8 +102,6 @@ export default class PoliciesNewRoute extends Route {
|
||||||
}
|
}
|
||||||
|
|
||||||
resetController(controller, isExiting) {
|
resetController(controller, isExiting) {
|
||||||
// If the user navigates away from /new, clear the path
|
|
||||||
controller.set('path', null);
|
|
||||||
if (isExiting) {
|
if (isExiting) {
|
||||||
// If user didn't save, delete the freshly created model
|
// If user didn't save, delete the freshly created model
|
||||||
if (controller.model.isNew) {
|
if (controller.model.isNew) {
|
|
@ -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 { hash } from 'rsvp';
|
import { hash } from 'rsvp';
|
||||||
|
|
||||||
export default class PoliciesPolicyRoute extends Route.extend(
|
export default class AccessControlPoliciesPolicyRoute extends Route.extend(
|
||||||
withForbiddenState,
|
withForbiddenState,
|
||||||
WithModelErrorHandling
|
WithModelErrorHandling
|
||||||
) {
|
) {
|
|
@ -0,0 +1,37 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Route from '@ember/routing/route';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
|
export default class AccessControlRolesNewRoute extends Route {
|
||||||
|
@service can;
|
||||||
|
@service router;
|
||||||
|
|
||||||
|
beforeModel() {
|
||||||
|
if (this.can.cannot('write role')) {
|
||||||
|
this.router.transitionTo('/access-control/roles');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async model() {
|
||||||
|
let role = await this.store.createRecord('role', {
|
||||||
|
name: '',
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
role,
|
||||||
|
policies: await this.store.findAll('policy'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
resetController(controller, isExiting) {
|
||||||
|
if (isExiting) {
|
||||||
|
// If user didn't save, delete the freshly created model
|
||||||
|
if (controller.model.role.isNew) {
|
||||||
|
controller.model.role.destroyRecord();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
// @ts-check
|
||||||
|
import Route from '@ember/routing/route';
|
||||||
|
import withForbiddenState from 'nomad-ui/mixins/with-forbidden-state';
|
||||||
|
import WithModelErrorHandling from 'nomad-ui/mixins/with-model-error-handling';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { hash } from 'rsvp';
|
||||||
|
|
||||||
|
export default class AccessControlRolesRoleRoute extends Route.extend(
|
||||||
|
withForbiddenState,
|
||||||
|
WithModelErrorHandling
|
||||||
|
) {
|
||||||
|
@service store;
|
||||||
|
|
||||||
|
async model(params) {
|
||||||
|
let role = await this.store.findRecord(
|
||||||
|
'role',
|
||||||
|
decodeURIComponent(params.id),
|
||||||
|
{
|
||||||
|
reload: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let policies = this.store.peekAll('policy');
|
||||||
|
|
||||||
|
return hash({
|
||||||
|
role,
|
||||||
|
tokens: this.store.peekAll('token').filter((token) => {
|
||||||
|
return token.roles.any((role) => {
|
||||||
|
return role.id === decodeURIComponent(params.id);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
policies,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Route from '@ember/routing/route';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
|
export default class AccessControlTokensNewRoute extends Route {
|
||||||
|
@service can;
|
||||||
|
@service router;
|
||||||
|
|
||||||
|
beforeModel() {
|
||||||
|
if (this.can.cannot('write token')) {
|
||||||
|
this.router.transitionTo('/access-control/tokens');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async model() {
|
||||||
|
let token = await this.store.createRecord('token', {
|
||||||
|
name: '',
|
||||||
|
type: 'client',
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
policies: await this.store.findAll('policy'),
|
||||||
|
roles: await this.store.findAll('role'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
resetController(controller, isExiting) {
|
||||||
|
if (isExiting) {
|
||||||
|
// If user didn't save, delete the freshly created model
|
||||||
|
if (controller.model.token.isNew) {
|
||||||
|
controller.model.token.destroyRecord();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
// @ts-check
|
||||||
|
import Route from '@ember/routing/route';
|
||||||
|
import withForbiddenState from 'nomad-ui/mixins/with-forbidden-state';
|
||||||
|
import WithModelErrorHandling from 'nomad-ui/mixins/with-model-error-handling';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { hash } from 'rsvp';
|
||||||
|
|
||||||
|
export default class AccessControlTokensTokenRoute extends Route.extend(
|
||||||
|
withForbiddenState,
|
||||||
|
WithModelErrorHandling
|
||||||
|
) {
|
||||||
|
@service store;
|
||||||
|
@service token;
|
||||||
|
|
||||||
|
// Route guard to prevent you from wrecking your current token
|
||||||
|
beforeModel() {
|
||||||
|
let id = this.paramsFor('access-control.tokens.token').id;
|
||||||
|
if (this.token.selfToken && this.token.selfToken.id === id) {
|
||||||
|
this.transitionTo('/access-control/tokens');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async model(params) {
|
||||||
|
let token = await this.store.findRecord(
|
||||||
|
'token',
|
||||||
|
decodeURIComponent(params.id),
|
||||||
|
{
|
||||||
|
reload: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let policies = this.store.peekAll('policy');
|
||||||
|
let roles = this.store.peekAll('role');
|
||||||
|
|
||||||
|
return hash({
|
||||||
|
token,
|
||||||
|
roles,
|
||||||
|
policies,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -56,7 +56,7 @@ export default class ApplicationRoute extends Route {
|
||||||
this.controllerFor('application').set('error', e);
|
this.controllerFor('application').set('error', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchSelfTokenAndPolicies = this.get(
|
const fetchSelfTokenAndPolicies = await this.get(
|
||||||
'token.fetchSelfTokenAndPolicies'
|
'token.fetchSelfTokenAndPolicies'
|
||||||
)
|
)
|
||||||
.perform()
|
.perform()
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: MPL-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
// @ts-check
|
||||||
|
import ApplicationSerializer from './application';
|
||||||
|
import classic from 'ember-classic-decorator';
|
||||||
|
import { copy } from 'ember-copy';
|
||||||
|
|
||||||
|
@classic
|
||||||
|
export default class RoleSerializer extends ApplicationSerializer {
|
||||||
|
normalize(typeHash, hash) {
|
||||||
|
hash.Policies = hash.Policies || []; // null guard
|
||||||
|
hash.PolicyIDs = hash.Policies.map((policy) => policy.Name);
|
||||||
|
hash.PolicyNames = copy(hash.PolicyIDs);
|
||||||
|
return super.normalize(typeHash, hash);
|
||||||
|
}
|
||||||
|
serialize(snapshot, options) {
|
||||||
|
const hash = super.serialize(snapshot, options);
|
||||||
|
// required for update/PUT requests
|
||||||
|
if (snapshot.id) {
|
||||||
|
hash.ID = snapshot.id;
|
||||||
|
}
|
||||||
|
hash.Policies = hash.PolicyIDs.map((policy) => {
|
||||||
|
return {
|
||||||
|
Name: policy,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
delete hash.PolicyIDs;
|
||||||
|
delete hash.PolicyNames;
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,35 @@ export default class TokenSerializer extends ApplicationSerializer {
|
||||||
normalize(typeHash, hash) {
|
normalize(typeHash, hash) {
|
||||||
hash.PolicyIDs = hash.Policies;
|
hash.PolicyIDs = hash.Policies;
|
||||||
hash.PolicyNames = copy(hash.Policies);
|
hash.PolicyNames = copy(hash.Policies);
|
||||||
|
hash.Roles = hash.Roles || [];
|
||||||
|
hash.RoleIDs = hash.Roles.map((role) => role.ID);
|
||||||
|
hash.ExpirationTimeVerbatim = hash.ExpirationTime;
|
||||||
return super.normalize(typeHash, hash);
|
return super.normalize(typeHash, hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serialize(snapshot, options) {
|
||||||
|
const hash = super.serialize(snapshot, options);
|
||||||
|
|
||||||
|
if (snapshot.id) {
|
||||||
|
hash.AccessorID = snapshot.id;
|
||||||
|
// If expirationTimeNanos exists, use that when saving expirationTime for equality check reasons;
|
||||||
|
// see note in token model.
|
||||||
|
hash.ExpirationTime = hash.ExpirationTimeVerbatim || hash.ExpirationTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete hash.CreateTime;
|
||||||
|
delete hash.SecretID;
|
||||||
|
|
||||||
|
hash.Policies = hash.PolicyIDs || [];
|
||||||
|
delete hash.PolicyIDs;
|
||||||
|
delete hash.PolicyNames;
|
||||||
|
|
||||||
|
hash.Roles =
|
||||||
|
(hash.RoleIDs || []).map((id) => {
|
||||||
|
return { ID: id };
|
||||||
|
}) || [];
|
||||||
|
delete hash.RoleIDs;
|
||||||
|
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,7 +71,24 @@ export default class TokenService extends Service {
|
||||||
@task(function* () {
|
@task(function* () {
|
||||||
try {
|
try {
|
||||||
if (this.selfToken) {
|
if (this.selfToken) {
|
||||||
return yield this.selfToken.get('policies');
|
// return yield this.selfToken.get('policies');
|
||||||
|
let tokenPolicies = yield this.selfToken.get('policies');
|
||||||
|
let rolePolicies = [];
|
||||||
|
const roles = yield this.selfToken.get('roles');
|
||||||
|
if (roles.length) {
|
||||||
|
yield Promise.all(
|
||||||
|
roles.map((role) => {
|
||||||
|
return role.policies;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
rolePolicies = roles
|
||||||
|
.map((role) => {
|
||||||
|
return role.policies;
|
||||||
|
})
|
||||||
|
.map((policies) => policies.toArray())
|
||||||
|
.flat();
|
||||||
|
}
|
||||||
|
return [...tokenPolicies.toArray(), ...rolePolicies];
|
||||||
} else {
|
} else {
|
||||||
let policy = yield this.store.findRecord('policy', 'anonymous');
|
let policy = yield this.store.findRecord('policy', 'anonymous');
|
||||||
return [policy];
|
return [policy];
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
* SPDX-License-Identifier: MPL-2.0
|
* SPDX-License-Identifier: MPL-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@import './components/accordion';
|
@import './components/accordion-internal';
|
||||||
@import './components/badge-nomad-internal';
|
@import './components/badge-nomad-internal';
|
||||||
@import './components/boxed-section';
|
@import './components/boxed-section';
|
||||||
@import './components/codemirror';
|
@import './components/codemirror';
|
||||||
|
@ -59,3 +59,4 @@
|
||||||
@import './components/policies';
|
@import './components/policies';
|
||||||
@import './components/metadata-editor';
|
@import './components/metadata-editor';
|
||||||
@import './components/job-status-panel';
|
@import './components/job-status-panel';
|
||||||
|
@import './components/access-control';
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
.access-control-overview {
|
||||||
|
.intro {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
& > div {
|
||||||
|
padding: 1rem;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
& > p {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
& > a {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&.hds-button {
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.acl-table {
|
||||||
|
.tag-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
a {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.acl-form {
|
||||||
|
display: grid;
|
||||||
|
gap: 2rem;
|
||||||
|
|
||||||
|
.selection-checkbox {
|
||||||
|
position: relative;
|
||||||
|
label {
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.expiration-time fieldset {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.acl-explainer {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
gap: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
|
@ -17,7 +17,7 @@
|
||||||
// Defensive styles in case header height goes over 100px, causing
|
// Defensive styles in case header height goes over 100px, causing
|
||||||
// the left gutter menu to be on top of the header.
|
// the left gutter menu to be on top of the header.
|
||||||
height: $header-height;
|
height: $header-height;
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-body {
|
.page-body {
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
* SPDX-License-Identifier: MPL-2.0
|
* SPDX-License-Identifier: MPL-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// TODO: merge this in with access-control.scss, consider getting rid of policies table specific stuff
|
||||||
|
|
||||||
table.policies {
|
table.policies {
|
||||||
tr {
|
tr {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -50,7 +52,8 @@ table.policies {
|
||||||
top: 3px;
|
top: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.create-test-token, pre {
|
button.create-test-token,
|
||||||
|
pre {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,7 +74,6 @@ table.policies {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
table.tokens {
|
table.tokens {
|
||||||
margin-bottom: 3rem;
|
margin-bottom: 3rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,9 +17,10 @@
|
||||||
color: $primary-invert;
|
color: $primary-invert;
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
padding-right: 20px;
|
padding-right: 20px;
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
z-index: $z-gutter;
|
||||||
|
|
||||||
.navbar-item {
|
.navbar-item {
|
||||||
color: rgba($primary-invert, 0.8);
|
color: rgba($primary-invert, 0.8);
|
||||||
|
@ -159,24 +160,16 @@
|
||||||
|
|
||||||
.profile-dropdown {
|
.profile-dropdown {
|
||||||
padding: 0.5rem 1rem 0.5rem 0.75rem;
|
padding: 0.5rem 1rem 0.5rem 0.75rem;
|
||||||
background-color: transparent;
|
z-index: $z-gutter;
|
||||||
border: none !important;
|
button.hds-dropdown-toggle-icon {
|
||||||
height: auto;
|
border-color: var(--token-color-palette-neutral-200);
|
||||||
box-shadow: none !important;
|
background-color: transparent;
|
||||||
|
color: var(--token-color-surface-primary);
|
||||||
|
|
||||||
&:focus {
|
&.hds-dropdown-toggle-icon--is-open {
|
||||||
background-color: #21a572;
|
background-color: var(--token-color-surface-primary);
|
||||||
}
|
color: var(--token-color-foreground-primary);
|
||||||
|
}
|
||||||
.ember-power-select-prefix {
|
|
||||||
color: rgba($primary-invert, 0.8);
|
|
||||||
}
|
|
||||||
.ember-power-select-selected-item {
|
|
||||||
margin-left: 0;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
.ember-power-select-status-icon {
|
|
||||||
border-top-color: white;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
{{!
|
||||||
|
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>
|
|
@ -0,0 +1,44 @@
|
||||||
|
{{!
|
||||||
|
Copyright (c) HashiCorp, Inc.
|
||||||
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
|
~}}
|
||||||
|
|
||||||
|
<section class="section access-control-overview">
|
||||||
|
<section class="intro">
|
||||||
|
<p>Your Nomad cluster has Access Control enabled, which you can use to control access to data and APIs. Here, you can manage the Tokens, Policies, and Roles for your system.</p>
|
||||||
|
<footer>
|
||||||
|
<Hds::Link::Standalone @icon="docs-link" @text="ACL System Fundamentals" @iconPosition="trailing" @href="https://developer.hashicorp.com/nomad/tutorials/access-control/access-control" />
|
||||||
|
<Hds::Link::Standalone @icon="docs-link" @text="ACL Policy Concepts" @iconPosition="trailing" @href="https://developer.hashicorp.com/nomad/tutorials/access-control/access-control-policies" />
|
||||||
|
</footer>
|
||||||
|
</section>
|
||||||
|
<div class="section-cards">
|
||||||
|
<Hds::Card::Container @level="mid" @hasBorder={{true}} data-test-tokens-card>
|
||||||
|
<LinkTo
|
||||||
|
@route="access-control.tokens"
|
||||||
|
>
|
||||||
|
{{this.model.tokens.length}} {{pluralize "Token" this.model.tokens.length}}
|
||||||
|
</LinkTo>
|
||||||
|
<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::Card::Container>
|
||||||
|
<Hds::Card::Container @level="mid" @hasBorder={{true}} data-test-roles-card>
|
||||||
|
<LinkTo
|
||||||
|
@route="access-control.roles"
|
||||||
|
>
|
||||||
|
{{this.model.roles.length}} {{pluralize "Role" this.model.roles.length}}
|
||||||
|
</LinkTo>
|
||||||
|
<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::Card::Container>
|
||||||
|
<Hds::Card::Container @level="mid" @hasBorder={{true}} data-test-policies-card>
|
||||||
|
<LinkTo
|
||||||
|
@route="access-control.policies"
|
||||||
|
>
|
||||||
|
{{this.model.policies.length}} {{pluralize "Policy" this.model.policies.length}}
|
||||||
|
</LinkTo>
|
||||||
|
<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::Card::Container>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{{outlet}}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{{!
|
||||||
|
Copyright (c) HashiCorp, Inc.
|
||||||
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
|
~}}
|
||||||
|
|
||||||
|
{{page-title "Policies"}}
|
||||||
|
<Breadcrumb @crumb={{hash label="Policies" args=(array "access-control.policies")}} />
|
||||||
|
{{outlet}}
|
|
@ -0,0 +1,82 @@
|
||||||
|
{{!
|
||||||
|
Copyright (c) HashiCorp, Inc.
|
||||||
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
|
~}}
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<header class="acl-explainer">
|
||||||
|
<p>
|
||||||
|
ACL Policies are sets of rules defining the capabilities granted to adhering tokens. You can create, modify, and delete them here.
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
{{#if (can "write policy")}}
|
||||||
|
<Hds::Button
|
||||||
|
@text="Create Policy"
|
||||||
|
@icon="plus"
|
||||||
|
@route="access-control.policies.new"
|
||||||
|
{{keyboard-shortcut
|
||||||
|
pattern=(array "n" "p")
|
||||||
|
action=(action this.goToNewPolicy)
|
||||||
|
label="Create Policy"
|
||||||
|
}}
|
||||||
|
data-test-create-policy
|
||||||
|
/>
|
||||||
|
{{else}}
|
||||||
|
<Hds::Button
|
||||||
|
@text="Create Policy"
|
||||||
|
@icon="plus"
|
||||||
|
disabled
|
||||||
|
data-test-disabled-create-policy
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{{#if this.policies.length}}
|
||||||
|
|
||||||
|
<Hds::Table @caption="A list of policies for this cluster" class="acl-table"
|
||||||
|
@model={{this.policies}}
|
||||||
|
@columns={{this.columns}}
|
||||||
|
@sortBy="name"
|
||||||
|
>
|
||||||
|
<:body as |B|>
|
||||||
|
<B.Tr
|
||||||
|
{{keyboard-shortcut
|
||||||
|
enumerated=true
|
||||||
|
action=(action "openPolicy" B.data)
|
||||||
|
}}
|
||||||
|
data-test-policy-row
|
||||||
|
>
|
||||||
|
<B.Td>
|
||||||
|
<LinkTo data-test-policy-name={{B.data.name}} @route="access-control.policies.policy" @model={{B.data.name}}>{{B.data.name}}</LinkTo></B.Td>
|
||||||
|
<B.Td>{{B.data.description}}</B.Td>
|
||||||
|
{{#if (can "list token")}}
|
||||||
|
<B.Td>
|
||||||
|
<span data-test-policy-total-tokens>{{B.data.tokens.length}}</span>
|
||||||
|
{{#if (filter-by "isExpired" B.data.tokens)}}
|
||||||
|
<span data-test-policy-expired-tokens class="number-expired">({{get (filter-by "isExpired" B.data.tokens) "length"}} expired)</span>
|
||||||
|
{{/if}}
|
||||||
|
</B.Td>
|
||||||
|
{{/if}}
|
||||||
|
{{#if (can "destroy policy")}}
|
||||||
|
<B.Td>
|
||||||
|
<Hds::Button @text="Delete" @size="small" @color="critical"
|
||||||
|
{{on "click" (perform this.deletePolicy B.data)}}
|
||||||
|
data-test-delete-policy
|
||||||
|
/>
|
||||||
|
</B.Td>
|
||||||
|
{{/if}}
|
||||||
|
</B.Tr>
|
||||||
|
</:body>
|
||||||
|
</Hds::Table>
|
||||||
|
{{else}}
|
||||||
|
<div class="empty-message">
|
||||||
|
<h3 data-test-empty-policies-list-headline class="empty-message-headline">
|
||||||
|
No Policies
|
||||||
|
</h3>
|
||||||
|
<p class="empty-message-body">
|
||||||
|
Get started by <LinkTo @route="access-control.policies.new">creating a new policy</LinkTo>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</section>
|
|
@ -3,7 +3,7 @@
|
||||||
SPDX-License-Identifier: MPL-2.0
|
SPDX-License-Identifier: MPL-2.0
|
||||||
~}}
|
~}}
|
||||||
|
|
||||||
<Breadcrumb @crumb={{hash label="New" args=(array "policies.new")}} />
|
<Breadcrumb @crumb={{hash label="New" args=(array "access-control.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: MPL-2.0
|
SPDX-License-Identifier: MPL-2.0
|
||||||
~}}
|
~}}
|
||||||
|
|
||||||
<Breadcrumb @crumb={{hash label=this.policy.name args=(array "policies.policy" this.policy.name)}} />
|
<Breadcrumb @crumb={{hash label=this.policy.name args=(array "access-control.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>
|
||||||
|
@ -12,17 +12,10 @@
|
||||||
</div>
|
</div>
|
||||||
{{#if (can "destroy policy")}}
|
{{#if (can "destroy policy")}}
|
||||||
<div>
|
<div>
|
||||||
<TwoStepButton
|
<Hds::Button @text="Delete Policy" @color="critical"
|
||||||
data-test-delete-button
|
data-test-delete-policy
|
||||||
@idleText="Delete policy"
|
{{on "click" (perform this.deletePolicy)}}
|
||||||
@cancelText="Cancel"
|
/>
|
||||||
@confirmText="Yes, delete"
|
|
||||||
@confirmationMessage="Are you sure?"
|
|
||||||
@awaitingConfirmation={{this.deletePolicy.isRunning}}
|
|
||||||
@onConfirm={{perform this.deletePolicy}}
|
|
||||||
@onPrompt={{this.onDeletePrompt}}
|
|
||||||
@onCancel={{this.onDeleteCancel}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</h1>
|
</h1>
|
||||||
|
@ -90,7 +83,7 @@
|
||||||
</t.head>
|
</t.head>
|
||||||
<t.body as |row|>
|
<t.body as |row|>
|
||||||
<tr data-test-policy-token-row>
|
<tr data-test-policy-token-row>
|
||||||
<td data-test-token-name>
|
<td data-test-token-name={{row.model.name}}>
|
||||||
<Tooltip @text={{row.model.id}}>
|
<Tooltip @text={{row.model.id}}>
|
||||||
{{row.model.name}}
|
{{row.model.name}}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -104,24 +97,14 @@
|
||||||
<span data-test-token-expiration-time class="{{if row.model.isExpired "has-text-danger"}}">{{moment-from-now row.model.expirationTime interval=1000}}</span>
|
<span data-test-token-expiration-time class="{{if row.model.isExpired "has-text-danger"}}">{{moment-from-now row.model.expirationTime interval=1000}}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{{else}}
|
{{else}}
|
||||||
<span class="has-text-grey">Never</span>
|
<span>Never</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</td>
|
</td>
|
||||||
{{#if (can "destroy token")}}
|
{{#if (can "destroy token")}}
|
||||||
<td class="is-200px">
|
<td class="is-200px">
|
||||||
<TwoStepButton
|
<Hds::Button @text="Delete Token" @color="critical"
|
||||||
data-test-delete-token-button
|
data-test-delete-token-button
|
||||||
@idleText="Delete"
|
{{on "click" (perform this.deleteToken row.model)}}
|
||||||
@cancelText="Cancel"
|
|
||||||
@confirmText="Yes, delete"
|
|
||||||
@confirmationMessage="Are you sure?"
|
|
||||||
@awaitingConfirmation={{row.model.isPendingDeletion}}
|
|
||||||
@onConfirm={{perform this.deleteToken row.model}}
|
|
||||||
@inlineText={{true}}
|
|
||||||
@classes={{hash
|
|
||||||
idleButton="is-danger is-outlined"
|
|
||||||
confirmButton="is-danger"
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
@ -141,5 +124,3 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{{outlet}}
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
{{!
|
||||||
|
Copyright (c) HashiCorp, Inc.
|
||||||
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
|
~}}
|
||||||
|
|
||||||
|
{{page-title "Roles"}}
|
||||||
|
<Breadcrumb @crumb={{hash label="Roles" args=(array "access-control.roles")}} />
|
||||||
|
{{outlet}}
|
|
@ -0,0 +1,102 @@
|
||||||
|
{{!
|
||||||
|
Copyright (c) HashiCorp, Inc.
|
||||||
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
|
~}}
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<header class="acl-explainer">
|
||||||
|
<p>
|
||||||
|
ACL Roles group one or more Policies into higher-level sets of permissions. A user token can have any number of roles or policies.
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
{{#if (can "write role")}}
|
||||||
|
<Hds::Button
|
||||||
|
@text="Create Role"
|
||||||
|
@icon="plus"
|
||||||
|
@route="access-control.roles.new"
|
||||||
|
{{keyboard-shortcut
|
||||||
|
pattern=(array "n" "r")
|
||||||
|
action=(action this.goToNewRole)
|
||||||
|
label="Create Role"
|
||||||
|
}}
|
||||||
|
data-test-create-role
|
||||||
|
/>
|
||||||
|
{{else}}
|
||||||
|
<Hds::Button
|
||||||
|
@text="Create Role"
|
||||||
|
@icon="plus"
|
||||||
|
disabled
|
||||||
|
data-test-disabled-create-role
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{{#if this.roles.length}}
|
||||||
|
<Hds::Table @caption="A list of roles for this cluster" class="acl-table"
|
||||||
|
@model={{this.roles}}
|
||||||
|
@columns={{this.columns}}
|
||||||
|
@sortBy="name"
|
||||||
|
>
|
||||||
|
<:body as |B|>
|
||||||
|
<B.Tr
|
||||||
|
{{keyboard-shortcut
|
||||||
|
enumerated=true
|
||||||
|
action=(action "openRole" B.data)
|
||||||
|
}}
|
||||||
|
data-test-role-row={{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>
|
||||||
|
<B.Td data-test-role-description>{{B.data.description}}</B.Td>
|
||||||
|
{{#if (can "list token")}}
|
||||||
|
<B.Td>
|
||||||
|
<span data-test-role-total-tokens>{{B.data.tokens.length}}</span>
|
||||||
|
{{#if (filter-by "isExpired" B.data.tokens)}}
|
||||||
|
<span data-test-role-expired-tokens class="number-expired">({{get (filter-by "isExpired" B.data.tokens) "length"}} expired)</span>
|
||||||
|
{{/if}}
|
||||||
|
</B.Td>
|
||||||
|
{{/if}}
|
||||||
|
{{#if (can "list policy")}}
|
||||||
|
<B.Td data-test-role-policies>
|
||||||
|
<div class="tag-group">
|
||||||
|
{{#each B.data.policyNames as |policyName|}}
|
||||||
|
{{#let (find-by "name" policyName this.model.policies) as |policy|}}
|
||||||
|
{{#if policy}}
|
||||||
|
<Hds::Tag @color="primary" @text="{{policy.name}}" @route="access-control.policies.policy" @model="{{policy.name}}" />
|
||||||
|
{{else}}
|
||||||
|
<Hds::Tag
|
||||||
|
{{hds-tooltip "This policy has been deleted"}}
|
||||||
|
@text="{{policyName}}"
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
{{/let}}
|
||||||
|
{{else}}
|
||||||
|
No Policies
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</B.Td>
|
||||||
|
{{/if}}
|
||||||
|
{{#if (can "destroy role")}}
|
||||||
|
<B.Td>
|
||||||
|
<Hds::Button @text="Delete" @size="small" @color="critical"
|
||||||
|
{{on "click" (perform this.deleteRole B.data)}}
|
||||||
|
data-test-delete-role
|
||||||
|
/>
|
||||||
|
</B.Td>
|
||||||
|
{{/if}}
|
||||||
|
</B.Tr>
|
||||||
|
</:body>
|
||||||
|
</Hds::Table>
|
||||||
|
|
||||||
|
{{else}}
|
||||||
|
<div class="empty-message">
|
||||||
|
<h3 data-test-empty-role-list-headline class="empty-message-headline">
|
||||||
|
No Roles
|
||||||
|
</h3>
|
||||||
|
<p class="empty-message-body">
|
||||||
|
Get started by <LinkTo @route="access-control.roles.new">creating a new role</LinkTo>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</section>
|
|
@ -0,0 +1,27 @@
|
||||||
|
{{!
|
||||||
|
Copyright (c) HashiCorp, Inc.
|
||||||
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
|
~}}
|
||||||
|
|
||||||
|
<Breadcrumb @crumb={{hash label="New" args=(array "access-control.roles.new")}} />
|
||||||
|
{{page-title "Create Role"}}
|
||||||
|
<section class="section">
|
||||||
|
<h1 class="title with-flex" data-test-title>
|
||||||
|
Create Role
|
||||||
|
</h1>
|
||||||
|
{{#if this.model.policies.length}}
|
||||||
|
<RoleEditor
|
||||||
|
@role={{this.model.role}}
|
||||||
|
@policies={{this.model.policies}}
|
||||||
|
/>
|
||||||
|
{{else}}
|
||||||
|
<div class="empty-message">
|
||||||
|
<h3 class="empty-message-headline">
|
||||||
|
No Policies
|
||||||
|
</h3>
|
||||||
|
<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>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</section>
|
|
@ -0,0 +1,123 @@
|
||||||
|
{{!
|
||||||
|
Copyright (c) HashiCorp, Inc.
|
||||||
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
|
~}}
|
||||||
|
<Breadcrumb @crumb={{hash label=this.role.name args=(array "access-control.roles.role" this.role.id)}} />
|
||||||
|
{{page-title "Role"}}
|
||||||
|
<section class="section">
|
||||||
|
<h1 class="title with-flex" data-test-title>
|
||||||
|
<div>
|
||||||
|
Edit Role
|
||||||
|
</div>
|
||||||
|
{{#if (can "destroy role")}}
|
||||||
|
<Hds::Button @text="Delete Role" @color="critical"
|
||||||
|
{{on "click" (perform this.deleteRole)}}
|
||||||
|
data-test-delete-role
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
</h1>
|
||||||
|
<RoleEditor
|
||||||
|
@role={{this.role}}
|
||||||
|
@policies={{this.policies}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{{#if (can "list token")}}
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<h2 class="title">
|
||||||
|
Tokens
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{{#if (can "write token")}}
|
||||||
|
<div class="token-operations">
|
||||||
|
<div class="boxed-section">
|
||||||
|
<div class="boxed-section-head">
|
||||||
|
<h3>Create a Test Token</h3>
|
||||||
|
</div>
|
||||||
|
<div class="boxed-section-body">
|
||||||
|
<p class="is-info">Create a test token that expires in 10 minutes for testing purposes.</p>
|
||||||
|
<label>
|
||||||
|
<Hds::Button @text="Create Test Token" @color="secondary"
|
||||||
|
data-test-create-test-token
|
||||||
|
class="create-test-token"
|
||||||
|
{{on "click" (perform this.createTestToken)}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="boxed-section">
|
||||||
|
<div class="boxed-section-head">
|
||||||
|
<h3>Create Tokens from the Nomad CLI</h3>
|
||||||
|
</div>
|
||||||
|
<div class="boxed-section-body">
|
||||||
|
<p>When you're ready to create more tokens, you can do so via the <a class="external-link" href="https://developer.hashicorp.com/nomad/docs/commands" target="_blank" rel="noopener noreferrer">Nomad CLI <FlightIcon @name="external-link" /></a> with the following:
|
||||||
|
<pre>
|
||||||
|
<code>{{this.newTokenString}}</code>
|
||||||
|
<CopyButton
|
||||||
|
data-test-copy-button
|
||||||
|
@clipboardText={{this.newTokenString}}
|
||||||
|
@compact={{true}}
|
||||||
|
>
|
||||||
|
</CopyButton>
|
||||||
|
</pre>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.tokens.length}}
|
||||||
|
<ListTable
|
||||||
|
@source={{this.tokens}}
|
||||||
|
@class="tokens no-mobile-condense" as |t|>
|
||||||
|
<t.head>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th>Expires</th>
|
||||||
|
{{#if (can "destroy token")}}
|
||||||
|
<th>Delete</th>
|
||||||
|
{{/if}}
|
||||||
|
</t.head>
|
||||||
|
<t.body as |row|>
|
||||||
|
<tr data-test-role-token-row>
|
||||||
|
<td data-test-token-name>
|
||||||
|
<Tooltip @text={{row.model.id}}>
|
||||||
|
{{row.model.name}}
|
||||||
|
</Tooltip>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{moment-from-now row.model.createTime interval=1000}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{#if row.model.expirationTime}}
|
||||||
|
<Tooltip @text={{row.model.expirationTime}}>
|
||||||
|
<span data-test-token-expiration-time class="{{if row.model.isExpired "has-text-danger"}}">{{moment-from-now row.model.expirationTime interval=1000}}</span>
|
||||||
|
</Tooltip>
|
||||||
|
{{else}}
|
||||||
|
<span>Never</span>
|
||||||
|
{{/if}}
|
||||||
|
</td>
|
||||||
|
{{#if (can "destroy token")}}
|
||||||
|
<td class="is-200px">
|
||||||
|
<Hds::Button @text="Delete Token" @color="critical"
|
||||||
|
data-test-delete-token-button
|
||||||
|
{{on "click" (perform this.deleteToken row.model)}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
{{/if}}
|
||||||
|
</tr>
|
||||||
|
</t.body>
|
||||||
|
</ListTable>
|
||||||
|
{{else}}
|
||||||
|
<div class="empty-message">
|
||||||
|
<h3 data-test-empty-role-list-headline class="empty-message-headline">
|
||||||
|
No Tokens
|
||||||
|
</h3>
|
||||||
|
<p class="empty-message-body">
|
||||||
|
No tokens are using this role.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
</section>
|
|
@ -0,0 +1,8 @@
|
||||||
|
{{!
|
||||||
|
Copyright (c) HashiCorp, Inc.
|
||||||
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
|
~}}
|
||||||
|
|
||||||
|
{{page-title "Tokens"}}
|
||||||
|
<Breadcrumb @crumb={{hash label="Tokens" args=(array "access-control.tokens")}} />
|
||||||
|
{{outlet}}
|
|
@ -0,0 +1,149 @@
|
||||||
|
{{!
|
||||||
|
Copyright (c) HashiCorp, Inc.
|
||||||
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
|
~}}
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<header class="acl-explainer">
|
||||||
|
<p>
|
||||||
|
ACL Tokens are associated with one or more policies or roles to grant specific capabilities. Users can use these to sign into, and operate, Nomad with the permissions laid out in their policies.
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
{{#if (can "write token")}}
|
||||||
|
<Hds::Button
|
||||||
|
@text="Create Token"
|
||||||
|
@icon="plus"
|
||||||
|
@route="access-control.tokens.new"
|
||||||
|
{{keyboard-shortcut
|
||||||
|
pattern=(array "n" "t")
|
||||||
|
action=(action this.goToNewToken)
|
||||||
|
label="Create Token"
|
||||||
|
}}
|
||||||
|
data-test-create-token
|
||||||
|
/>
|
||||||
|
{{else}}
|
||||||
|
<Hds::Button
|
||||||
|
@text="Create Token"
|
||||||
|
@icon="plus"
|
||||||
|
disabled
|
||||||
|
data-test-disabled-create-token
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
{{#if this.model.tokens.length}}
|
||||||
|
<Hds::Table @caption="A list of tokens for this cluster" class="acl-table"
|
||||||
|
@model={{this.model.tokens}}
|
||||||
|
@columns={{array
|
||||||
|
(hash key="name" label="Name" isSortable=true)
|
||||||
|
(hash key="type" label="Type" isSortable=true)
|
||||||
|
(hash key="createTime" label="Created" isSortable=true)
|
||||||
|
(hash key="expirationTime" label="Expires" isSortable=true)
|
||||||
|
(hash key="roles" label="Roles")
|
||||||
|
(hash key="policies" label="Policies")
|
||||||
|
|
||||||
|
(hash key="delete" label="Delete")
|
||||||
|
}}
|
||||||
|
@sortBy="name"
|
||||||
|
>
|
||||||
|
<:body as |B|>
|
||||||
|
<B.Tr
|
||||||
|
{{keyboard-shortcut
|
||||||
|
enumerated=true
|
||||||
|
action=(action "openToken" B.data)
|
||||||
|
}}
|
||||||
|
data-test-token-row
|
||||||
|
>
|
||||||
|
<B.Td data-test-token-name={{B.data.name}}>
|
||||||
|
{{#if (eq B.data.id this.selfToken.id)}}
|
||||||
|
<strong>{{B.data.name}}</strong>
|
||||||
|
{{else}}
|
||||||
|
<LinkTo @route="access-control.tokens.token" @model={{B.data.id}}>
|
||||||
|
{{B.data.name}}
|
||||||
|
</LinkTo>
|
||||||
|
{{/if}}
|
||||||
|
</B.Td>
|
||||||
|
<B.Td data-test-token-type={{B.data.type}}>{{B.data.type}}</B.Td>
|
||||||
|
<B.Td>{{moment-from-now B.data.createTime interval=1000}}</B.Td>
|
||||||
|
<B.Td>
|
||||||
|
{{#if B.data.expirationTime}}
|
||||||
|
<Tooltip @text={{B.data.expirationTime}}>
|
||||||
|
<span data-test-token-expiration-time class="{{if B.data.isExpired "has-text-danger"}}">{{moment-from-now B.data.expirationTime interval=1000}}</span>
|
||||||
|
</Tooltip>
|
||||||
|
{{else}}
|
||||||
|
<span data-test-token-expiration-time>Never</span>
|
||||||
|
{{/if}}
|
||||||
|
</B.Td>
|
||||||
|
|
||||||
|
<B.Td data-test-token-roles>
|
||||||
|
<div class="tag-group">
|
||||||
|
{{!--
|
||||||
|
We don't treat roles (roleNames) the same as policies, because Roles' names are currently
|
||||||
|
returning blank on the /tokens endpoint: https://github.com/hashicorp/nomad/issues/18451
|
||||||
|
TODO: when that's fixed, we can use an #each #let pattern like we do for policyNames.
|
||||||
|
--}}
|
||||||
|
{{#each B.data.roles as |role|}}
|
||||||
|
{{#if role.name}}
|
||||||
|
<Hds::Tag @color="primary" @text="{{role.name}}" @route="access-control.roles.role" @model="{{role.id}}" />
|
||||||
|
{{/if}}
|
||||||
|
{{else}}
|
||||||
|
{{#if (eq B.data.type "management")}}
|
||||||
|
Management Access
|
||||||
|
{{else}}
|
||||||
|
No Roles
|
||||||
|
{{/if}}
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</B.Td>
|
||||||
|
|
||||||
|
<B.Td data-test-token-policies>
|
||||||
|
<div class="tag-group">
|
||||||
|
{{#each B.data.policyNames as |policyName|}}
|
||||||
|
{{#let (find-by "name" policyName this.model.policies) as |policy|}}
|
||||||
|
{{#if policy}}
|
||||||
|
<Hds::Tag @color="primary" @text="{{policy.name}}" @route="access-control.policies.policy" @model="{{policy.name}}" />
|
||||||
|
{{else}}
|
||||||
|
<Hds::Tag
|
||||||
|
{{hds-tooltip "This policy has been deleted"}}
|
||||||
|
@text="{{policyName}}"
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
{{/let}}
|
||||||
|
{{else}}
|
||||||
|
{{#if (eq B.data.type "management")}}
|
||||||
|
Management Access
|
||||||
|
{{else}}
|
||||||
|
No Policies
|
||||||
|
{{/if}}
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</B.Td>
|
||||||
|
|
||||||
|
{{#if (can "destroy token")}}
|
||||||
|
<B.Td data-test-delete-token>
|
||||||
|
{{#if (eq B.data.id this.selfToken.id)}}
|
||||||
|
<Tooltip @text="Can't delete your own token" @isFullText={{true}}>
|
||||||
|
<Hds::Button @text="Delete" disabled @size="small" @color="critical" />
|
||||||
|
</Tooltip>
|
||||||
|
{{else}}
|
||||||
|
<Hds::Button @text="Delete" @size="small" @color="critical"
|
||||||
|
{{on "click" (perform this.deleteToken B.data)}}
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
</B.Td>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
</B.Tr>
|
||||||
|
</:body>
|
||||||
|
</Hds::Table>
|
||||||
|
{{else}}
|
||||||
|
<div class="empty-message">
|
||||||
|
<h3 data-test-empty-policies-list-headline class="empty-message-headline">
|
||||||
|
No Tokens
|
||||||
|
</h3>
|
||||||
|
<p class="empty-message-body">
|
||||||
|
Get started by <LinkTo @route="access-control.policies.new">creating a new policy</LinkTo>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</section>
|
|
@ -0,0 +1,17 @@
|
||||||
|
{{!
|
||||||
|
Copyright (c) HashiCorp, Inc.
|
||||||
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
|
~}}
|
||||||
|
|
||||||
|
<Breadcrumb @crumb={{hash label="New" args=(array "access-control.tokens.new")}} />
|
||||||
|
{{page-title "Create Token"}}
|
||||||
|
<section class="section">
|
||||||
|
<h1 class="title with-flex" data-test-title>
|
||||||
|
Create Token
|
||||||
|
</h1>
|
||||||
|
<TokenEditor
|
||||||
|
@token={{this.model.token}}
|
||||||
|
@roles={{this.model.roles}}
|
||||||
|
@policies={{this.model.policies}}
|
||||||
|
/>
|
||||||
|
</section>
|
|
@ -0,0 +1,24 @@
|
||||||
|
{{!
|
||||||
|
Copyright (c) HashiCorp, Inc.
|
||||||
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
|
~}}
|
||||||
|
<Breadcrumb @crumb={{hash label=this.activeToken.name args=(array "access-control.tokens.token" this.activeToken.id)}} />
|
||||||
|
{{page-title "Token"}}
|
||||||
|
<section class="section">
|
||||||
|
<h1 class="title with-flex" data-test-title>
|
||||||
|
<div>
|
||||||
|
Edit Token
|
||||||
|
</div>
|
||||||
|
{{#if (can "destroy token")}}
|
||||||
|
<Hds::Button @text="Delete Token" @color="critical"
|
||||||
|
{{on "click" (perform this.deleteToken)}}
|
||||||
|
data-test-delete-token
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
</h1>
|
||||||
|
<TokenEditor
|
||||||
|
@token={{this.activeToken}}
|
||||||
|
@policies={{this.policies}}
|
||||||
|
@roles={{this.roles}}
|
||||||
|
/>
|
||||||
|
</section>
|
|
@ -0,0 +1,13 @@
|
||||||
|
{{!
|
||||||
|
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>
|
||||||
|
</ul>
|
||||||
|
</div>
|
|
@ -135,16 +135,16 @@
|
||||||
<li
|
<li
|
||||||
{{keyboard-shortcut
|
{{keyboard-shortcut
|
||||||
menuLevel=true
|
menuLevel=true
|
||||||
pattern=(array "g" "l")
|
pattern=(array "g" "a")
|
||||||
action=(action this.transitionTo 'policies')
|
action=(action this.transitionTo 'access-control')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LinkTo
|
<LinkTo
|
||||||
@route="policies"
|
@route="access-control"
|
||||||
@activeClass="is-active"
|
@activeClass="is-active"
|
||||||
data-test-gutter-link="policies"
|
data-test-gutter-link="access-control"
|
||||||
>
|
>
|
||||||
Policies
|
Access Control
|
||||||
</LinkTo>
|
</LinkTo>
|
||||||
</li>
|
</li>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<Hds::Alert @type="inline" @color="highlight" as |A|>
|
<Hds::Alert @type="inline" @color="highlight" as |A|>
|
||||||
<A.Title>Automatic Access to Variables</A.Title>
|
<A.Title>Automatic Access to Variables</A.Title>
|
||||||
<A.Description>
|
<A.Description>
|
||||||
<p>Tasks in this job can have <a href="https://developer.hashicorp.com/nomad/docs/concepts/variables#task-access-to-variables" target="_blank" rel="noopener noreferrer">automatic access to Nomad Variables</a>.</p>
|
<p>Tasks in this job can have <Hds::Link::Inline @href="https://developer.hashicorp.com/nomad/docs/concepts/variables#task-access-to-variables" target="_blank" rel="noopener noreferrer">automatic access to Nomad Variables</Hds::Link::Inline>.</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li data-test-variables-intro-all-jobs>Use
|
<li data-test-variables-intro-all-jobs>Use
|
||||||
<code>
|
<code>
|
||||||
|
|
|
@ -3,12 +3,18 @@
|
||||||
SPDX-License-Identifier: MPL-2.0
|
SPDX-License-Identifier: MPL-2.0
|
||||||
~}}
|
~}}
|
||||||
|
|
||||||
{{page-title "Authorization"}}
|
{{page-title (if this.tokenRecord "Profile" "Sign In")}}
|
||||||
<section class="section authorization-page">
|
<section class="section authorization-page">
|
||||||
{{#if this.isValidatingToken}}
|
{{#if this.isValidatingToken}}
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
{{else}}
|
{{else}}
|
||||||
<h1 class="title">Authorization and access control</h1>
|
<h1 class="title">
|
||||||
|
{{#if this.tokenRecord}}
|
||||||
|
Profile
|
||||||
|
{{else}}
|
||||||
|
Sign In
|
||||||
|
{{/if}}
|
||||||
|
</h1>
|
||||||
|
|
||||||
<div class="status-notifications {{if this.canSignIn "is-half"}}">
|
<div class="status-notifications {{if this.canSignIn "is-half"}}">
|
||||||
|
|
||||||
|
@ -159,6 +165,31 @@
|
||||||
<button data-test-token-clear class="button is-primary" {{action "clearTokenProperties"}} type="button">Sign Out</button>
|
<button data-test-token-clear class="button is-primary" {{action "clearTokenProperties"}} type="button">Sign Out</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{#if this.tokenRecord.roles.length}}
|
||||||
|
<h3 class="title is-4">Roles</h3>
|
||||||
|
{{#each this.tokenRecord.roles as |role|}}
|
||||||
|
<div data-test-token-role class="boxed-section">
|
||||||
|
<div data-test-role-name class="boxed-section-head">
|
||||||
|
{{role.name}}
|
||||||
|
</div>
|
||||||
|
<div class="boxed-section-body">
|
||||||
|
{{#if role.description}}
|
||||||
|
<p class="content">
|
||||||
|
{{role.description}}
|
||||||
|
</p>
|
||||||
|
{{/if}}
|
||||||
|
<div data-test-role-policies>
|
||||||
|
<h4 class="title is-5">Policies</h4>
|
||||||
|
{{#each role.policies as |policy|}}
|
||||||
|
<li><a href="#{{policy.name}}">{{policy.name}}</a></li>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
<h3 class="title is-4">Policies</h3>
|
<h3 class="title is-4">Policies</h3>
|
||||||
{{#if (eq this.tokenRecord.type "management")}}
|
{{#if (eq this.tokenRecord.type "management")}}
|
||||||
<div data-test-token-management-message class="boxed-section">
|
<div data-test-token-management-message class="boxed-section">
|
||||||
|
@ -167,8 +198,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#each this.tokenRecord.policies as |policy|}}
|
{{#each this.tokenRecord.combinedPolicies as |policy|}}
|
||||||
<div data-test-token-policy class="boxed-section">
|
<div id="{{policy.name}}" data-test-token-policy class="boxed-section">
|
||||||
<div data-test-policy-name class="boxed-section-head">
|
<div data-test-policy-name class="boxed-section-head">
|
||||||
{{policy.name}}
|
{{policy.name}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -299,8 +299,16 @@ export default function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
const { policyIds } = token;
|
const policyIds = token.policyIds || [];
|
||||||
const policies = server.db.policies.find(policyIds);
|
|
||||||
|
const roleIds = token.roleIds || [];
|
||||||
|
const roles = server.db.roles.find(roleIds);
|
||||||
|
const rolePolicyIds = roles.map((role) => role.policyIds).flat();
|
||||||
|
|
||||||
|
const policies = server.db.policies.find([
|
||||||
|
...policyIds,
|
||||||
|
...rolePolicyIds,
|
||||||
|
]);
|
||||||
const hasReadPolicy = policies.find(
|
const hasReadPolicy = policies.find(
|
||||||
(p) =>
|
(p) =>
|
||||||
p.rulesJSON.Node?.Policy === 'read' ||
|
p.rulesJSON.Node?.Policy === 'read' ||
|
||||||
|
@ -476,16 +484,59 @@ export default function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.post('/acl/token', function (schema, request) {
|
this.post('/acl/token', function (schema, request) {
|
||||||
const { Name, Policies, Type } = JSON.parse(request.requestBody);
|
const { Name, Policies, Type, ExpirationTTL, ExpirationTime } = JSON.parse(
|
||||||
|
request.requestBody
|
||||||
|
);
|
||||||
|
|
||||||
|
function parseDuration(duration) {
|
||||||
|
const [_, value, unit] = duration.match(/(\d+)(\w)/);
|
||||||
|
const unitMap = {
|
||||||
|
s: 1000,
|
||||||
|
m: 1000 * 60,
|
||||||
|
h: 1000 * 60 * 60,
|
||||||
|
d: 1000 * 60 * 60 * 24,
|
||||||
|
};
|
||||||
|
return value * unitMap[unit];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's an expirationTime, use that. Otherwise, use the TTL.
|
||||||
|
const expirationTime = ExpirationTime
|
||||||
|
? new Date(ExpirationTime)
|
||||||
|
: ExpirationTTL
|
||||||
|
? new Date(Date.now() + parseDuration(ExpirationTTL))
|
||||||
|
: null;
|
||||||
|
|
||||||
return server.create('token', {
|
return server.create('token', {
|
||||||
name: Name,
|
name: Name,
|
||||||
policyIds: Policies,
|
policyIds: Policies,
|
||||||
type: Type,
|
type: Type,
|
||||||
id: faker.random.uuid(),
|
id: faker.random.uuid(),
|
||||||
|
expirationTime,
|
||||||
createTime: new Date().toISOString(),
|
createTime: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.post('/acl/token/:id', function (schema, request) {
|
||||||
|
// If both Policies and Roles arrays are empty, return an error
|
||||||
|
const { Policies, Roles } = JSON.parse(request.requestBody);
|
||||||
|
if (!Policies.length && !Roles.length) {
|
||||||
|
return new Response(
|
||||||
|
500,
|
||||||
|
{},
|
||||||
|
'Either Policies or Roles must be specified'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new Response(
|
||||||
|
200,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
id: request.params.id,
|
||||||
|
Policies,
|
||||||
|
Roles,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
this.get('/acl/token/self', function ({ tokens }, req) {
|
this.get('/acl/token/self', function ({ tokens }, req) {
|
||||||
const secret = req.requestHeaders['X-Nomad-Token'];
|
const secret = req.requestHeaders['X-Nomad-Token'];
|
||||||
const tokenForSecret = tokens.findBy({ secretId: secret });
|
const tokenForSecret = tokens.findBy({ secretId: secret });
|
||||||
|
@ -557,7 +608,6 @@ export default function () {
|
||||||
const policy = policies.findBy({ name: req.params.id });
|
const policy = policies.findBy({ name: req.params.id });
|
||||||
const secret = req.requestHeaders['X-Nomad-Token'];
|
const secret = req.requestHeaders['X-Nomad-Token'];
|
||||||
const tokenForSecret = tokens.findBy({ secretId: secret });
|
const tokenForSecret = tokens.findBy({ secretId: secret });
|
||||||
|
|
||||||
if (req.params.id === 'anonymous') {
|
if (req.params.id === 'anonymous') {
|
||||||
if (policy) {
|
if (policy) {
|
||||||
return this.serialize(policy);
|
return this.serialize(policy);
|
||||||
|
@ -565,13 +615,15 @@ export default function () {
|
||||||
return new Response(404, {}, null);
|
return new Response(404, {}, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the policy only if the token that matches the request header
|
// Return the policy only if the token that matches the request header
|
||||||
// includes the policy or if the token that matches the request header
|
// includes the policy or if the token that matches the request header
|
||||||
// is of type management
|
// is of type management
|
||||||
if (
|
if (
|
||||||
tokenForSecret &&
|
tokenForSecret &&
|
||||||
(tokenForSecret.policies.includes(policy) ||
|
(tokenForSecret.policies.includes(policy) ||
|
||||||
|
tokenForSecret.roles.models.any((role) =>
|
||||||
|
role.policies.includes(policy)
|
||||||
|
) ||
|
||||||
tokenForSecret.type === 'management')
|
tokenForSecret.type === 'management')
|
||||||
) {
|
) {
|
||||||
return this.serialize(policy);
|
return this.serialize(policy);
|
||||||
|
@ -581,21 +633,82 @@ export default function () {
|
||||||
return new Response(403, {}, null);
|
return new Response(403, {}, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.get('/acl/roles', function ({ roles }, req) {
|
||||||
|
return this.serialize(roles.all());
|
||||||
|
});
|
||||||
|
|
||||||
|
this.get('/acl/role/:id', function ({ roles }, req) {
|
||||||
|
const role = roles.findBy({ id: req.params.id });
|
||||||
|
return this.serialize(role);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.post('/acl/role', function (schema, request) {
|
||||||
|
const { Name, Description } = JSON.parse(request.requestBody);
|
||||||
|
return server.create('role', {
|
||||||
|
name: Name,
|
||||||
|
description: Description,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.put('/acl/role/:id', function (schema, request) {
|
||||||
|
const { Policies } = JSON.parse(request.requestBody);
|
||||||
|
if (!Policies.length) {
|
||||||
|
return new Response(500, {}, 'Policies must be specified');
|
||||||
|
}
|
||||||
|
return new Response(
|
||||||
|
200,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
id: request.params.id,
|
||||||
|
Policies,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.delete('/acl/role/:id', function (schema, request) {
|
||||||
|
const { id } = request.params;
|
||||||
|
|
||||||
|
// Also update any tokens whose policyIDs include this policy
|
||||||
|
const tokens =
|
||||||
|
server.schema.tokens.where((token) => token.roleIds?.includes(id)) || [];
|
||||||
|
tokens.models.forEach((token) => {
|
||||||
|
token.update({
|
||||||
|
roleIds: token.roleIds.filter((roleId) => roleId !== id),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.db.roles.remove(id);
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
this.get('/acl/policies', function ({ policies }, req) {
|
this.get('/acl/policies', function ({ policies }, req) {
|
||||||
return this.serialize(policies.all());
|
return this.serialize(policies.all());
|
||||||
});
|
});
|
||||||
|
|
||||||
this.delete('/acl/policy/:id', function (schema, request) {
|
this.delete('/acl/policy/:id', function (schema, request) {
|
||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
schema.tokens
|
|
||||||
.all()
|
// Also update any tokens whose policyIDs include this policy
|
||||||
.models.filter((token) => token.policyIds.includes(id))
|
const tokens =
|
||||||
.forEach((token) => {
|
server.schema.tokens.where((token) => token.policyIds?.includes(id)) ||
|
||||||
token.update({
|
[];
|
||||||
policyIds: token.policyIds.filter((pid) => pid !== id),
|
tokens.models.forEach((token) => {
|
||||||
});
|
token.update({
|
||||||
|
policyIds: token.policyIds.filter((policyId) => policyId !== id),
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also update any roles whose policyIDs include this policy
|
||||||
|
const roles =
|
||||||
|
server.schema.roles.where((role) => role.policyIds?.includes(id)) || [];
|
||||||
|
roles.models.forEach((role) => {
|
||||||
|
role.update({
|
||||||
|
policyIds: role.policyIds.filter((policyId) => policyId !== id),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
server.db.policies.remove(id);
|
server.db.policies.remove(id);
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -7,11 +7,17 @@ import { Factory } from 'ember-cli-mirage';
|
||||||
import faker from 'nomad-ui/mirage/faker';
|
import faker from 'nomad-ui/mirage/faker';
|
||||||
|
|
||||||
export default Factory.extend({
|
export default Factory.extend({
|
||||||
id: () => faker.hacker.verb(),
|
// Extra randomness appended to not conflict with the otherwise-uniq'd policies generated
|
||||||
|
// in factories.token.afterCreate
|
||||||
|
id: () =>
|
||||||
|
`${faker.hacker.verb().replace(/\s/g, '-')}-${faker.random.alphaNumeric(
|
||||||
|
5
|
||||||
|
)}`,
|
||||||
name() {
|
name() {
|
||||||
return this.id;
|
return this.id;
|
||||||
},
|
},
|
||||||
description: () => (faker.random.number(10) >= 2 ? faker.lorem.sentence() : null),
|
description: () =>
|
||||||
|
faker.random.number(10) >= 2 ? faker.lorem.sentence() : null,
|
||||||
rules: `# Allow read only access to the default namespace
|
rules: `# Allow read only access to the default namespace
|
||||||
namespace "default" {
|
namespace "default" {
|
||||||
policy = "read"
|
policy = "read"
|
||||||
|
|
|
@ -19,161 +19,166 @@ export default Factory.extend({
|
||||||
oneTimeSecret: () => faker.random.uuid(),
|
oneTimeSecret: () => faker.random.uuid(),
|
||||||
|
|
||||||
afterCreate(token, server) {
|
afterCreate(token, server) {
|
||||||
if (token.policyIds && token.policyIds.length) return;
|
// If the user has neither policies, nor roles with policies, add some fake ones.
|
||||||
const policyIds = Array(faker.random.number({ min: 1, max: 5 }))
|
if (
|
||||||
.fill(0)
|
!(token.policyIds && token.policyIds.length) &&
|
||||||
.map(() => faker.hacker.verb())
|
!(token.roles && token.roles.models.map((r) => r.policies).flat().length)
|
||||||
.uniq();
|
) {
|
||||||
|
const policyIds = Array(faker.random.number({ min: 1, max: 5 }))
|
||||||
|
.fill(0)
|
||||||
|
.map(() => faker.hacker.verb().replace(/\s/g, '-'))
|
||||||
|
.uniq();
|
||||||
|
|
||||||
policyIds.forEach((policy) => {
|
policyIds.forEach((policy) => {
|
||||||
const dbPolicy = server.db.policies.find(policy);
|
const dbPolicy = server.db.policies.find(policy);
|
||||||
if (!dbPolicy) {
|
if (!dbPolicy) {
|
||||||
server.create('policy', { id: policy });
|
server.create('policy', { id: policy });
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
token.update({ policyIds });
|
|
||||||
|
|
||||||
// Create a special policy with variables rules in place
|
|
||||||
if (token.id === '53cur3-v4r14bl35') {
|
|
||||||
const variableMakerPolicy = {
|
|
||||||
id: 'Variable Maker',
|
|
||||||
rules: `
|
|
||||||
# Allow read only access to the default namespace
|
|
||||||
namespace "*" {
|
|
||||||
policy = "read"
|
|
||||||
capabilities = ["list-jobs", "alloc-exec", "read-logs"]
|
|
||||||
variables {
|
|
||||||
# Base access is to all abilities for all variables
|
|
||||||
path "*" {
|
|
||||||
capabilities = ["list", "read", "destroy", "create"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
node {
|
|
||||||
policy = "read"
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
|
|
||||||
rulesJSON: {
|
|
||||||
Namespaces: [
|
|
||||||
{
|
|
||||||
Name: '*',
|
|
||||||
Capabilities: ['list-jobs', 'alloc-exec', 'read-logs'],
|
|
||||||
Variables: {
|
|
||||||
Paths: [
|
|
||||||
{
|
|
||||||
Capabilities: ['write', 'read', 'destroy', 'list'],
|
|
||||||
PathSpec: '*',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
server.create('policy', variableMakerPolicy);
|
|
||||||
token.policyIds.push(variableMakerPolicy.id);
|
|
||||||
}
|
|
||||||
if (token.id === 'f3w3r-53cur3-v4r14bl35') {
|
|
||||||
const variableViewerPolicy = {
|
|
||||||
id: 'Variable Viewer',
|
|
||||||
rules: `
|
|
||||||
# Allow read only access to the default namespace
|
|
||||||
namespace "*" {
|
|
||||||
policy = "read"
|
|
||||||
capabilities = ["list-jobs", "alloc-exec", "read-logs"]
|
|
||||||
variables {
|
|
||||||
# Base access is to all abilities for all variables
|
|
||||||
path "*" {
|
|
||||||
capabilities = ["list"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace "namespace-1" {
|
|
||||||
policy = "read"
|
|
||||||
capabilities = ["list-jobs", "alloc-exec", "read-logs"]
|
|
||||||
variables {
|
|
||||||
# Base access is to all abilities for all variables
|
|
||||||
path "*" {
|
|
||||||
capabilities = ["list", "read", "destroy", "create"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace "namespace-2" {
|
|
||||||
policy = "read"
|
|
||||||
capabilities = ["list-jobs", "alloc-exec", "read-logs"]
|
|
||||||
variables {
|
|
||||||
# Base access is to all abilities for all variables
|
|
||||||
path "blue/*" {
|
|
||||||
capabilities = ["list", "read", "destroy", "create"]
|
|
||||||
}
|
|
||||||
path "nomad/jobs/*" {
|
|
||||||
capabilities = ["list", "read", "create"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
node {
|
|
||||||
policy = "read"
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
|
|
||||||
rulesJSON: {
|
|
||||||
Namespaces: [
|
|
||||||
{
|
|
||||||
Name: '*',
|
|
||||||
Capabilities: ['list-jobs', 'alloc-exec', 'read-logs'],
|
|
||||||
Variables: {
|
|
||||||
Paths: [
|
|
||||||
{
|
|
||||||
Capabilities: ['list'],
|
|
||||||
PathSpec: '*',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: 'namespace-1',
|
|
||||||
Capabilities: ['list-jobs', 'alloc-exec', 'read-logs'],
|
|
||||||
Variables: {
|
|
||||||
Paths: [
|
|
||||||
{
|
|
||||||
Capabilities: ['list', 'read', 'destroy', 'create'],
|
|
||||||
PathSpec: '*',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: 'namespace-2',
|
|
||||||
Capabilities: ['list-jobs', 'alloc-exec', 'read-logs'],
|
|
||||||
Variables: {
|
|
||||||
Paths: [
|
|
||||||
{
|
|
||||||
Capabilities: ['list', 'read', 'destroy', 'create'],
|
|
||||||
PathSpec: 'blue/*',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Capabilities: ['list', 'read', 'create'],
|
|
||||||
PathSpec: 'nomad/jobs/*',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
server.create('policy', variableViewerPolicy);
|
|
||||||
token.policyIds.push(variableViewerPolicy.id);
|
|
||||||
}
|
|
||||||
if (token.id === '3XP1R35-1N-3L3V3N-M1NU735') {
|
|
||||||
token.update({
|
|
||||||
expirationTime: new Date(new Date().getTime() + 11 * 60 * 1000),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
token.update({ policyIds });
|
||||||
|
|
||||||
|
// Create a special policy with variables rules in place
|
||||||
|
if (token.id === '53cur3-v4r14bl35') {
|
||||||
|
const variableMakerPolicy = {
|
||||||
|
id: 'Variable-Maker',
|
||||||
|
rules: `
|
||||||
|
# Allow read only access to the default namespace
|
||||||
|
namespace "*" {
|
||||||
|
policy = "read"
|
||||||
|
capabilities = ["list-jobs", "alloc-exec", "read-logs"]
|
||||||
|
variables {
|
||||||
|
# Base access is to all abilities for all variables
|
||||||
|
path "*" {
|
||||||
|
capabilities = ["list", "read", "destroy", "create"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node {
|
||||||
|
policy = "read"
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
|
||||||
|
rulesJSON: {
|
||||||
|
Namespaces: [
|
||||||
|
{
|
||||||
|
Name: '*',
|
||||||
|
Capabilities: ['list-jobs', 'alloc-exec', 'read-logs'],
|
||||||
|
Variables: {
|
||||||
|
Paths: [
|
||||||
|
{
|
||||||
|
Capabilities: ['write', 'read', 'destroy', 'list'],
|
||||||
|
PathSpec: '*',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
server.create('policy', variableMakerPolicy);
|
||||||
|
token.policyIds.push(variableMakerPolicy.id);
|
||||||
|
}
|
||||||
|
if (token.id === 'f3w3r-53cur3-v4r14bl35') {
|
||||||
|
const variableViewerPolicy = {
|
||||||
|
id: 'Variable-Viewer',
|
||||||
|
rules: `
|
||||||
|
# Allow read only access to the default namespace
|
||||||
|
namespace "*" {
|
||||||
|
policy = "read"
|
||||||
|
capabilities = ["list-jobs", "alloc-exec", "read-logs"]
|
||||||
|
variables {
|
||||||
|
# Base access is to all abilities for all variables
|
||||||
|
path "*" {
|
||||||
|
capabilities = ["list"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace "namespace-1" {
|
||||||
|
policy = "read"
|
||||||
|
capabilities = ["list-jobs", "alloc-exec", "read-logs"]
|
||||||
|
variables {
|
||||||
|
# Base access is to all abilities for all variables
|
||||||
|
path "*" {
|
||||||
|
capabilities = ["list", "read", "destroy", "create"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace "namespace-2" {
|
||||||
|
policy = "read"
|
||||||
|
capabilities = ["list-jobs", "alloc-exec", "read-logs"]
|
||||||
|
variables {
|
||||||
|
# Base access is to all abilities for all variables
|
||||||
|
path "blue/*" {
|
||||||
|
capabilities = ["list", "read", "destroy", "create"]
|
||||||
|
}
|
||||||
|
path "nomad/jobs/*" {
|
||||||
|
capabilities = ["list", "read", "create"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node {
|
||||||
|
policy = "read"
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
|
||||||
|
rulesJSON: {
|
||||||
|
Namespaces: [
|
||||||
|
{
|
||||||
|
Name: '*',
|
||||||
|
Capabilities: ['list-jobs', 'alloc-exec', 'read-logs'],
|
||||||
|
Variables: {
|
||||||
|
Paths: [
|
||||||
|
{
|
||||||
|
Capabilities: ['list'],
|
||||||
|
PathSpec: '*',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: 'namespace-1',
|
||||||
|
Capabilities: ['list-jobs', 'alloc-exec', 'read-logs'],
|
||||||
|
Variables: {
|
||||||
|
Paths: [
|
||||||
|
{
|
||||||
|
Capabilities: ['list', 'read', 'destroy', 'create'],
|
||||||
|
PathSpec: '*',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: 'namespace-2',
|
||||||
|
Capabilities: ['list-jobs', 'alloc-exec', 'read-logs'],
|
||||||
|
Variables: {
|
||||||
|
Paths: [
|
||||||
|
{
|
||||||
|
Capabilities: ['list', 'read', 'destroy', 'create'],
|
||||||
|
PathSpec: 'blue/*',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Capabilities: ['list', 'read', 'create'],
|
||||||
|
PathSpec: 'nomad/jobs/*',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
server.create('policy', variableViewerPolicy);
|
||||||
|
token.policyIds.push(variableViewerPolicy.id);
|
||||||
|
}
|
||||||
|
if (token.id === '3XP1R35-1N-3L3V3N-M1NU735') {
|
||||||
|
token.update({
|
||||||
|
expirationTime: new Date(new Date().getTime() + 11 * 60 * 1000),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Model, belongsTo, hasMany } from 'ember-cli-mirage';
|
||||||
|
|
||||||
|
export default Model.extend({
|
||||||
|
policies: hasMany(),
|
||||||
|
roles: hasMany(),
|
||||||
|
});
|
|
@ -26,6 +26,7 @@ export const allScenarios = {
|
||||||
variableTestCluster,
|
variableTestCluster,
|
||||||
servicesTestCluster,
|
servicesTestCluster,
|
||||||
policiesTestCluster,
|
policiesTestCluster,
|
||||||
|
rolesTestCluster,
|
||||||
...topoScenarios,
|
...topoScenarios,
|
||||||
...sysbatchScenarios,
|
...sysbatchScenarios,
|
||||||
};
|
};
|
||||||
|
@ -96,6 +97,102 @@ function smallCluster(server) {
|
||||||
activeDeployment: true,
|
activeDeployment: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.create('policy', {
|
||||||
|
id: 'client-reader',
|
||||||
|
name: 'client-reader',
|
||||||
|
description: "Can read nodes and that's about it",
|
||||||
|
rulesJSON: {
|
||||||
|
Node: {
|
||||||
|
Policy: 'read',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: `# Allow node read access`,
|
||||||
|
});
|
||||||
|
|
||||||
|
server.create('policy', {
|
||||||
|
id: 'client-writer',
|
||||||
|
name: 'client-writer',
|
||||||
|
description: 'Can write to nodes',
|
||||||
|
rulesJSON: {
|
||||||
|
Node: {
|
||||||
|
Policy: 'write',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: `# Allow node write access`,
|
||||||
|
});
|
||||||
|
|
||||||
|
server.create('policy', {
|
||||||
|
id: 'job-reader',
|
||||||
|
name: 'job-reader',
|
||||||
|
description: "Can read jobs and that's about it",
|
||||||
|
rulesJSON: {
|
||||||
|
namespace: {
|
||||||
|
'*': {
|
||||||
|
policy: 'read',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: `# Job read access`,
|
||||||
|
});
|
||||||
|
|
||||||
|
server.create('policy', {
|
||||||
|
id: 'job-writer',
|
||||||
|
name: 'job-writer',
|
||||||
|
description: 'Can write jobs',
|
||||||
|
rulesJSON: {
|
||||||
|
Namespaces: [
|
||||||
|
{
|
||||||
|
Name: '*',
|
||||||
|
Policy: '',
|
||||||
|
Capabilities: ['submit-job'],
|
||||||
|
Variables: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
rules: `# Job write access`,
|
||||||
|
});
|
||||||
|
|
||||||
|
server.create('policy', {
|
||||||
|
id: 'variable-lister',
|
||||||
|
name: 'variable-lister',
|
||||||
|
description: 'Can list variables',
|
||||||
|
rulesJSON: {
|
||||||
|
namespace: {
|
||||||
|
'*': {
|
||||||
|
variables: {
|
||||||
|
path: {
|
||||||
|
capabilities: ['list'],
|
||||||
|
pathspec: '*',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: `# Variable list access`,
|
||||||
|
});
|
||||||
|
|
||||||
|
server.create('role', {
|
||||||
|
id: 'operator',
|
||||||
|
name: 'operator',
|
||||||
|
description: 'Can operate',
|
||||||
|
policyIds: ['client-reader', 'client-writer', 'job-reader', 'job-writer'],
|
||||||
|
});
|
||||||
|
|
||||||
|
server.create('role', {
|
||||||
|
id: 'sysadmin',
|
||||||
|
name: 'sysadmin',
|
||||||
|
description: 'Can modify nodes',
|
||||||
|
policyIds: ['client-reader', 'client-writer'],
|
||||||
|
});
|
||||||
|
|
||||||
|
server.create('token', {
|
||||||
|
type: 'client',
|
||||||
|
name: 'Tiarna Riarthóir',
|
||||||
|
id: 'administrator-token',
|
||||||
|
roleIds: ['operator', 'sysadmin'],
|
||||||
|
policyIds: ['variable-lister'],
|
||||||
|
});
|
||||||
|
|
||||||
//#region Active Deployment
|
//#region Active Deployment
|
||||||
|
|
||||||
const activelyDeployingJobGroups = 2;
|
const activelyDeployingJobGroups = 2;
|
||||||
|
@ -439,6 +536,218 @@ function policiesTestCluster(server) {
|
||||||
server.createList('agent', 3, 'withConsulLink', 'withVaultLink');
|
server.createList('agent', 3, 'withConsulLink', 'withVaultLink');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rolesTestCluster(server) {
|
||||||
|
faker.seed(1);
|
||||||
|
|
||||||
|
server.createList('agent', 3, 'withConsulLink', 'withVaultLink');
|
||||||
|
server.createList('node-pool', 2);
|
||||||
|
server.createList('node', 5);
|
||||||
|
server.createList('job', 5);
|
||||||
|
|
||||||
|
// createTokens(server);
|
||||||
|
|
||||||
|
// Create policies
|
||||||
|
const clientReaderPolicy = server.create('policy', {
|
||||||
|
id: 'client-reader',
|
||||||
|
name: 'client-reader',
|
||||||
|
description: "Can read nodes and that's about it",
|
||||||
|
rulesJSON: {
|
||||||
|
Node: {
|
||||||
|
Policy: 'read',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientWriterPolicy = server.create('policy', {
|
||||||
|
id: 'client-writer',
|
||||||
|
name: 'client-writer',
|
||||||
|
description: 'Can write to nodes',
|
||||||
|
rulesJSON: {
|
||||||
|
Node: {
|
||||||
|
Policy: 'write',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientDenierPolicy = server.create('policy', {
|
||||||
|
id: 'client-denier',
|
||||||
|
name: 'client-denier',
|
||||||
|
description: "Can't do anything with Clients",
|
||||||
|
rulesJSON: {
|
||||||
|
Node: {
|
||||||
|
Policy: 'deny',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const jobDenierPolicy = server.create('policy', {
|
||||||
|
id: 'job-denier',
|
||||||
|
name: 'job-denier',
|
||||||
|
description: "Can't do anything with Jobs",
|
||||||
|
rulesJSON: {
|
||||||
|
namespace: {
|
||||||
|
'*': {
|
||||||
|
policy: 'deny',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const operatorPolicy = server.create('policy', {
|
||||||
|
id: 'operator',
|
||||||
|
name: 'operator',
|
||||||
|
description: 'Can operate',
|
||||||
|
rulesJSON: {
|
||||||
|
operator: {
|
||||||
|
policy: 'write',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const jobReaderPolicy = server.create('policy', {
|
||||||
|
id: 'job-reader',
|
||||||
|
name: 'job-reader',
|
||||||
|
description: 'Can learn about jobs',
|
||||||
|
rulesJSON: {
|
||||||
|
namespace: {
|
||||||
|
'*': {
|
||||||
|
policy: 'read',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const highLevelJobPolicy = server.create('policy', {
|
||||||
|
id: 'job-writer',
|
||||||
|
name: 'job-writer',
|
||||||
|
description: 'Can do lots with jobs',
|
||||||
|
rulesJSON: {
|
||||||
|
Namespaces: [
|
||||||
|
{
|
||||||
|
Name: '*',
|
||||||
|
Policy: '',
|
||||||
|
Capabilities: ['submit-job'],
|
||||||
|
Variables: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create roles
|
||||||
|
const editorRole = server.create('role', {
|
||||||
|
id: 'editor',
|
||||||
|
name: 'editor',
|
||||||
|
description: 'Can edit things',
|
||||||
|
policyIds: [clientWriterPolicy.id],
|
||||||
|
});
|
||||||
|
|
||||||
|
const highLevelRole = server.create('role', {
|
||||||
|
id: 'high-level',
|
||||||
|
name: 'high-level',
|
||||||
|
description: 'Can do lots of things',
|
||||||
|
policyIds: [highLevelJobPolicy.id],
|
||||||
|
});
|
||||||
|
|
||||||
|
const readerRole = server.create('role', {
|
||||||
|
id: 'reader',
|
||||||
|
name: 'reader',
|
||||||
|
description: 'Can read things',
|
||||||
|
policyIds: [clientReaderPolicy.id, jobReaderPolicy.id],
|
||||||
|
});
|
||||||
|
|
||||||
|
const denierRole = server.create('role', {
|
||||||
|
id: 'denier',
|
||||||
|
name: 'denier',
|
||||||
|
description: "Can't do anything",
|
||||||
|
policyIds: [clientDenierPolicy.id, jobDenierPolicy.id],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create tokens
|
||||||
|
|
||||||
|
let managementToken = server.create('token', {
|
||||||
|
type: 'management',
|
||||||
|
name: 'Management Token',
|
||||||
|
});
|
||||||
|
|
||||||
|
let clientReaderToken = server.create('token', {
|
||||||
|
type: 'client',
|
||||||
|
name: "N. O'DeReader",
|
||||||
|
policyIds: [clientReaderPolicy.id],
|
||||||
|
});
|
||||||
|
|
||||||
|
let clientWriterToken = server.create('token', {
|
||||||
|
type: 'client',
|
||||||
|
name: "N. O'DeWriter",
|
||||||
|
policyIds: [clientWriterPolicy.id],
|
||||||
|
});
|
||||||
|
|
||||||
|
let dualPolicyToken = server.create('token', {
|
||||||
|
type: 'client',
|
||||||
|
name: 'Multi-policy Token',
|
||||||
|
policyIds: [clientReaderPolicy.id, clientWriterPolicy.id],
|
||||||
|
});
|
||||||
|
|
||||||
|
let highLevelViaPolicyToken = server.create('token', {
|
||||||
|
type: 'client',
|
||||||
|
name: 'High Level Policy Token',
|
||||||
|
policyIds: [highLevelJobPolicy.id],
|
||||||
|
});
|
||||||
|
|
||||||
|
let highLevelViaRoleToken = server.create('token', {
|
||||||
|
type: 'client',
|
||||||
|
name: 'High Level Role Token',
|
||||||
|
roleIds: [highLevelRole.id],
|
||||||
|
});
|
||||||
|
|
||||||
|
let policyAndRoleToken = server.create('token', {
|
||||||
|
type: 'client',
|
||||||
|
name: 'Policy And Role Token',
|
||||||
|
policyIds: [operatorPolicy.id],
|
||||||
|
roleIds: [readerRole.id],
|
||||||
|
});
|
||||||
|
|
||||||
|
let multiRoleToken = server.create('token', {
|
||||||
|
type: 'client',
|
||||||
|
name: 'Multi Role Token',
|
||||||
|
roleIds: [editorRole.id, highLevelRole.id],
|
||||||
|
});
|
||||||
|
|
||||||
|
let multiRoleAndPolicyToken = server.create('token', {
|
||||||
|
type: 'client',
|
||||||
|
name: 'Multi Role And Policy Token',
|
||||||
|
roleIds: [editorRole.id, highLevelRole.id],
|
||||||
|
policyIds: [clientWriterPolicy.id], // also included within editorRole, so redundant here.
|
||||||
|
});
|
||||||
|
|
||||||
|
let noClientsViaPolicyToken = server.create('token', {
|
||||||
|
type: 'client',
|
||||||
|
name: 'Clientless Policy Token',
|
||||||
|
policyIds: [clientDenierPolicy.id],
|
||||||
|
});
|
||||||
|
|
||||||
|
let noClientsViaRoleToken = server.create('token', {
|
||||||
|
type: 'client',
|
||||||
|
name: 'Clientless Role Token',
|
||||||
|
roleIds: [denierRole.id],
|
||||||
|
});
|
||||||
|
|
||||||
|
// malleable test token
|
||||||
|
server.create('token', {
|
||||||
|
name: 'Clay-Token',
|
||||||
|
id: 'cl4y-t0k3n',
|
||||||
|
type: 'client',
|
||||||
|
policyIds: [clientReaderPolicy.id, operatorPolicy.id],
|
||||||
|
roleIds: [editorRole.id],
|
||||||
|
expirationTime: new Date(new Date().getTime() + 60 * 60 * 1000),
|
||||||
|
});
|
||||||
|
|
||||||
|
logTokens(server);
|
||||||
|
|
||||||
|
server.create('auth-method', { name: 'vault' });
|
||||||
|
|
||||||
|
server.createList('agent', 3, 'withConsulLink', 'withVaultLink');
|
||||||
|
}
|
||||||
|
|
||||||
function servicesTestCluster(server) {
|
function servicesTestCluster(server) {
|
||||||
faker.seed(1);
|
faker.seed(1);
|
||||||
server.create('feature', { name: 'Dynamic Application Sizing' });
|
server.create('feature', { name: 'Dynamic Application Sizing' });
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
// @ts-check
|
||||||
|
import ApplicationSerializer from './application';
|
||||||
|
|
||||||
|
export default ApplicationSerializer.extend({
|
||||||
|
serializeIds: 'always',
|
||||||
|
|
||||||
|
serialize() {
|
||||||
|
var json = ApplicationSerializer.prototype.serialize.apply(this, arguments);
|
||||||
|
if (json instanceof Array) {
|
||||||
|
json.forEach(serializeRole);
|
||||||
|
} else {
|
||||||
|
serializeRole(json);
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function serializeRole(role) {
|
||||||
|
role.Policies = (role.Policies || []).map((policy) => {
|
||||||
|
return { ID: policy, Name: policy };
|
||||||
|
});
|
||||||
|
delete role.PolicyIDs;
|
||||||
|
return role;
|
||||||
|
}
|
|
@ -12,6 +12,29 @@ export default ApplicationSerializer.extend({
|
||||||
if (relationship === 'policies') {
|
if (relationship === 'policies') {
|
||||||
return 'Policies';
|
return 'Policies';
|
||||||
}
|
}
|
||||||
return ApplicationSerializer.prototype.keyForRelationshipIds.apply(this, arguments);
|
if (relationship === 'roles') {
|
||||||
|
return 'Roles';
|
||||||
|
}
|
||||||
|
return ApplicationSerializer.prototype.keyForRelationshipIds.apply(
|
||||||
|
this,
|
||||||
|
arguments
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
serialize() {
|
||||||
|
var json = ApplicationSerializer.prototype.serialize.apply(this, arguments);
|
||||||
|
if (json instanceof Array) {
|
||||||
|
json.forEach(serializeToken);
|
||||||
|
} else {
|
||||||
|
serializeToken(json);
|
||||||
|
}
|
||||||
|
return json;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function serializeToken(token) {
|
||||||
|
token.Roles = (token.Roles || []).map((role) => {
|
||||||
|
return { ID: role, Name: role };
|
||||||
|
});
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
|
@ -87,7 +87,7 @@
|
||||||
"ember-cli-sri": "^2.1.1",
|
"ember-cli-sri": "^2.1.1",
|
||||||
"ember-cli-string-helpers": "^6.1.0",
|
"ember-cli-string-helpers": "^6.1.0",
|
||||||
"ember-cli-terser": "^4.0.2",
|
"ember-cli-terser": "^4.0.2",
|
||||||
"ember-click-outside": "^3.0.0",
|
"ember-click-outside": "^5.0.0",
|
||||||
"ember-composable-helpers": "^5.0.0",
|
"ember-composable-helpers": "^5.0.0",
|
||||||
"ember-concurrency": "^2.2.1",
|
"ember-concurrency": "^2.2.1",
|
||||||
"ember-copy": "^2.0.1",
|
"ember-copy": "^2.0.1",
|
||||||
|
@ -178,7 +178,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-string-parser": "^7.19.4",
|
"@babel/helper-string-parser": "^7.19.4",
|
||||||
"@hashicorp/design-system-components": "^2.6.0",
|
"@hashicorp/design-system-components": "^2.12.0",
|
||||||
"@hashicorp/ember-flight-icons": "^3.0.4",
|
"@hashicorp/ember-flight-icons": "^3.0.4",
|
||||||
"@percy/cli": "^1.6.1",
|
"@percy/cli": "^1.6.1",
|
||||||
"@percy/ember": "^3.0.0",
|
"@percy/ember": "^3.0.0",
|
||||||
|
|
|
@ -0,0 +1,150 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { module, test } from 'qunit';
|
||||||
|
import { currentURL, triggerKeyEvent } from '@ember/test-helpers';
|
||||||
|
import { setupApplicationTest } from 'ember-qunit';
|
||||||
|
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||||
|
import AccessControl from 'nomad-ui/tests/pages/access-control';
|
||||||
|
import Tokens from 'nomad-ui/tests/pages/settings/tokens';
|
||||||
|
import { allScenarios } from '../../mirage/scenarios/default';
|
||||||
|
import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
|
||||||
|
|
||||||
|
// Several related tests within Access Control are contained in the Tokens, Roles,
|
||||||
|
// and Policies acceptance tests.
|
||||||
|
|
||||||
|
module('Acceptance | access control', function (hooks) {
|
||||||
|
setupApplicationTest(hooks);
|
||||||
|
setupMirage(hooks);
|
||||||
|
|
||||||
|
hooks.beforeEach(function () {
|
||||||
|
window.localStorage.clear();
|
||||||
|
window.sessionStorage.clear();
|
||||||
|
// server.create('token');
|
||||||
|
allScenarios.rolesTestCluster(server);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Access Control is only accessible by a management user', async function (assert) {
|
||||||
|
assert.expect(7);
|
||||||
|
await AccessControl.visit();
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
currentURL(),
|
||||||
|
'/jobs',
|
||||||
|
'redirected to the jobs page if a non-management token on /access-control'
|
||||||
|
);
|
||||||
|
|
||||||
|
await AccessControl.visitTokens();
|
||||||
|
assert.equal(
|
||||||
|
currentURL(),
|
||||||
|
'/jobs',
|
||||||
|
'redirected to the jobs page if a non-management token on /tokens'
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.dom('[data-test-gutter-link="access-control"]').doesNotExist();
|
||||||
|
|
||||||
|
await Tokens.visit();
|
||||||
|
const managementToken = server.db.tokens.findBy(
|
||||||
|
(t) => t.type === 'management'
|
||||||
|
);
|
||||||
|
const { secretId } = managementToken;
|
||||||
|
await Tokens.secret(secretId).submit();
|
||||||
|
|
||||||
|
assert.dom('[data-test-gutter-link="access-control"]').exists();
|
||||||
|
|
||||||
|
await AccessControl.visit();
|
||||||
|
assert.equal(
|
||||||
|
currentURL(),
|
||||||
|
'/access-control',
|
||||||
|
'management token can access /access-control'
|
||||||
|
);
|
||||||
|
|
||||||
|
await a11yAudit(assert);
|
||||||
|
|
||||||
|
await AccessControl.visitTokens();
|
||||||
|
assert.equal(
|
||||||
|
currentURL(),
|
||||||
|
'/access-control/tokens',
|
||||||
|
'management token can access /access-control/tokens'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Access control index content', async function (assert) {
|
||||||
|
await Tokens.visit();
|
||||||
|
const managementToken = server.db.tokens.findBy(
|
||||||
|
(t) => t.type === 'management'
|
||||||
|
);
|
||||||
|
const { secretId } = managementToken;
|
||||||
|
await Tokens.secret(secretId).submit();
|
||||||
|
|
||||||
|
await AccessControl.visit();
|
||||||
|
assert.dom('[data-test-tokens-card]').exists();
|
||||||
|
assert.dom('[data-test-roles-card]').exists();
|
||||||
|
assert.dom('[data-test-policies-card]').exists();
|
||||||
|
|
||||||
|
const numberOfTokens = server.db.tokens.length;
|
||||||
|
const numberOfRoles = server.db.roles.length;
|
||||||
|
const numberOfPolicies = server.db.policies.length;
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom('[data-test-tokens-card] a')
|
||||||
|
.includesText(`${numberOfTokens} Tokens`);
|
||||||
|
assert
|
||||||
|
.dom('[data-test-roles-card] a')
|
||||||
|
.includesText(`${numberOfRoles} Roles`);
|
||||||
|
assert
|
||||||
|
.dom('[data-test-policies-card] a')
|
||||||
|
.includesText(`${numberOfPolicies} Policies`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Access control subnav', async function (assert) {
|
||||||
|
await Tokens.visit();
|
||||||
|
const managementToken = server.db.tokens.findBy(
|
||||||
|
(t) => t.type === 'management'
|
||||||
|
);
|
||||||
|
const { secretId } = managementToken;
|
||||||
|
await Tokens.secret(secretId).submit();
|
||||||
|
|
||||||
|
await AccessControl.visit();
|
||||||
|
|
||||||
|
assert.equal(currentURL(), '/access-control');
|
||||||
|
|
||||||
|
await triggerKeyEvent('.page-layout', 'keydown', 'ArrowRight', {
|
||||||
|
shiftKey: true,
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
currentURL(),
|
||||||
|
`/access-control/tokens`,
|
||||||
|
'Shift+ArrowRight takes you to the next tab (Tokens)'
|
||||||
|
);
|
||||||
|
|
||||||
|
await triggerKeyEvent('.page-layout', 'keydown', 'ArrowRight', {
|
||||||
|
shiftKey: true,
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
currentURL(),
|
||||||
|
`/access-control/roles`,
|
||||||
|
'Shift+ArrowRight takes you to the next tab (Roles)'
|
||||||
|
);
|
||||||
|
|
||||||
|
await triggerKeyEvent('.page-layout', 'keydown', 'ArrowRight', {
|
||||||
|
shiftKey: true,
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
currentURL(),
|
||||||
|
`/access-control/policies`,
|
||||||
|
'Shift+ArrowRight takes you to the next tab (Policies)'
|
||||||
|
);
|
||||||
|
|
||||||
|
await triggerKeyEvent('.page-layout', 'keydown', 'ArrowRight', {
|
||||||
|
shiftKey: true,
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
currentURL(),
|
||||||
|
`/access-control`,
|
||||||
|
'Shift+ArrowLeft takes you back to the Access Control index page'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -74,7 +74,7 @@ module('Acceptance | global header', function (hooks) {
|
||||||
assert.false(Layout.navbar.end.signInLink.isVisible);
|
assert.false(Layout.navbar.end.signInLink.isVisible);
|
||||||
await Layout.navbar.end.profileDropdown.open();
|
await Layout.navbar.end.profileDropdown.open();
|
||||||
|
|
||||||
await click('.dropdown-options .ember-power-select-option:nth-child(1)');
|
await click('[data-test-profile-dropdown-profile-link]');
|
||||||
assert.equal(
|
assert.equal(
|
||||||
currentURL(),
|
currentURL(),
|
||||||
'/settings/tokens',
|
'/settings/tokens',
|
||||||
|
@ -82,7 +82,7 @@ module('Acceptance | global header', function (hooks) {
|
||||||
);
|
);
|
||||||
|
|
||||||
await Layout.navbar.end.profileDropdown.open();
|
await Layout.navbar.end.profileDropdown.open();
|
||||||
await click('.dropdown-options .ember-power-select-option:nth-child(2)');
|
await click('[data-test-profile-dropdown-sign-out-link]');
|
||||||
assert.equal(window.localStorage.nomadTokenSecret, null, 'Token is wiped');
|
assert.equal(window.localStorage.nomadTokenSecret, null, 'Token is wiped');
|
||||||
assert.equal(currentURL(), '/jobs', 'After signout, back on the jobs page');
|
assert.equal(currentURL(), '/jobs', 'After signout, back on the jobs page');
|
||||||
});
|
});
|
||||||
|
|
|
@ -19,9 +19,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('/policies');
|
await visit('/access-control/policies');
|
||||||
assert.dom('[data-test-gutter-link="policies"]').exists();
|
assert.dom('[data-test-gutter-link="access-control"]').exists();
|
||||||
assert.equal(currentURL(), '/policies');
|
assert.equal(currentURL(), '/access-control/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 });
|
||||||
|
@ -34,9 +34,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('/policies');
|
await visit('/access-control/policies');
|
||||||
assert.equal(currentURL(), '/jobs');
|
assert.equal(currentURL(), '/jobs');
|
||||||
assert.dom('[data-test-gutter-link="policies"]').doesNotExist();
|
assert.dom('[data-test-gutter-link="access-control"]').doesNotExist();
|
||||||
// Reset Token
|
// Reset Token
|
||||||
window.localStorage.nomadTokenSecret = null;
|
window.localStorage.nomadTokenSecret = null;
|
||||||
});
|
});
|
||||||
|
@ -44,51 +44,80 @@ 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('/policies');
|
await visit('/access-control/policies');
|
||||||
await click('[data-test-policy-row]:first-child');
|
await click('[data-test-policy-row]:first-child a');
|
||||||
assert.equal(currentURL(), `/policies/${server.db.policies[0].name}`);
|
// Table sorts by name by default
|
||||||
|
let firstPolicy = server.db.policies.sort((a, b) => {
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
})[0];
|
||||||
|
assert.equal(currentURL(), `/access-control/policies/${firstPolicy.name}`);
|
||||||
assert.dom('[data-test-policy-editor]').exists();
|
assert.dom('[data-test-policy-editor]').exists();
|
||||||
assert.dom('[data-test-title]').includesText(server.db.policies[0].name);
|
assert.dom('[data-test-title]').includesText(firstPolicy.name);
|
||||||
await click('button[type="submit"]');
|
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(),
|
||||||
`/policies/${server.db.policies[0].name}`,
|
`/access-control/policies/${firstPolicy.name}`,
|
||||||
'remain on page after save'
|
'remain on page after save'
|
||||||
);
|
);
|
||||||
// Reset Token
|
// Reset Token
|
||||||
window.localStorage.nomadTokenSecret = null;
|
window.localStorage.nomadTokenSecret = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Creating a test token', async function (assert) {
|
||||||
|
allScenarios.policiesTestCluster(server);
|
||||||
|
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
||||||
|
await visit('/access-control/policies');
|
||||||
|
await click('[data-test-policy-name="Variable-Maker"]');
|
||||||
|
assert.equal(currentURL(), '/access-control/policies/Variable-Maker');
|
||||||
|
await click('[data-test-create-test-token]');
|
||||||
|
assert.dom('.flash-message.alert-success').exists();
|
||||||
|
assert
|
||||||
|
.dom('[data-test-token-name="Example Token for Variable-Maker"]')
|
||||||
|
.exists('Test token is created and visible');
|
||||||
|
const newTokenRow = [
|
||||||
|
...findAll('[data-test-token-name="Example Token for Variable-Maker"]'),
|
||||||
|
][0].parentElement;
|
||||||
|
const newTokenDeleteButton = newTokenRow.querySelector(
|
||||||
|
'[data-test-delete-token-button]'
|
||||||
|
);
|
||||||
|
await click(newTokenDeleteButton);
|
||||||
|
assert
|
||||||
|
.dom('[data-test-token-name="Example Token for Variable-Maker"]')
|
||||||
|
.doesNotExist('Token is deleted');
|
||||||
|
// Reset Token
|
||||||
|
window.localStorage.nomadTokenSecret = null;
|
||||||
|
});
|
||||||
|
|
||||||
test('Creating a new policy', async function (assert) {
|
test('Creating a new policy', async function (assert) {
|
||||||
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('/policies');
|
await visit('/access-control/policies');
|
||||||
await click('[data-test-create-policy]');
|
await click('[data-test-create-policy]');
|
||||||
assert.equal(currentURL(), '/policies/new');
|
assert.equal(currentURL(), '/access-control/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[type="submit"]');
|
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(), '/policies/new');
|
assert.equal(currentURL(), '/access-control/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[type="submit"]');
|
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(),
|
||||||
'/policies/My-Fun-Policy',
|
'/access-control/policies/My-Fun-Policy',
|
||||||
'redirected to the now-created policy'
|
'redirected to the now-created policy'
|
||||||
);
|
);
|
||||||
await visit('/policies');
|
await visit('/access-control/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(), '/policies/My-Fun-Policy');
|
assert.equal(currentURL(), '/access-control/policies/My-Fun-Policy');
|
||||||
await percySnapshot(assert);
|
await percySnapshot(assert);
|
||||||
// Reset Token
|
// Reset Token
|
||||||
window.localStorage.nomadTokenSecret = null;
|
window.localStorage.nomadTokenSecret = null;
|
||||||
|
@ -97,20 +126,44 @@ 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('/policies');
|
await visit('/access-control/policies');
|
||||||
const firstPolicyName = server.db.policies[0].name;
|
let firstPolicy = server.db.policies.sort((a, b) => {
|
||||||
const firstPolicyRow = [...findAll('[data-test-policy-name]')].filter(
|
return a.name.localeCompare(b.name);
|
||||||
|
})[0];
|
||||||
|
|
||||||
|
const firstPolicyName = firstPolicy.name;
|
||||||
|
const firstPolicyLink = [...findAll('[data-test-policy-name]')].filter(
|
||||||
(row) => row.textContent.includes(firstPolicyName)
|
(row) => row.textContent.includes(firstPolicyName)
|
||||||
)[0];
|
)[0];
|
||||||
await click(firstPolicyRow);
|
await click(firstPolicyLink);
|
||||||
assert.equal(currentURL(), `/policies/${firstPolicyName}`);
|
assert.equal(currentURL(), `/access-control/policies/${firstPolicyName}`);
|
||||||
await click('[data-test-delete-button] button');
|
await click('[data-test-delete-policy]');
|
||||||
assert.dom('[data-test-confirm-button]').exists();
|
|
||||||
await click('[data-test-confirm-button]');
|
|
||||||
assert.dom('.flash-message.alert-success').exists();
|
assert.dom('.flash-message.alert-success').exists();
|
||||||
assert.equal(currentURL(), '/policies');
|
assert.equal(currentURL(), '/access-control/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;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Policies Index', async function (assert) {
|
||||||
|
allScenarios.policiesTestCluster(server);
|
||||||
|
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
||||||
|
await visit('/access-control/policies');
|
||||||
|
// Table contains every policy in db
|
||||||
|
assert
|
||||||
|
.dom('[data-test-policy-row]')
|
||||||
|
.exists({ count: server.db.policies.length });
|
||||||
|
|
||||||
|
assert.dom('[data-test-empty-policies-list-headline]').doesNotExist();
|
||||||
|
|
||||||
|
// Deleting all policies results in a message
|
||||||
|
const policyRows = findAll('[data-test-policy-row]');
|
||||||
|
for (const row of policyRows) {
|
||||||
|
const deleteButton = row.querySelector('[data-test-delete-policy]');
|
||||||
|
await click(deleteButton);
|
||||||
|
}
|
||||||
|
assert.dom('[data-test-empty-policies-list-headline]').exists();
|
||||||
|
// Reset Token
|
||||||
|
window.localStorage.nomadTokenSecret = null;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -194,29 +194,34 @@ module('Acceptance | regions (many)', function (hooks) {
|
||||||
await JobsList.jobs.objectAt(0).clickRow();
|
await JobsList.jobs.objectAt(0).clickRow();
|
||||||
await Layout.gutter.visitClients();
|
await Layout.gutter.visitClients();
|
||||||
await Layout.gutter.visitServers();
|
await Layout.gutter.visitServers();
|
||||||
const [
|
|
||||||
,
|
const regionsRequest = server.pretender.handledRequests.find((req) =>
|
||||||
,
|
req.responseURL.includes('/v1/regions')
|
||||||
,
|
);
|
||||||
// License request
|
const licenseRequest = server.pretender.handledRequests.find((req) =>
|
||||||
// Token/policies request
|
req.responseURL.includes('/v1/operator/license')
|
||||||
// Search feature detection
|
);
|
||||||
regionsRequest,
|
const appRequests = server.pretender.handledRequests.filter(
|
||||||
defaultRegionRequest,
|
(req) =>
|
||||||
...appRequests
|
!req.responseURL.includes('/v1/regions') &&
|
||||||
] = server.pretender.handledRequests;
|
!req.responseURL.includes('/v1/operator/license')
|
||||||
|
);
|
||||||
|
|
||||||
assert.notOk(
|
assert.notOk(
|
||||||
regionsRequest.url.includes('region='),
|
regionsRequest.url.includes('region='),
|
||||||
'The regions request is made without a region qp'
|
'The regions request is made without a region qp'
|
||||||
);
|
);
|
||||||
assert.notOk(
|
assert.notOk(
|
||||||
defaultRegionRequest.url.includes('region='),
|
licenseRequest.url.includes('region='),
|
||||||
'The default region request is made without a region qp'
|
'The default region request is made without a region qp'
|
||||||
);
|
);
|
||||||
|
|
||||||
appRequests.forEach((req) => {
|
appRequests.forEach((req) => {
|
||||||
if (req.url === '/v1/agent/self') {
|
if (
|
||||||
|
req.url === '/v1/agent/self' ||
|
||||||
|
req.url === '/v1/acl/token/self' ||
|
||||||
|
req.url === '/v1/agent/members'
|
||||||
|
) {
|
||||||
assert.notOk(req.url.includes('region='), `(no region) ${req.url}`);
|
assert.notOk(req.url.includes('region='), `(no region) ${req.url}`);
|
||||||
} else {
|
} else {
|
||||||
assert.ok(req.url.includes(`region=${region}`), req.url);
|
assert.ok(req.url.includes(`region=${region}`), req.url);
|
||||||
|
|
|
@ -0,0 +1,295 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { module, test } from 'qunit';
|
||||||
|
import { findAll, fillIn, find, click, currentURL } from '@ember/test-helpers';
|
||||||
|
import { setupApplicationTest } from 'ember-qunit';
|
||||||
|
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||||
|
import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
|
||||||
|
import { allScenarios } from '../../mirage/scenarios/default';
|
||||||
|
import Tokens from 'nomad-ui/tests/pages/settings/tokens';
|
||||||
|
import AccessControl from 'nomad-ui/tests/pages/access-control';
|
||||||
|
import percySnapshot from '@percy/ember';
|
||||||
|
|
||||||
|
module('Acceptance | roles', function (hooks) {
|
||||||
|
setupApplicationTest(hooks);
|
||||||
|
setupMirage(hooks);
|
||||||
|
|
||||||
|
hooks.beforeEach(async function () {
|
||||||
|
window.localStorage.clear();
|
||||||
|
window.sessionStorage.clear();
|
||||||
|
allScenarios.rolesTestCluster(server);
|
||||||
|
await Tokens.visit();
|
||||||
|
const managementToken = server.db.tokens.findBy(
|
||||||
|
(t) => t.type === 'management'
|
||||||
|
);
|
||||||
|
const { secretId } = managementToken;
|
||||||
|
await Tokens.secret(secretId).submit();
|
||||||
|
await AccessControl.visitRoles();
|
||||||
|
});
|
||||||
|
|
||||||
|
hooks.afterEach(async function () {
|
||||||
|
await Tokens.visit();
|
||||||
|
await Tokens.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Roles index, general', async function (assert) {
|
||||||
|
assert.expect(3);
|
||||||
|
await a11yAudit(assert);
|
||||||
|
|
||||||
|
assert.equal(currentURL(), '/access-control/roles');
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom('[data-test-role-row]')
|
||||||
|
.exists({ count: server.db.roles.length });
|
||||||
|
|
||||||
|
await percySnapshot(assert);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Roles index: deletion', async function (assert) {
|
||||||
|
// Delete every role
|
||||||
|
assert
|
||||||
|
.dom('[data-test-empty-role-list-headline]')
|
||||||
|
.doesNotExist('no empty state');
|
||||||
|
const roleRows = findAll('[data-test-role-row]');
|
||||||
|
for (const row of roleRows) {
|
||||||
|
const deleteButton = row.querySelector('[data-test-delete-role]');
|
||||||
|
await click(deleteButton);
|
||||||
|
}
|
||||||
|
// there should be as many success messages as there were roles
|
||||||
|
assert
|
||||||
|
.dom('.flash-message.alert-success')
|
||||||
|
.exists({ count: roleRows.length });
|
||||||
|
|
||||||
|
assert.dom('[data-test-empty-role-list-headline]').exists('empty state');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Roles have policies lists', async function (assert) {
|
||||||
|
const role = server.db.roles.findBy((r) => r.name === 'reader');
|
||||||
|
const roleRow = find(`[data-test-role-row="${role.name}"]`);
|
||||||
|
const rolePoliciesCell = roleRow.querySelector('[data-test-role-policies]');
|
||||||
|
const policiesCellTags = rolePoliciesCell
|
||||||
|
.querySelector('.tag-group')
|
||||||
|
.querySelectorAll('span');
|
||||||
|
assert.equal(policiesCellTags.length, 2);
|
||||||
|
assert.equal(policiesCellTags[0].textContent.trim(), 'client-reader');
|
||||||
|
assert.equal(policiesCellTags[1].textContent.trim(), 'job-reader');
|
||||||
|
|
||||||
|
await click(policiesCellTags[0].querySelector('a'));
|
||||||
|
assert.equal(currentURL(), '/access-control/policies/client-reader');
|
||||||
|
assert.dom('[data-test-title]').containsText('client-reader');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Edit Role: Name and Description', async function (assert) {
|
||||||
|
assert.expect(8);
|
||||||
|
const role = server.db.roles.findBy((r) => r.name === 'reader');
|
||||||
|
await click('[data-test-role-name="reader"] a');
|
||||||
|
assert.equal(currentURL(), `/access-control/roles/${role.id}`);
|
||||||
|
|
||||||
|
assert.dom('[data-test-role-name-input]').hasValue(role.name);
|
||||||
|
assert.dom('[data-test-role-description-input]').hasValue(role.description);
|
||||||
|
assert.dom('[data-test-role-policies]').exists();
|
||||||
|
|
||||||
|
// Modify the name and description
|
||||||
|
await fillIn('[data-test-role-name-input]', 'reader-edited');
|
||||||
|
await fillIn('[data-test-role-description-input]', 'edited description');
|
||||||
|
await click('button[data-test-save-role]');
|
||||||
|
assert.dom('.flash-message.alert-success').exists();
|
||||||
|
assert.equal(
|
||||||
|
currentURL(),
|
||||||
|
`/access-control/roles/${role.name}`,
|
||||||
|
'remain on page after save'
|
||||||
|
);
|
||||||
|
await percySnapshot(assert);
|
||||||
|
|
||||||
|
// Go back to the roles index
|
||||||
|
await AccessControl.visitRoles();
|
||||||
|
let readerRoleRow = find('[data-test-role-row="reader-edited"]');
|
||||||
|
assert.dom(readerRoleRow).exists();
|
||||||
|
assert.equal(
|
||||||
|
readerRoleRow
|
||||||
|
.querySelector('[data-test-role-description]')
|
||||||
|
.textContent.trim(),
|
||||||
|
'edited description'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Edit Role: Policies', async function (assert) {
|
||||||
|
const role = server.db.roles.findBy((r) => r.name === 'reader');
|
||||||
|
await click('[data-test-role-name="reader"] a');
|
||||||
|
assert.equal(currentURL(), `/access-control/roles/${role.id}`);
|
||||||
|
|
||||||
|
// Policies table is sortable
|
||||||
|
|
||||||
|
const nameCells = findAll('[data-test-policy-name]');
|
||||||
|
const nameCellText = nameCells.map((cell) => cell.textContent.trim());
|
||||||
|
const sortedNameCellText = nameCellText.slice().sort();
|
||||||
|
assert.deepEqual(
|
||||||
|
nameCellText,
|
||||||
|
sortedNameCellText,
|
||||||
|
'Policy names are sorted alphabetically'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Click on the second thead tr th to reverse
|
||||||
|
assert
|
||||||
|
.dom('table[data-test-role-policies] thead tr th:nth-child(2)')
|
||||||
|
.hasAttribute('aria-sort', 'ascending');
|
||||||
|
// await click('table[data-test-role-policies] thead tr th:nth-child(2)');
|
||||||
|
// above didnt work, another way?
|
||||||
|
await click('[data-test-role-policies] thead tr th:nth-child(2) button');
|
||||||
|
assert
|
||||||
|
.dom('table[data-test-role-policies] thead tr th:nth-child(2)')
|
||||||
|
.hasAttribute('aria-sort', 'descending');
|
||||||
|
|
||||||
|
const reversedNameCells = findAll('[data-test-policy-name]');
|
||||||
|
const reversedNameCellText = reversedNameCells.map((cell) =>
|
||||||
|
cell.textContent.trim()
|
||||||
|
);
|
||||||
|
const reversedSortedNameCellText = nameCellText.slice().sort().reverse();
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
reversedNameCellText,
|
||||||
|
reversedSortedNameCellText,
|
||||||
|
'Names are reversed alphabetically after click'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Make sure the correct policies are checked
|
||||||
|
const rolePolicies = role.policyIds;
|
||||||
|
// All possible policies are shown
|
||||||
|
const allPolicies = server.db.policies;
|
||||||
|
assert.equal(
|
||||||
|
findAll('[data-test-role-policies] tbody tr').length,
|
||||||
|
allPolicies.length,
|
||||||
|
'all policies are shown'
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkedPolicyRows = findAll(
|
||||||
|
'[data-test-role-policies] tbody tr input:checked'
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
checkedPolicyRows.length,
|
||||||
|
rolePolicies.length,
|
||||||
|
'correct number of policies are checked'
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkedPolicyNames = checkedPolicyRows.map((row) =>
|
||||||
|
row
|
||||||
|
.closest('tr')
|
||||||
|
.querySelector('[data-test-policy-name]')
|
||||||
|
.textContent.trim()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
checkedPolicyNames.sort(),
|
||||||
|
rolePolicies.sort(),
|
||||||
|
'All policies belonging to this role are checked'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Try de-selecting all policies and saving
|
||||||
|
checkedPolicyRows.forEach((row) => row.click());
|
||||||
|
await click('button[data-test-save-role]');
|
||||||
|
assert
|
||||||
|
.dom('.flash-message.alert-critical')
|
||||||
|
.exists('Doesnt let you save with no policies selected');
|
||||||
|
|
||||||
|
// Check all policies
|
||||||
|
findAll('[data-test-role-policies] tbody tr input').forEach((row) =>
|
||||||
|
row.click()
|
||||||
|
);
|
||||||
|
await click('button[data-test-save-role]');
|
||||||
|
assert.dom('.flash-message.alert-success').exists();
|
||||||
|
|
||||||
|
await AccessControl.visitRoles();
|
||||||
|
const readerRoleRow = find('[data-test-role-row="reader"]');
|
||||||
|
const readerRolePolicies = readerRoleRow
|
||||||
|
.querySelector('[data-test-role-policies]')
|
||||||
|
.querySelectorAll('span');
|
||||||
|
assert.equal(
|
||||||
|
readerRolePolicies.length,
|
||||||
|
allPolicies.length,
|
||||||
|
'all policies are attached to the role at index level'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Edit Role: Tokens', async function (assert) {
|
||||||
|
assert.expect(10);
|
||||||
|
const role = server.db.roles.findBy((r) => r.name === 'reader');
|
||||||
|
|
||||||
|
await click('[data-test-role-name="reader"] a');
|
||||||
|
assert.equal(currentURL(), `/access-control/roles/${role.id}`);
|
||||||
|
assert.dom('table.tokens').exists();
|
||||||
|
|
||||||
|
// "Reader" role has a single token with it applied by default
|
||||||
|
assert.dom('[data-test-role-token-row]').exists({ count: 1 });
|
||||||
|
|
||||||
|
// Delete it; should get a nice No Tokens message
|
||||||
|
await click('[data-test-delete-token-button]');
|
||||||
|
assert.dom('.flash-message.alert-success').exists();
|
||||||
|
assert.dom('[data-test-role-token-row]').doesNotExist();
|
||||||
|
assert.dom('[data-test-empty-role-list-headline]').exists();
|
||||||
|
// Create two test tokens
|
||||||
|
await click('[data-test-create-test-token]');
|
||||||
|
assert.dom('[data-test-empty-role-list-headline]').doesNotExist();
|
||||||
|
await click('[data-test-create-test-token]');
|
||||||
|
assert
|
||||||
|
.dom('[data-test-role-token-row]')
|
||||||
|
.exists({ count: 2 }, 'Test tokens are included on the page');
|
||||||
|
assert
|
||||||
|
.dom('[data-test-role-token-row]:last-child [data-test-token-name]')
|
||||||
|
.hasText(`Example Token for ${role.name}`);
|
||||||
|
|
||||||
|
await percySnapshot(assert);
|
||||||
|
|
||||||
|
await AccessControl.visitTokens();
|
||||||
|
assert
|
||||||
|
.dom('[data-test-token-name="Example Token for reader"]')
|
||||||
|
.exists(
|
||||||
|
{ count: 2 },
|
||||||
|
'The two newly-created tokens are listed on the tokens index page'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
test('Edit Role: Deletion', async function (assert) {
|
||||||
|
const role = server.db.roles.findBy((r) => r.name === 'reader');
|
||||||
|
await click('[data-test-role-name="reader"] a');
|
||||||
|
assert.equal(currentURL(), `/access-control/roles/${role.id}`);
|
||||||
|
await click('[data-test-delete-role]');
|
||||||
|
assert.dom('.flash-message.alert-success').exists();
|
||||||
|
assert.equal(currentURL(), '/access-control/roles');
|
||||||
|
assert.dom('[data-test-role-row="reader"]').doesNotExist();
|
||||||
|
});
|
||||||
|
test('New Role', async function (assert) {
|
||||||
|
await click('[data-test-create-role]');
|
||||||
|
assert.equal(currentURL(), '/access-control/roles/new');
|
||||||
|
await fillIn('[data-test-role-name-input]', 'test-role');
|
||||||
|
await click('button[data-test-save-role]');
|
||||||
|
assert
|
||||||
|
.dom('.flash-message.alert-critical')
|
||||||
|
.exists('Cannnot save with no policies selected');
|
||||||
|
|
||||||
|
// Select a policy
|
||||||
|
await click('[data-test-role-policies] tbody tr input');
|
||||||
|
await click('button[data-test-save-role]');
|
||||||
|
assert.dom('.flash-message.alert-success').exists();
|
||||||
|
assert.equal(currentURL(), '/access-control/roles/1'); // default id created via mirage
|
||||||
|
await AccessControl.visitRoles();
|
||||||
|
assert.dom('[data-test-role-row="test-role"]').exists();
|
||||||
|
|
||||||
|
// Now, try deleting all policies then doing this again. There'll be a warning on the roles/new page.
|
||||||
|
await AccessControl.visitPolicies();
|
||||||
|
const policyRows = findAll('[data-test-policy-row]');
|
||||||
|
for (const row of policyRows) {
|
||||||
|
const deleteButton = row.querySelector('[data-test-delete-policy]');
|
||||||
|
await click(deleteButton);
|
||||||
|
}
|
||||||
|
assert.dom('[data-test-empty-policies-list-headline]').exists();
|
||||||
|
await AccessControl.visitRoles();
|
||||||
|
await click('[data-test-create-role]');
|
||||||
|
assert.dom('.empty-message').exists();
|
||||||
|
assert
|
||||||
|
.dom('.empty-message-body')
|
||||||
|
.containsText('At least one Policy is required to create a Role');
|
||||||
|
});
|
||||||
|
});
|
|
@ -4,7 +4,14 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-disable qunit/require-expect */
|
/* eslint-disable qunit/require-expect */
|
||||||
import { currentURL, find, findAll, visit, click } from '@ember/test-helpers';
|
import {
|
||||||
|
currentURL,
|
||||||
|
find,
|
||||||
|
findAll,
|
||||||
|
visit,
|
||||||
|
click,
|
||||||
|
fillIn,
|
||||||
|
} from '@ember/test-helpers';
|
||||||
import { module, skip, test } from 'qunit';
|
import { module, skip, test } from 'qunit';
|
||||||
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';
|
||||||
|
@ -14,6 +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 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';
|
||||||
|
@ -28,6 +36,7 @@ let job;
|
||||||
let node;
|
let node;
|
||||||
let managementToken;
|
let managementToken;
|
||||||
let clientToken;
|
let clientToken;
|
||||||
|
|
||||||
module('Acceptance | tokens', function (hooks) {
|
module('Acceptance | tokens', function (hooks) {
|
||||||
setupApplicationTest(hooks);
|
setupApplicationTest(hooks);
|
||||||
setupMirage(hooks);
|
setupMirage(hooks);
|
||||||
|
@ -61,7 +70,7 @@ module('Acceptance | tokens', function (hooks) {
|
||||||
null,
|
null,
|
||||||
'No token secret set'
|
'No token secret set'
|
||||||
);
|
);
|
||||||
assert.ok(document.title.includes('Authorization'));
|
assert.ok(document.title.includes('Sign In'));
|
||||||
|
|
||||||
await Tokens.secret(secretId).submit();
|
await Tokens.secret(secretId).submit();
|
||||||
assert.equal(
|
assert.equal(
|
||||||
|
@ -567,7 +576,6 @@ module('Acceptance | tokens', function (hooks) {
|
||||||
assert.dom('.dropdown-options').exists('Dropdown options are shown');
|
assert.dom('.dropdown-options').exists('Dropdown options are shown');
|
||||||
|
|
||||||
await selectChoose('[data-test-select-jwt]', 'JWT-Regional');
|
await selectChoose('[data-test-select-jwt]', 'JWT-Regional');
|
||||||
console.log(currentURL());
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
currentURL(),
|
currentURL(),
|
||||||
'/settings/tokens?jwtAuthMethod=JWT-Regional',
|
'/settings/tokens?jwtAuthMethod=JWT-Regional',
|
||||||
|
@ -586,21 +594,24 @@ module('Acceptance | tokens', function (hooks) {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Tokens are shown on the policies index page', async function (assert) {
|
test('Tokens are shown on the Access Control Policies index page', async function (assert) {
|
||||||
allScenarios.policiesTestCluster(server);
|
allScenarios.policiesTestCluster(server);
|
||||||
|
let firstPolicy = server.db.policies.sort((a, b) => {
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
})[0];
|
||||||
// Create an expired token
|
// Create an expired token
|
||||||
server.create('token', {
|
server.create('token', {
|
||||||
name: 'Expired Token',
|
name: 'Expired Token',
|
||||||
id: 'just-expired',
|
id: 'just-expired',
|
||||||
policyIds: [server.db.policies[0].name],
|
policyIds: [firstPolicy.name],
|
||||||
expirationTime: new Date(new Date().getTime() - 10 * 60 * 1000), // 10 minutes ago
|
expirationTime: new Date(new Date().getTime() - 10 * 60 * 1000), // 10 minutes ago
|
||||||
});
|
});
|
||||||
|
|
||||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
||||||
await visit('/policies');
|
await visit('/access-control/policies');
|
||||||
assert.dom('[data-test-policy-token-count]').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(server.db.policies[0].name);
|
return token.policyIds.includes(firstPolicy.name);
|
||||||
});
|
});
|
||||||
assert
|
assert
|
||||||
.dom('[data-test-policy-total-tokens]')
|
.dom('[data-test-policy-total-tokens]')
|
||||||
|
@ -611,22 +622,25 @@ module('Acceptance | tokens', function (hooks) {
|
||||||
|
|
||||||
test('Tokens are shown on a policy page', async function (assert) {
|
test('Tokens are shown on a policy page', async function (assert) {
|
||||||
allScenarios.policiesTestCluster(server);
|
allScenarios.policiesTestCluster(server);
|
||||||
|
let firstPolicy = server.db.policies.sort((a, b) => {
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
})[0];
|
||||||
|
|
||||||
// Create an expired token
|
// Create an expired token
|
||||||
server.create('token', {
|
server.create('token', {
|
||||||
name: 'Expired Token',
|
name: 'Expired Token',
|
||||||
id: 'just-expired',
|
id: 'just-expired',
|
||||||
policyIds: [server.db.policies[0].name],
|
policyIds: [firstPolicy.name],
|
||||||
expirationTime: new Date(new Date().getTime() - 10 * 60 * 1000), // 10 minutes ago
|
expirationTime: new Date(new Date().getTime() - 10 * 60 * 1000), // 10 minutes ago
|
||||||
});
|
});
|
||||||
|
|
||||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
||||||
await visit('/policies');
|
await visit('/access-control/policies');
|
||||||
|
await click('[data-test-policy-name]');
|
||||||
await click('[data-test-policy-row]:first-child');
|
assert.equal(currentURL(), `/access-control/policies/${firstPolicy.name}`);
|
||||||
assert.equal(currentURL(), `/policies/${server.db.policies[0].name}`);
|
|
||||||
|
|
||||||
const expectedFirstPolicyTokens = server.db.tokens.filter((token) => {
|
const expectedFirstPolicyTokens = server.db.tokens.filter((token) => {
|
||||||
return token.policyIds.includes(server.db.policies[0].name);
|
return token.policyIds.includes(firstPolicy.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
assert
|
assert
|
||||||
|
@ -635,14 +649,25 @@ module('Acceptance | tokens', function (hooks) {
|
||||||
{ count: expectedFirstPolicyTokens.length },
|
{ count: expectedFirstPolicyTokens.length },
|
||||||
'Expected number of tokens are shown'
|
'Expected number of tokens are shown'
|
||||||
);
|
);
|
||||||
assert.dom('[data-test-token-expiration-time]').hasText('10 minutes ago');
|
|
||||||
|
const expiredTokenRow = [...findAll('[data-test-policy-token-row]')].find(
|
||||||
|
(a) => a.textContent.includes('Expired Token')
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.dom(expiredTokenRow).exists();
|
||||||
|
assert
|
||||||
|
.dom(expiredTokenRow.querySelector('[data-test-token-expiration-time]'))
|
||||||
|
.hasText('10 minutes ago');
|
||||||
|
|
||||||
window.localStorage.nomadTokenSecret = null;
|
window.localStorage.nomadTokenSecret = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Tokens Deletion', async function (assert) {
|
test('Tokens Deletion from Policy page', async function (assert) {
|
||||||
allScenarios.policiesTestCluster(server);
|
allScenarios.policiesTestCluster(server);
|
||||||
const testPolicy = server.db.policies[0];
|
let testPolicy = server.db.policies.sort((a, b) => {
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
})[0];
|
||||||
|
|
||||||
const existingTokens = server.db.tokens.filter((t) =>
|
const existingTokens = server.db.tokens.filter((t) =>
|
||||||
t.policyIds.includes(testPolicy.name)
|
t.policyIds.includes(testPolicy.name)
|
||||||
);
|
);
|
||||||
|
@ -654,28 +679,22 @@ module('Acceptance | tokens', function (hooks) {
|
||||||
});
|
});
|
||||||
|
|
||||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
||||||
await visit('/policies');
|
await visit('/access-control/policies');
|
||||||
|
|
||||||
await click('[data-test-policy-row]:first-child');
|
await click('[data-test-policy-name]:first-child');
|
||||||
assert.equal(currentURL(), `/policies/${testPolicy.name}`);
|
assert.equal(currentURL(), `/access-control/policies/${testPolicy.name}`);
|
||||||
assert
|
assert
|
||||||
.dom('[data-test-policy-token-row]')
|
.dom('[data-test-policy-token-row]')
|
||||||
.exists(
|
.exists(
|
||||||
{ count: existingTokens.length + 1 },
|
{ count: existingTokens.length + 1 },
|
||||||
'Expected number of tokens are shown'
|
'Expected number of tokens are shown'
|
||||||
);
|
);
|
||||||
|
|
||||||
const doomedTokenRow = [...findAll('[data-test-policy-token-row]')].find(
|
const doomedTokenRow = [...findAll('[data-test-policy-token-row]')].find(
|
||||||
(a) => a.textContent.includes('Doomed Token')
|
(a) => a.textContent.includes('Doomed Token')
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.dom(doomedTokenRow).exists();
|
assert.dom(doomedTokenRow).exists();
|
||||||
|
|
||||||
await click(doomedTokenRow.querySelector('button'));
|
await click(doomedTokenRow.querySelector('button'));
|
||||||
assert
|
|
||||||
.dom(doomedTokenRow.querySelector('[data-test-confirm-button]'))
|
|
||||||
.exists();
|
|
||||||
await click(doomedTokenRow.querySelector('[data-test-confirm-button]'));
|
|
||||||
assert.dom('.flash-message.alert-success').exists();
|
assert.dom('.flash-message.alert-success').exists();
|
||||||
assert
|
assert
|
||||||
.dom('[data-test-policy-token-row]')
|
.dom('[data-test-policy-token-row]')
|
||||||
|
@ -687,18 +706,21 @@ module('Acceptance | tokens', function (hooks) {
|
||||||
window.localStorage.nomadTokenSecret = null;
|
window.localStorage.nomadTokenSecret = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Test Token Creation', async function (assert) {
|
test('Test Token Creation from Policy Page', async function (assert) {
|
||||||
allScenarios.policiesTestCluster(server);
|
allScenarios.policiesTestCluster(server);
|
||||||
const testPolicy = server.db.policies[0];
|
let testPolicy = server.db.policies.sort((a, b) => {
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
})[0];
|
||||||
|
|
||||||
const existingTokens = server.db.tokens.filter((t) =>
|
const existingTokens = server.db.tokens.filter((t) =>
|
||||||
t.policyIds.includes(testPolicy.name)
|
t.policyIds.includes(testPolicy.name)
|
||||||
);
|
);
|
||||||
|
|
||||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
||||||
await visit('/policies');
|
await visit('/access-control/policies');
|
||||||
|
|
||||||
await click('[data-test-policy-row]:first-child');
|
await click('[data-test-policy-name]');
|
||||||
assert.equal(currentURL(), `/policies/${testPolicy.name}`);
|
assert.equal(currentURL(), `/access-control/policies/${testPolicy.name}`);
|
||||||
|
|
||||||
assert
|
assert
|
||||||
.dom('[data-test-policy-token-row]')
|
.dom('[data-test-policy-token-row]')
|
||||||
|
@ -730,4 +752,534 @@ module('Acceptance | tokens', function (hooks) {
|
||||||
requestHeaders[name.toUpperCase()]
|
requestHeaders[name.toUpperCase()]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module('Roles', function (hooks) {
|
||||||
|
// Set up a token with a role
|
||||||
|
hooks.beforeEach(function () {
|
||||||
|
window.localStorage.clear();
|
||||||
|
window.sessionStorage.clear();
|
||||||
|
faker.seed(1);
|
||||||
|
allScenarios.rolesTestCluster(server);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Policies are derived from role', async function (assert) {
|
||||||
|
assert.expect(19);
|
||||||
|
|
||||||
|
await Tokens.visit();
|
||||||
|
|
||||||
|
let token;
|
||||||
|
|
||||||
|
// User with 1 role, containing 1 policy, and no direct policies
|
||||||
|
token = server.db.tokens.findBy(
|
||||||
|
(t) => t.name === 'High Level Role Token'
|
||||||
|
);
|
||||||
|
await Tokens.secret(token.secretId).submit();
|
||||||
|
|
||||||
|
assert.dom('[data-test-token-role]').exists({ count: 1 });
|
||||||
|
assert.dom('[data-test-role-name]').hasText('high-level');
|
||||||
|
assert.dom('[data-test-role-policies] li').exists({ count: 1 });
|
||||||
|
assert.dom('[data-test-role-policies] li').hasText('job-writer');
|
||||||
|
|
||||||
|
assert.dom('[data-test-token-policy]').exists({ count: 1 });
|
||||||
|
assert.dom('[data-test-policy-name]').hasText('job-writer');
|
||||||
|
|
||||||
|
await Tokens.clear();
|
||||||
|
|
||||||
|
// User with 1 role, containing 2 policies, and a direct policy
|
||||||
|
token = server.db.tokens.findBy(
|
||||||
|
(t) => t.name === 'Policy And Role Token'
|
||||||
|
);
|
||||||
|
await Tokens.secret(token.secretId).submit();
|
||||||
|
|
||||||
|
assert.dom('[data-test-token-role]').exists({ count: 1 });
|
||||||
|
assert.dom('[data-test-role-name]').hasText('reader');
|
||||||
|
assert.dom('[data-test-role-policies] li').exists({ count: 2 });
|
||||||
|
let policyLinks = findAll('[data-test-role-policies] li');
|
||||||
|
assert.dom(policyLinks[0]).hasText('client-reader');
|
||||||
|
assert.dom(policyLinks[1]).hasText('job-reader');
|
||||||
|
|
||||||
|
assert.dom('[data-test-token-policy]').exists({ count: 3 });
|
||||||
|
let policyBlocks = findAll('[data-test-policy-name]');
|
||||||
|
assert.dom(policyBlocks[0]).hasText('operator');
|
||||||
|
assert.dom(policyBlocks[1]).hasText('client-reader');
|
||||||
|
assert.dom(policyBlocks[2]).hasText('job-reader');
|
||||||
|
|
||||||
|
await percySnapshot(assert);
|
||||||
|
|
||||||
|
await Tokens.clear();
|
||||||
|
|
||||||
|
// User with 2 roles, each containing 1 policy, and one of the policies is also directly on their token
|
||||||
|
token = server.db.tokens.findBy(
|
||||||
|
(t) => t.name === 'Multi Role And Policy Token'
|
||||||
|
);
|
||||||
|
await Tokens.secret(token.secretId).submit();
|
||||||
|
|
||||||
|
assert.equal(token.roleIds.length, 2);
|
||||||
|
assert.equal(token.policyIds.length, 1);
|
||||||
|
|
||||||
|
assert.dom('[data-test-token-role]').exists({ count: 2 });
|
||||||
|
assert.dom('[data-test-token-policy]').exists({ count: 2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Token priveleges are derived from role', async function (assert) {
|
||||||
|
// First, check that a node reader can read nodes if the policy to do so only exists at their role level
|
||||||
|
await visit('/clients');
|
||||||
|
// Expect to see some nodes
|
||||||
|
let nodes = server.db.nodes;
|
||||||
|
assert.dom('[data-test-client-node-row]').exists({ count: nodes.length });
|
||||||
|
|
||||||
|
// Head back and sign in as Clientless Role Token
|
||||||
|
await Tokens.visit();
|
||||||
|
let token = server.db.tokens.findBy(
|
||||||
|
(t) => t.name === 'Clientless Role Token'
|
||||||
|
);
|
||||||
|
await Tokens.secret(token.secretId).submit();
|
||||||
|
|
||||||
|
await visit('/clients');
|
||||||
|
// Expect no rows, and a denied message
|
||||||
|
assert.dom('[data-test-client-node-row]').doesNotExist();
|
||||||
|
assert.dom('[data-test-error]').exists();
|
||||||
|
|
||||||
|
// Pop over to the jobs page and make sure the Run button is disabled
|
||||||
|
await visit('/jobs');
|
||||||
|
assert.dom('[data-test-run-job]').hasTagName('button');
|
||||||
|
assert.dom('[data-test-run-job]').isDisabled();
|
||||||
|
|
||||||
|
// Sign out, and sign back in as a high-level role token
|
||||||
|
await Tokens.visit();
|
||||||
|
await Tokens.clear();
|
||||||
|
token = server.db.tokens.findBy(
|
||||||
|
(t) => t.name === 'High Level Role Token'
|
||||||
|
);
|
||||||
|
await Tokens.secret(token.secretId).submit();
|
||||||
|
|
||||||
|
await visit('/jobs');
|
||||||
|
// Expect the Run button/link to work now
|
||||||
|
assert.dom('[data-test-run-job]').hasTagName('a');
|
||||||
|
assert.dom('[data-test-run-job]').hasAttribute('href', '/ui/jobs/run');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module('Access Control Tokens section', function (hooks) {
|
||||||
|
hooks.beforeEach(async function () {
|
||||||
|
window.localStorage.clear();
|
||||||
|
window.sessionStorage.clear();
|
||||||
|
faker.seed(1);
|
||||||
|
allScenarios.rolesTestCluster(server);
|
||||||
|
await Tokens.visit();
|
||||||
|
const managementToken = server.db.tokens.findBy(
|
||||||
|
(t) => t.type === 'management'
|
||||||
|
);
|
||||||
|
const { secretId } = managementToken;
|
||||||
|
await Tokens.secret(secretId).submit();
|
||||||
|
await AccessControl.visitTokens();
|
||||||
|
});
|
||||||
|
|
||||||
|
hooks.afterEach(async function () {
|
||||||
|
await Tokens.visit();
|
||||||
|
await Tokens.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Tokens index, general', async function (assert) {
|
||||||
|
assert.equal(currentURL(), '/access-control/tokens');
|
||||||
|
// Number of token rows equivalent to number in db
|
||||||
|
assert
|
||||||
|
.dom('[data-test-token-row]')
|
||||||
|
.exists({ count: server.db.tokens.length });
|
||||||
|
|
||||||
|
await percySnapshot(assert);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Tokens index, management token handling', async function (assert) {
|
||||||
|
// two management tokens, one of which is yours; yours cannot be deleted or clicked into.
|
||||||
|
assert.dom('[data-test-token-type="management"]').exists({ count: 2 });
|
||||||
|
const managementToken = server.db.tokens.findBy(
|
||||||
|
(t) => t.type === 'management'
|
||||||
|
);
|
||||||
|
const managementTokenRow = [...findAll('[data-test-token-row]')].find(
|
||||||
|
(row) => row.textContent.includes(managementToken.name)
|
||||||
|
);
|
||||||
|
const otherManagerRow = [...findAll('[data-test-token-row]')].find(
|
||||||
|
(row) =>
|
||||||
|
row.textContent.includes('management') &&
|
||||||
|
!row.textContent.includes(managementToken.name)
|
||||||
|
);
|
||||||
|
assert
|
||||||
|
.dom(managementTokenRow.querySelector('[data-test-token-name] a'))
|
||||||
|
.doesNotExist('Cannot click into and edit your own token');
|
||||||
|
assert
|
||||||
|
.dom(otherManagerRow.querySelector('[data-test-token-name] a'))
|
||||||
|
.exists('Can click into and edit another manager token');
|
||||||
|
assert
|
||||||
|
.dom(
|
||||||
|
managementTokenRow.querySelector('[data-test-delete-token] button')
|
||||||
|
)
|
||||||
|
.isDisabled('Cannot delete your own token');
|
||||||
|
assert
|
||||||
|
.dom(otherManagerRow.querySelector('[data-test-delete-token] button'))
|
||||||
|
.isNotDisabled('Can delete another manager token');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Tokens index, table sorting', async function (assert) {
|
||||||
|
const nameCells = findAll('[data-test-token-name]');
|
||||||
|
const nameCellText = nameCells.map((cell) => cell.textContent.trim());
|
||||||
|
const sortedNameCellText = nameCellText.slice().sort();
|
||||||
|
assert.deepEqual(
|
||||||
|
nameCellText,
|
||||||
|
sortedNameCellText,
|
||||||
|
'Names are sorted alphabetically'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Click on the first thead tr th to reverse
|
||||||
|
assert
|
||||||
|
.dom('table.acl-table thead tr th')
|
||||||
|
.hasAttribute('aria-sort', 'ascending');
|
||||||
|
await click('table.acl-table thead tr th button');
|
||||||
|
assert
|
||||||
|
.dom('table.acl-table thead tr th')
|
||||||
|
.hasAttribute('aria-sort', 'descending');
|
||||||
|
|
||||||
|
const reversedNameCells = findAll('[data-test-token-name]');
|
||||||
|
const reversedNameCellText = reversedNameCells.map((cell) =>
|
||||||
|
cell.textContent.trim()
|
||||||
|
);
|
||||||
|
const reversedSortedNameCellText = nameCellText.slice().sort().reverse();
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
reversedNameCellText,
|
||||||
|
reversedSortedNameCellText,
|
||||||
|
'Names are reversed alphabetically'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Tokens index, deletion', async function (assert) {
|
||||||
|
const numberOfTokens = server.db.tokens.length;
|
||||||
|
assert
|
||||||
|
.dom('[data-test-token-row]')
|
||||||
|
.exists(
|
||||||
|
{ count: numberOfTokens },
|
||||||
|
'Number of tokens matches number in db'
|
||||||
|
);
|
||||||
|
const tokenToDelete = server.db.tokens.findBy((t) => t.type === 'client');
|
||||||
|
const tokenRowToDelete = [...findAll('[data-test-token-row]')].find(
|
||||||
|
(row) => row.textContent.includes(tokenToDelete.name)
|
||||||
|
);
|
||||||
|
await click(
|
||||||
|
tokenRowToDelete.querySelector('[data-test-delete-token] button')
|
||||||
|
);
|
||||||
|
assert.dom('.flash-message.alert-success').exists();
|
||||||
|
assert
|
||||||
|
.dom('[data-test-token-row]')
|
||||||
|
.exists(
|
||||||
|
{ count: numberOfTokens - 1 },
|
||||||
|
'Number of token rows decreased after deletion'
|
||||||
|
);
|
||||||
|
|
||||||
|
const nameCells = findAll('[data-test-token-name]');
|
||||||
|
const nameCellText = nameCells.map((cell) => cell.textContent.trim());
|
||||||
|
assert.notOk(
|
||||||
|
nameCellText.includes(tokenToDelete.name),
|
||||||
|
'Deleted token name not found among name cells'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Tokens index, clicking into a token page', async function (assert) {
|
||||||
|
const tokenToClick = server.db.tokens.findBy((t) => t.type === 'client');
|
||||||
|
const tokenRowToClick = [...findAll('[data-test-token-row]')].find(
|
||||||
|
(row) => row.textContent.includes(tokenToClick.name)
|
||||||
|
);
|
||||||
|
await click(tokenRowToClick.querySelector('[data-test-token-name] a'));
|
||||||
|
assert.equal(currentURL(), `/access-control/tokens/${tokenToClick.id}`);
|
||||||
|
assert.dom('[data-test-token-name-input]').hasValue(tokenToClick.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Tokens index, roles and policies attached to a token show up as links', async function (assert) {
|
||||||
|
// Staying on the index page, Rows should have a Roles column with either "No Roles" or a bunch of links to roles. Ditto policies.
|
||||||
|
const tokenWithRolesAndPolicies = server.db.tokens.findBy(
|
||||||
|
(t) => t.name === 'Multi Role And Policy Token'
|
||||||
|
);
|
||||||
|
const tokenRowWithRolesAndPolicies = [
|
||||||
|
...findAll('[data-test-token-row]'),
|
||||||
|
].find((row) => row.textContent.includes(tokenWithRolesAndPolicies.name));
|
||||||
|
const rolesCell = tokenRowWithRolesAndPolicies.querySelector(
|
||||||
|
'[data-test-token-roles]'
|
||||||
|
);
|
||||||
|
const policiesCell = tokenRowWithRolesAndPolicies.querySelector(
|
||||||
|
'[data-test-token-policies]'
|
||||||
|
);
|
||||||
|
assert.dom(rolesCell).exists();
|
||||||
|
assert.dom(policiesCell).exists();
|
||||||
|
|
||||||
|
const rolesCellTags = rolesCell
|
||||||
|
.querySelector('.tag-group')
|
||||||
|
.querySelectorAll('span');
|
||||||
|
const policiesCellTags = policiesCell
|
||||||
|
.querySelector('.tag-group')
|
||||||
|
.querySelectorAll('span');
|
||||||
|
assert.equal(rolesCellTags.length, 2);
|
||||||
|
assert.equal(policiesCellTags.length, 1);
|
||||||
|
|
||||||
|
const policyLessToken = server.db.tokens.findBy(
|
||||||
|
(t) => t.name === 'High Level Role Token'
|
||||||
|
);
|
||||||
|
const policyLessTokenRow = [...findAll('[data-test-token-row]')].find(
|
||||||
|
(row) => row.textContent.includes(policyLessToken.name)
|
||||||
|
);
|
||||||
|
const rolesCell2 = policyLessTokenRow.querySelector(
|
||||||
|
'[data-test-token-roles]'
|
||||||
|
);
|
||||||
|
const policiesCell2 = policyLessTokenRow.querySelector(
|
||||||
|
'[data-test-token-policies]'
|
||||||
|
);
|
||||||
|
assert.dom(rolesCell2).exists();
|
||||||
|
assert.dom(policiesCell2).exists();
|
||||||
|
|
||||||
|
const rolesCellTags2 = rolesCell2
|
||||||
|
.querySelector('.tag-group')
|
||||||
|
.querySelectorAll('span');
|
||||||
|
const policiesCellTags2 = policiesCell2
|
||||||
|
.querySelector('.tag-group')
|
||||||
|
.querySelectorAll('span');
|
||||||
|
assert.equal(rolesCellTags2.length, 1);
|
||||||
|
assert.equal(policiesCellTags2.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Token page, general', async function (assert) {
|
||||||
|
const token = server.db.tokens.findBy((t) => t.id === 'cl4y-t0k3n');
|
||||||
|
await visit(`/access-control/tokens/${token.id}`);
|
||||||
|
assert.dom('[data-test-token-name-input]').hasValue(token.name);
|
||||||
|
assert.dom('[data-test-token-accessor]').hasValue(token.accessorId);
|
||||||
|
assert.dom('[data-test-token-secret]').hasValue(token.secretId);
|
||||||
|
assert.dom('[data-test-token-type="client"]').isChecked();
|
||||||
|
assert.dom('[data-test-token-type="management"]').isNotChecked();
|
||||||
|
|
||||||
|
assert.dom('.expiration-time').hasText('Token expires in an hour');
|
||||||
|
|
||||||
|
assert.dom('[data-test-token-roles]').exists();
|
||||||
|
assert.dom('[data-test-token-policies]').exists();
|
||||||
|
|
||||||
|
// All possible policies are shown
|
||||||
|
const allPolicies = server.db.policies;
|
||||||
|
const allPolicyRows = findAll('[data-test-token-policies] tbody tr');
|
||||||
|
assert.equal(
|
||||||
|
allPolicyRows.length,
|
||||||
|
allPolicies.length,
|
||||||
|
'All policies are shown'
|
||||||
|
);
|
||||||
|
|
||||||
|
// The policies/roles belonging to this token are checked
|
||||||
|
const tokenPolicies = token.policyIds;
|
||||||
|
|
||||||
|
const checkedPolicyRows = findAll(
|
||||||
|
'[data-test-token-policies] tbody tr input:checked'
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
checkedPolicyRows.length,
|
||||||
|
tokenPolicies.length,
|
||||||
|
'All policies belonging to this token are checked'
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkedPolicyNames = checkedPolicyRows.map((row) =>
|
||||||
|
row
|
||||||
|
.closest('tr')
|
||||||
|
.querySelector('[data-test-policy-name]')
|
||||||
|
.textContent.trim()
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
checkedPolicyNames.sort(),
|
||||||
|
tokenPolicies.sort(),
|
||||||
|
'All policies belonging to this token are checked'
|
||||||
|
);
|
||||||
|
|
||||||
|
const allRoles = server.db.roles;
|
||||||
|
const allRoleRows = findAll('[data-test-token-roles] tbody tr');
|
||||||
|
assert.equal(allRoleRows.length, allRoles.length, 'All roles are shown');
|
||||||
|
|
||||||
|
const tokenRoles = token.roleIds;
|
||||||
|
|
||||||
|
const checkedRoleRows = findAll(
|
||||||
|
'[data-test-token-roles] tbody tr input:checked'
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
checkedRoleRows.length,
|
||||||
|
tokenRoles.length,
|
||||||
|
'All roles belonging to this token are checked'
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkedRoleNames = checkedRoleRows.map((row) =>
|
||||||
|
row
|
||||||
|
.closest('tr')
|
||||||
|
.querySelector('[data-test-role-name]')
|
||||||
|
.textContent.trim()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
checkedRoleNames.sort(),
|
||||||
|
tokenRoles.sort(),
|
||||||
|
'All roles belonging to this token are checked'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
test('Token name can be edited', async function (assert) {
|
||||||
|
const token = server.db.tokens.findBy((t) => t.id === 'cl4y-t0k3n');
|
||||||
|
await visit(`/access-control/tokens/${token.id}`);
|
||||||
|
assert.dom('[data-test-token-name-input]').hasValue(token.name);
|
||||||
|
await fillIn('[data-test-token-name-input]', 'Mud-Token');
|
||||||
|
await click('[data-test-token-save]');
|
||||||
|
assert.dom('.flash-message.alert-success').exists();
|
||||||
|
await AccessControl.visitTokens();
|
||||||
|
assert.dom('[data-test-token-name="Mud-Token"]').exists({ count: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Token policies and roles can be edited', async function (assert) {
|
||||||
|
const token = server.db.tokens.findBy((t) => t.id === 'cl4y-t0k3n');
|
||||||
|
await visit(`/access-control/tokens/${token.id}`);
|
||||||
|
|
||||||
|
// The policies/roles belonging to this token are checked
|
||||||
|
const tokenPolicies = token.policyIds;
|
||||||
|
|
||||||
|
const checkedPolicyRows = findAll(
|
||||||
|
'[data-test-token-policies] tbody tr input:checked'
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
checkedPolicyRows.length,
|
||||||
|
tokenPolicies.length,
|
||||||
|
'All policies belonging to this token are checked'
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkedPolicyNames = checkedPolicyRows.map((row) =>
|
||||||
|
row
|
||||||
|
.closest('tr')
|
||||||
|
.querySelector('[data-test-policy-name]')
|
||||||
|
.textContent.trim()
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
checkedPolicyNames.sort(),
|
||||||
|
tokenPolicies.sort(),
|
||||||
|
'All policies belonging to this token are checked'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Try unchecking ALL checked roles and policies and saving
|
||||||
|
// First, find all checked ones
|
||||||
|
const checkedPolicies = findAll(
|
||||||
|
'[data-test-token-policies] tbody tr input:checked'
|
||||||
|
);
|
||||||
|
const checkedRoles = findAll(
|
||||||
|
'[data-test-token-roles] tbody tr input:checked'
|
||||||
|
);
|
||||||
|
// Then uncheck them
|
||||||
|
checkedPolicies.forEach((policy) => {
|
||||||
|
policy.click();
|
||||||
|
});
|
||||||
|
checkedRoles.forEach((role) => {
|
||||||
|
role.click();
|
||||||
|
});
|
||||||
|
await click('[data-test-token-save]');
|
||||||
|
assert.dom('.flash-message.alert-critical').exists();
|
||||||
|
|
||||||
|
// Try selecting a single role
|
||||||
|
await click('[data-test-token-roles] tbody tr input');
|
||||||
|
await click('[data-test-token-save]');
|
||||||
|
assert.dom('.flash-message.alert-success').exists();
|
||||||
|
|
||||||
|
await percySnapshot(assert);
|
||||||
|
|
||||||
|
await AccessControl.visitTokens();
|
||||||
|
// Policies cell for our clay token should read "No Policies"
|
||||||
|
const clayToken = server.db.tokens.findBy((t) => t.id === 'cl4y-t0k3n');
|
||||||
|
const clayTokenRow = [...findAll('[data-test-token-row]')].find((row) =>
|
||||||
|
row.textContent.includes(clayToken.name)
|
||||||
|
);
|
||||||
|
const policiesCell = clayTokenRow.querySelector(
|
||||||
|
'[data-test-token-policies]'
|
||||||
|
);
|
||||||
|
assert.dom(policiesCell).exists();
|
||||||
|
assert.dom(policiesCell).hasText('No Policies');
|
||||||
|
|
||||||
|
// Roles cell should have 1 tag
|
||||||
|
const rolesCell = clayTokenRow.querySelector('[data-test-token-roles]');
|
||||||
|
const rolesCellTags = rolesCell
|
||||||
|
.querySelector('.tag-group')
|
||||||
|
.querySelectorAll('span');
|
||||||
|
assert.equal(rolesCellTags.length, 1);
|
||||||
|
});
|
||||||
|
test('Token can be deleted', async function (assert) {
|
||||||
|
const token = server.db.tokens.findBy((t) => t.id === 'cl4y-t0k3n');
|
||||||
|
await visit(`/access-control/tokens/${token.id}`);
|
||||||
|
await click('[data-test-delete-token]');
|
||||||
|
assert.dom('.flash-message.alert-success').exists();
|
||||||
|
await AccessControl.visitTokens();
|
||||||
|
assert.dom('[data-test-token-name="cl4y-t0k3n"]').doesNotExist();
|
||||||
|
});
|
||||||
|
test('New Token creation', async function (assert) {
|
||||||
|
await click('[data-test-create-token]');
|
||||||
|
assert.equal(currentURL(), '/access-control/tokens/new');
|
||||||
|
await fillIn('[data-test-token-name-input]', 'Timeless Token');
|
||||||
|
await click('[data-test-token-save]');
|
||||||
|
assert.dom('.flash-message.alert-success').exists();
|
||||||
|
await AccessControl.visitTokens();
|
||||||
|
assert
|
||||||
|
.dom('[data-test-token-name="Timeless Token"]')
|
||||||
|
.exists({ count: 1 });
|
||||||
|
const newTokenRow = [...findAll('[data-test-token-row]')].find((row) =>
|
||||||
|
row.textContent.includes('Timeless Token')
|
||||||
|
);
|
||||||
|
const newTokenExpirationCell = newTokenRow.querySelector(
|
||||||
|
'[data-test-token-expiration-time]'
|
||||||
|
);
|
||||||
|
assert.dom(newTokenExpirationCell).hasText('Never');
|
||||||
|
|
||||||
|
// Now create one with a TTL
|
||||||
|
await click('[data-test-create-token]');
|
||||||
|
assert.equal(currentURL(), '/access-control/tokens/new');
|
||||||
|
await fillIn('[data-test-token-name-input]', 'TTL Token');
|
||||||
|
// Select the "8 hours" radio within the .expiration-time div
|
||||||
|
await click('.expiration-time input[value="8h"]');
|
||||||
|
await click('[data-test-token-save]');
|
||||||
|
assert.dom('.flash-message.alert-success').exists();
|
||||||
|
await AccessControl.visitTokens();
|
||||||
|
assert.dom('[data-test-token-name="TTL Token"]').exists({ count: 1 });
|
||||||
|
const ttlTokenRow = [...findAll('[data-test-token-row]')].find((row) =>
|
||||||
|
row.textContent.includes('TTL Token')
|
||||||
|
);
|
||||||
|
const ttlTokenExpirationCell = ttlTokenRow.querySelector(
|
||||||
|
'[data-test-token-expiration-time]'
|
||||||
|
);
|
||||||
|
assert.dom(ttlTokenExpirationCell).hasText('in 8 hours');
|
||||||
|
|
||||||
|
// Now create one with an expiration time
|
||||||
|
await click('[data-test-create-token]');
|
||||||
|
assert.equal(currentURL(), '/access-control/tokens/new');
|
||||||
|
await fillIn('[data-test-token-name-input]', 'Expiring Token');
|
||||||
|
// select the Custom radio button
|
||||||
|
await click('.expiration-time input[value="custom"]');
|
||||||
|
assert
|
||||||
|
.dom('[data-test-token-expiration-time-input]')
|
||||||
|
.exists('HTML datetime-local picker exists');
|
||||||
|
await percySnapshot(assert);
|
||||||
|
// select a date/time for 100 minutes into the future in GMT
|
||||||
|
const soon = new Date();
|
||||||
|
soon.setMinutes(soon.getMinutes() + 100);
|
||||||
|
var tzoffset = new Date().getTimezoneOffset() * 60000; //offset in milliseconds
|
||||||
|
var soonString = new Date(soon - tzoffset).toISOString().slice(0, -1);
|
||||||
|
await fillIn('[data-test-token-expiration-time-input]', soonString);
|
||||||
|
await click('[data-test-token-save]');
|
||||||
|
assert.dom('.flash-message.alert-success').exists();
|
||||||
|
await AccessControl.visitTokens();
|
||||||
|
assert
|
||||||
|
.dom('[data-test-token-name="Expiring Token"]')
|
||||||
|
.exists({ count: 1 });
|
||||||
|
const expiringTokenRow = [...findAll('[data-test-token-row]')].find(
|
||||||
|
(row) => row.textContent.includes('Expiring Token')
|
||||||
|
);
|
||||||
|
const expiringTokenExpirationCell = expiringTokenRow.querySelector(
|
||||||
|
'[data-test-token-expiration-time]'
|
||||||
|
);
|
||||||
|
assert
|
||||||
|
.dom(expiringTokenExpirationCell)
|
||||||
|
.hasText('in 2 hours', 'Expiration time is relativized and rounded');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -71,7 +71,7 @@ module('Acceptance | variables', function (hooks) {
|
||||||
const variablesToken = server.db.tokens.find(VARIABLE_TOKEN_ID);
|
const variablesToken = server.db.tokens.find(VARIABLE_TOKEN_ID);
|
||||||
window.localStorage.nomadTokenSecret = variablesToken.secretId;
|
window.localStorage.nomadTokenSecret = variablesToken.secretId;
|
||||||
server.db.variables.update({ namespace: 'default' });
|
server.db.variables.update({ namespace: 'default' });
|
||||||
const policy = server.db.policies.find('Variable Maker');
|
const policy = server.db.policies.find('Variable-Maker');
|
||||||
policy.rulesJSON.Namespaces[0].Variables.Paths.find(
|
policy.rulesJSON.Namespaces[0].Variables.Paths.find(
|
||||||
(path) => path.PathSpec === '*'
|
(path) => path.PathSpec === '*'
|
||||||
).Capabilities = ['list', 'read', 'destroy'];
|
).Capabilities = ['list', 'read', 'destroy'];
|
||||||
|
@ -452,7 +452,7 @@ module('Acceptance | variables', function (hooks) {
|
||||||
server.createList('variable', 3);
|
server.createList('variable', 3);
|
||||||
const variablesToken = server.db.tokens.find(VARIABLE_TOKEN_ID);
|
const variablesToken = server.db.tokens.find(VARIABLE_TOKEN_ID);
|
||||||
window.localStorage.nomadTokenSecret = variablesToken.secretId;
|
window.localStorage.nomadTokenSecret = variablesToken.secretId;
|
||||||
const policy = server.db.policies.find('Variable Maker');
|
const policy = server.db.policies.find('Variable-Maker');
|
||||||
policy.rulesJSON.Namespaces[0].Variables.Paths.find(
|
policy.rulesJSON.Namespaces[0].Variables.Paths.find(
|
||||||
(path) => path.PathSpec === '*'
|
(path) => path.PathSpec === '*'
|
||||||
).Capabilities = ['list'];
|
).Capabilities = ['list'];
|
||||||
|
@ -580,7 +580,7 @@ module('Acceptance | variables', function (hooks) {
|
||||||
server.createList('variable', 3);
|
server.createList('variable', 3);
|
||||||
const variablesToken = server.db.tokens.find(VARIABLE_TOKEN_ID);
|
const variablesToken = server.db.tokens.find(VARIABLE_TOKEN_ID);
|
||||||
window.localStorage.nomadTokenSecret = variablesToken.secretId;
|
window.localStorage.nomadTokenSecret = variablesToken.secretId;
|
||||||
const policy = server.db.policies.find('Variable Maker');
|
const policy = server.db.policies.find('Variable-Maker');
|
||||||
policy.rulesJSON.Namespaces[0].Variables.Paths.find(
|
policy.rulesJSON.Namespaces[0].Variables.Paths.find(
|
||||||
(path) => path.PathSpec === '*'
|
(path) => path.PathSpec === '*'
|
||||||
).Capabilities = ['list', 'read', 'write'];
|
).Capabilities = ['list', 'read', 'write'];
|
||||||
|
@ -634,7 +634,7 @@ module('Acceptance | variables', function (hooks) {
|
||||||
server.createList('variable', 3);
|
server.createList('variable', 3);
|
||||||
const variablesToken = server.db.tokens.find(VARIABLE_TOKEN_ID);
|
const variablesToken = server.db.tokens.find(VARIABLE_TOKEN_ID);
|
||||||
window.localStorage.nomadTokenSecret = variablesToken.secretId;
|
window.localStorage.nomadTokenSecret = variablesToken.secretId;
|
||||||
const policy = server.db.policies.find('Variable Maker');
|
const policy = server.db.policies.find('Variable-Maker');
|
||||||
policy.rulesJSON.Namespaces[0].Variables.Paths.find(
|
policy.rulesJSON.Namespaces[0].Variables.Paths.find(
|
||||||
(path) => path.PathSpec === '*'
|
(path) => path.PathSpec === '*'
|
||||||
).Capabilities = ['list', 'read'];
|
).Capabilities = ['list', 'read'];
|
||||||
|
@ -763,7 +763,7 @@ module('Acceptance | variables', function (hooks) {
|
||||||
server.createList('variable', 3);
|
server.createList('variable', 3);
|
||||||
const variablesToken = server.db.tokens.find(VARIABLE_TOKEN_ID);
|
const variablesToken = server.db.tokens.find(VARIABLE_TOKEN_ID);
|
||||||
window.localStorage.nomadTokenSecret = variablesToken.secretId;
|
window.localStorage.nomadTokenSecret = variablesToken.secretId;
|
||||||
const policy = server.db.policies.find('Variable Maker');
|
const policy = server.db.policies.find('Variable-Maker');
|
||||||
policy.rulesJSON.Namespaces[0].Variables.Paths.find(
|
policy.rulesJSON.Namespaces[0].Variables.Paths.find(
|
||||||
(path) => path.PathSpec === '*'
|
(path) => path.PathSpec === '*'
|
||||||
).Capabilities = ['list', 'read', 'destroy'];
|
).Capabilities = ['list', 'read', 'destroy'];
|
||||||
|
@ -799,7 +799,7 @@ module('Acceptance | variables', function (hooks) {
|
||||||
server.createList('variable', 3);
|
server.createList('variable', 3);
|
||||||
const variablesToken = server.db.tokens.find(VARIABLE_TOKEN_ID);
|
const variablesToken = server.db.tokens.find(VARIABLE_TOKEN_ID);
|
||||||
window.localStorage.nomadTokenSecret = variablesToken.secretId;
|
window.localStorage.nomadTokenSecret = variablesToken.secretId;
|
||||||
const policy = server.db.policies.find('Variable Maker');
|
const policy = server.db.policies.find('Variable-Maker');
|
||||||
policy.rulesJSON.Namespaces[0].Variables.Paths.find(
|
policy.rulesJSON.Namespaces[0].Variables.Paths.find(
|
||||||
(path) => path.PathSpec === '*'
|
(path) => path.PathSpec === '*'
|
||||||
).Capabilities = ['list', 'read'];
|
).Capabilities = ['list', 'read'];
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
/**
|
||||||
|
* 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'),
|
||||||
|
});
|
139
ui/yarn.lock
139
ui/yarn.lock
|
@ -2840,6 +2840,16 @@
|
||||||
ember-cli-version-checker "^5.1.2"
|
ember-cli-version-checker "^5.1.2"
|
||||||
semver "^7.3.5"
|
semver "^7.3.5"
|
||||||
|
|
||||||
|
"@ember/test-waiters@^3.0.2":
|
||||||
|
version "3.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@ember/test-waiters/-/test-waiters-3.0.2.tgz#5b950c580a1891ed1d4ee64f9c6bacf49a15ea6f"
|
||||||
|
integrity sha512-H8Q3Xy9rlqhDKnQpwt2pzAYDouww4TZIGSI1pZJhM7mQIGufQKuB0ijzn/yugA6Z+bNdjYp1HioP8Y4hn2zazQ==
|
||||||
|
dependencies:
|
||||||
|
calculate-cache-key-for-tree "^2.0.0"
|
||||||
|
ember-cli-babel "^7.26.6"
|
||||||
|
ember-cli-version-checker "^5.1.2"
|
||||||
|
semver "^7.3.5"
|
||||||
|
|
||||||
"@embroider/addon-shim@^1.0.0", "@embroider/addon-shim@^1.2.0":
|
"@embroider/addon-shim@^1.0.0", "@embroider/addon-shim@^1.2.0":
|
||||||
version "1.5.0"
|
version "1.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/@embroider/addon-shim/-/addon-shim-1.5.0.tgz#639b8b394336a5ae26dd3e24ffc3d34d864ac5ce"
|
resolved "https://registry.yarnpkg.com/@embroider/addon-shim/-/addon-shim-1.5.0.tgz#639b8b394336a5ae26dd3e24ffc3d34d864ac5ce"
|
||||||
|
@ -2848,7 +2858,7 @@
|
||||||
"@embroider/shared-internals" "^1.5.0"
|
"@embroider/shared-internals" "^1.5.0"
|
||||||
semver "^7.3.5"
|
semver "^7.3.5"
|
||||||
|
|
||||||
"@embroider/addon-shim@^1.5.0", "@embroider/addon-shim@^1.8.4":
|
"@embroider/addon-shim@^1.8.4":
|
||||||
version "1.8.4"
|
version "1.8.4"
|
||||||
resolved "https://registry.yarnpkg.com/@embroider/addon-shim/-/addon-shim-1.8.4.tgz#0e7f32c5506bf0f3eb0840506e31c36c7053763c"
|
resolved "https://registry.yarnpkg.com/@embroider/addon-shim/-/addon-shim-1.8.4.tgz#0e7f32c5506bf0f3eb0840506e31c36c7053763c"
|
||||||
integrity sha512-sFhfWC0vI18KxVenmswQ/ShIvBg4juL8ubI+Q3NTSdkCTeaPQ/DIOUF6oR5DCQ8eO/TkIaw+kdG3FkTY6yNJqA==
|
integrity sha512-sFhfWC0vI18KxVenmswQ/ShIvBg4juL8ubI+Q3NTSdkCTeaPQ/DIOUF6oR5DCQ8eO/TkIaw+kdG3FkTY6yNJqA==
|
||||||
|
@ -3432,35 +3442,36 @@
|
||||||
resolved "https://registry.yarnpkg.com/@handlebars/parser/-/parser-1.1.0.tgz#d6dbc7574774b238114582410e8fee0dc3532bdf"
|
resolved "https://registry.yarnpkg.com/@handlebars/parser/-/parser-1.1.0.tgz#d6dbc7574774b238114582410e8fee0dc3532bdf"
|
||||||
integrity sha512-rR7tJoSwJ2eooOpYGxGGW95sLq6GXUaS1UtWvN7pei6n2/okYvCGld9vsUTvkl2migxbkszsycwtMf/GEc1k1A==
|
integrity sha512-rR7tJoSwJ2eooOpYGxGGW95sLq6GXUaS1UtWvN7pei6n2/okYvCGld9vsUTvkl2migxbkszsycwtMf/GEc1k1A==
|
||||||
|
|
||||||
"@hashicorp/design-system-components@^2.6.0":
|
"@hashicorp/design-system-components@^2.12.0":
|
||||||
version "2.6.0"
|
version "2.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/@hashicorp/design-system-components/-/design-system-components-2.6.0.tgz#578cfed9f05d659c49b1bb23093d5df81b600200"
|
resolved "https://registry.yarnpkg.com/@hashicorp/design-system-components/-/design-system-components-2.12.0.tgz#295b910c1673d7f2c8bc62be9f07c585788f2df3"
|
||||||
integrity sha512-mfCTc3JuNME0pVUxxdrcGjFVRnHtkacWEJZyTUByYaM6lerxXQzztuVTEI/eDhH594ytGjLjoPhRm85YYfoGuA==
|
integrity sha512-ewUWfyavTRVVcwKHigICdEIcQeDwLYXt4S/m+xGKyjowRGTfOIcU6tvg5eIB0YVtbhyA1AHEQi80+NhK867Kvg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ember/render-modifiers" "^2.0.5"
|
"@ember/render-modifiers" "^2.0.5"
|
||||||
"@hashicorp/design-system-tokens" "^1.5.0"
|
"@ember/test-waiters" "^3.0.2"
|
||||||
"@hashicorp/ember-flight-icons" "^3.0.4"
|
"@hashicorp/design-system-tokens" "^1.8.0"
|
||||||
|
"@hashicorp/ember-flight-icons" "^3.1.2"
|
||||||
dialog-polyfill "^0.5.6"
|
dialog-polyfill "^0.5.6"
|
||||||
ember-a11y-refocus "^3.0.2"
|
ember-a11y-refocus "^3.0.2"
|
||||||
ember-auto-import "^2.6.0"
|
ember-auto-import "^2.6.3"
|
||||||
ember-cached-decorator-polyfill "^0.1.4"
|
ember-cached-decorator-polyfill "^0.1.4"
|
||||||
ember-cli-babel "^7.26.11"
|
ember-cli-babel "^7.26.11"
|
||||||
|
ember-cli-clipboard "^1.0.0"
|
||||||
ember-cli-htmlbars "^6.2.0"
|
ember-cli-htmlbars "^6.2.0"
|
||||||
ember-cli-sass "^10.0.1"
|
ember-cli-sass "^10.0.1"
|
||||||
ember-composable-helpers "^4.5.0"
|
ember-composable-helpers "^4.5.0"
|
||||||
ember-focus-trap "^1.0.1"
|
ember-focus-trap "^1.0.2"
|
||||||
ember-keyboard "^8.1.0"
|
ember-keyboard "^8.2.0"
|
||||||
ember-named-blocks-polyfill "^0.2.5"
|
|
||||||
ember-stargate "^0.4.3"
|
ember-stargate "^0.4.3"
|
||||||
ember-style-modifier "^0.8.0"
|
ember-style-modifier "^3.0.1"
|
||||||
ember-truth-helpers "^3.0.0"
|
ember-truth-helpers "^3.1.1"
|
||||||
sass "^1.58.3"
|
sass "^1.62.1"
|
||||||
tippy.js "^6.3.7"
|
tippy.js "^6.3.7"
|
||||||
|
|
||||||
"@hashicorp/design-system-tokens@^1.5.0":
|
"@hashicorp/design-system-tokens@^1.8.0":
|
||||||
version "1.5.0"
|
version "1.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/@hashicorp/design-system-tokens/-/design-system-tokens-1.5.0.tgz#e2a5ff96ed4e8b03f3b3258e93ef5eb115479402"
|
resolved "https://registry.yarnpkg.com/@hashicorp/design-system-tokens/-/design-system-tokens-1.8.0.tgz#8734bc46fbdaf72b694927ba7352694d0da3e8e1"
|
||||||
integrity sha512-Th/UOl73XZsPG7ypBrgVR7ZSKV9gfES1nC/E5kqEN0AOSBhlX2JaE2kFFprPYoe+zwaJ6FjASztWKBSK2h7+0A==
|
integrity sha512-miRHSodtBJ0mkBkRpppW857U79lk2vIwNTv7bPmIbX1SQJONFsWQaOXJOKGAHEAxxWpGn0M98xnqo3Eol9Y6Eg==
|
||||||
|
|
||||||
"@hashicorp/ember-flight-icons@^3.0.4":
|
"@hashicorp/ember-flight-icons@^3.0.4":
|
||||||
version "3.0.4"
|
version "3.0.4"
|
||||||
|
@ -3472,11 +3483,26 @@
|
||||||
ember-cli-babel "^7.26.11"
|
ember-cli-babel "^7.26.11"
|
||||||
ember-cli-htmlbars "^6.1.0"
|
ember-cli-htmlbars "^6.1.0"
|
||||||
|
|
||||||
|
"@hashicorp/ember-flight-icons@^3.1.2":
|
||||||
|
version "3.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@hashicorp/ember-flight-icons/-/ember-flight-icons-3.1.2.tgz#39982cae2d1ba6a25cd3f8aa336cae674b2c463c"
|
||||||
|
integrity sha512-aroJ4xd/+6/HTTJnK7KCGNh77Eei8aDpQquEeqBkTT+TK5+C8y043J8joTUAOXq1brIaOwGtd9WuWPpJbb/Csw==
|
||||||
|
dependencies:
|
||||||
|
"@hashicorp/flight-icons" "^2.19.0"
|
||||||
|
ember-auto-import "^2.6.3"
|
||||||
|
ember-cli-babel "^7.26.11"
|
||||||
|
ember-cli-htmlbars "^6.2.0"
|
||||||
|
|
||||||
"@hashicorp/flight-icons@^2.13.0":
|
"@hashicorp/flight-icons@^2.13.0":
|
||||||
version "2.13.0"
|
version "2.13.0"
|
||||||
resolved "https://registry.yarnpkg.com/@hashicorp/flight-icons/-/flight-icons-2.13.0.tgz#5ffa5edc3aa96e8574e57ed8ff049ac652febca0"
|
resolved "https://registry.yarnpkg.com/@hashicorp/flight-icons/-/flight-icons-2.13.0.tgz#5ffa5edc3aa96e8574e57ed8ff049ac652febca0"
|
||||||
integrity sha512-nWZ20v+r3c35OOUMhV+BdT34AHwqNELB59ZcnWaElqbJ4nkppQA9Xr/bT/wGx1yhftwZaDtpRayBWdJCT9zy6g==
|
integrity sha512-nWZ20v+r3c35OOUMhV+BdT34AHwqNELB59ZcnWaElqbJ4nkppQA9Xr/bT/wGx1yhftwZaDtpRayBWdJCT9zy6g==
|
||||||
|
|
||||||
|
"@hashicorp/flight-icons@^2.19.0":
|
||||||
|
version "2.19.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@hashicorp/flight-icons/-/flight-icons-2.19.0.tgz#4088574887232bb50a3c1d6e5044456c90b88e40"
|
||||||
|
integrity sha512-FzEHAOLSQMS5yJorF5H3xP4BKfpIUFRnQgkFl6i1RmvwpOJQgeoz9w/QqWvjh+H/DhFomeC6OxHGgD6rZL7phw==
|
||||||
|
|
||||||
"@hashicorp/structure-icons@^1.3.0":
|
"@hashicorp/structure-icons@^1.3.0":
|
||||||
version "1.9.2"
|
version "1.9.2"
|
||||||
resolved "https://registry.yarnpkg.com/@hashicorp/structure-icons/-/structure-icons-1.9.2.tgz#c75f955b2eec414ecb92f3926c79b4ca01731d3c"
|
resolved "https://registry.yarnpkg.com/@hashicorp/structure-icons/-/structure-icons-1.9.2.tgz#c75f955b2eec414ecb92f3926c79b4ca01731d3c"
|
||||||
|
@ -10133,7 +10159,7 @@ ember-assign-helper@^0.3.0:
|
||||||
ember-cli-babel "^7.19.0"
|
ember-cli-babel "^7.19.0"
|
||||||
ember-cli-htmlbars "^4.3.1"
|
ember-cli-htmlbars "^4.3.1"
|
||||||
|
|
||||||
ember-auto-import@^1.10.1, ember-auto-import@^1.11.3, ember-auto-import@^1.2.19, ember-auto-import@^1.6.0, ember-auto-import@^2.2.4, ember-auto-import@^2.4.0, ember-auto-import@^2.4.2, ember-auto-import@^2.6.0:
|
ember-auto-import@^1.10.1, ember-auto-import@^1.11.3, ember-auto-import@^1.2.19, ember-auto-import@^1.6.0, ember-auto-import@^2.2.4, ember-auto-import@^2.4.0, ember-auto-import@^2.4.2, ember-auto-import@^2.5.0, ember-auto-import@^2.6.0, ember-auto-import@^2.6.3:
|
||||||
version "2.4.0"
|
version "2.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/ember-auto-import/-/ember-auto-import-2.4.0.tgz#91c4797f08315728086e35af954cb60bd23c14bc"
|
resolved "https://registry.yarnpkg.com/ember-auto-import/-/ember-auto-import-2.4.0.tgz#91c4797f08315728086e35af954cb60bd23c14bc"
|
||||||
integrity sha512-BwF6iTaoSmT2vJ9NEHEGRBCh2+qp+Nlaz/Q7roqNSxl5oL5iMRwenPnHhOoBPTYZvPhcV/KgXR5e+pBQ107plQ==
|
integrity sha512-BwF6iTaoSmT2vJ9NEHEGRBCh2+qp+Nlaz/Q7roqNSxl5oL5iMRwenPnHhOoBPTYZvPhcV/KgXR5e+pBQ107plQ==
|
||||||
|
@ -10881,14 +10907,14 @@ ember-cli@~3.28.5:
|
||||||
workerpool "^6.1.4"
|
workerpool "^6.1.4"
|
||||||
yam "^1.0.0"
|
yam "^1.0.0"
|
||||||
|
|
||||||
ember-click-outside@^3.0.0:
|
ember-click-outside@^5.0.0:
|
||||||
version "3.0.0"
|
version "5.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/ember-click-outside/-/ember-click-outside-3.0.0.tgz#a7271345c5960b5dfe1e45a7f7245d1cf8f383dc"
|
resolved "https://registry.yarnpkg.com/ember-click-outside/-/ember-click-outside-5.0.1.tgz#e7df866cf03d940c73741effa0766e175213c7e3"
|
||||||
integrity sha512-X2hLE9Set/tQ9KAEUxfGzCTUgJu/g2sKG+t2ghk/EDz8zF+Y/DPtlxeyZTR6NEPsUbzu3Pqe9gWJUxwaiXC0wg==
|
integrity sha512-RilHTCQvD/5d9pZf6H7MbmBWlVl68nhvn1BPLtfpt9iCNyhtnh5SgwIWGHkJRuTz+DooN6hqTe4Wmq8Zk6kYDw==
|
||||||
dependencies:
|
dependencies:
|
||||||
ember-cli-babel "^7.26.6"
|
ember-cli-babel "^7.26.6"
|
||||||
ember-cli-htmlbars "^5.7.1"
|
ember-cli-htmlbars "^5.7.1"
|
||||||
ember-modifier "^2.1.0 || ^3.0.0"
|
ember-modifier "^3.2.0"
|
||||||
|
|
||||||
ember-compatibility-helpers@^1.1.1, ember-compatibility-helpers@^1.1.2, ember-compatibility-helpers@^1.2.5:
|
ember-compatibility-helpers@^1.1.1, ember-compatibility-helpers@^1.1.2, ember-compatibility-helpers@^1.2.5:
|
||||||
version "1.2.6"
|
version "1.2.6"
|
||||||
|
@ -11076,10 +11102,10 @@ ember-fetch@^8.1.1:
|
||||||
node-fetch "^2.6.1"
|
node-fetch "^2.6.1"
|
||||||
whatwg-fetch "^3.6.2"
|
whatwg-fetch "^3.6.2"
|
||||||
|
|
||||||
ember-focus-trap@^1.0.1:
|
ember-focus-trap@^1.0.2:
|
||||||
version "1.0.1"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/ember-focus-trap/-/ember-focus-trap-1.0.1.tgz#a99565f6ce55d500b92a0965e79e3ad04219f157"
|
resolved "https://registry.yarnpkg.com/ember-focus-trap/-/ember-focus-trap-1.1.0.tgz#e3c47c6e916e838af3884b43e2794e87088d2bac"
|
||||||
integrity sha512-ZUyq5ZkIuXp+ng9rCMkqBh36/V95PltL7iljStkma4+651xlAy3Z84L9WOu/uOJyVpNUxii8RJBbAySHV6c+RQ==
|
integrity sha512-KxbCKpAJaBVZm+bW4tHPoBJAZThmxa6pI+WQusL+bj0RtAnGUNkWsVy6UBMZ5QqTQzf4EvGHkCVACVp5lbAWMQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@embroider/addon-shim" "^1.0.0"
|
"@embroider/addon-shim" "^1.0.0"
|
||||||
focus-trap "^6.7.1"
|
focus-trap "^6.7.1"
|
||||||
|
@ -11141,12 +11167,12 @@ ember-inline-svg@^1.0.1:
|
||||||
svgo "~1.2.2"
|
svgo "~1.2.2"
|
||||||
walk-sync "~2.0.2"
|
walk-sync "~2.0.2"
|
||||||
|
|
||||||
ember-keyboard@^8.1.0:
|
ember-keyboard@^8.2.0:
|
||||||
version "8.2.0"
|
version "8.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/ember-keyboard/-/ember-keyboard-8.2.0.tgz#d11fa7f0443606b7c1850bbd8253274a00046e11"
|
resolved "https://registry.yarnpkg.com/ember-keyboard/-/ember-keyboard-8.2.1.tgz#945a8a71068d81c06ad26851008ef81061db2a59"
|
||||||
integrity sha512-h2kuS2irtIyvNbAMkGDlDTB4TPXwgmC6Nu9bIuGWoCjkGdgJbUg0VegfyRJ1TlxbIHlAelbqVpE8UhfgY5wEag==
|
integrity sha512-wT9xpt3GKsiodGZoifKU4OyeRjXWlmKV9ZHHsp6wJBwMFpl4wWPjTNdINxivk2qg/WFNIh8nUiwuG4+soWXPdw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@embroider/addon-shim" "^1.5.0"
|
"@embroider/addon-shim" "^1.8.4"
|
||||||
ember-destroyable-polyfill "^2.0.3"
|
ember-destroyable-polyfill "^2.0.3"
|
||||||
ember-modifier "^2.1.2 || ^3.1.0 || ^4.0.0"
|
ember-modifier "^2.1.2 || ^3.1.0 || ^4.0.0"
|
||||||
ember-modifier-manager-polyfill "^1.2.0"
|
ember-modifier-manager-polyfill "^1.2.0"
|
||||||
|
@ -11188,7 +11214,7 @@ ember-modifier-manager-polyfill@^1.2.0:
|
||||||
ember-cli-version-checker "^2.1.2"
|
ember-cli-version-checker "^2.1.2"
|
||||||
ember-compatibility-helpers "^1.2.0"
|
ember-compatibility-helpers "^1.2.0"
|
||||||
|
|
||||||
ember-modifier@3.2.7, "ember-modifier@^2.1.0 || ^3.0.0", ember-modifier@^3.0.0, ember-modifier@^3.2.7:
|
ember-modifier@3.2.7, ember-modifier@^3.0.0, ember-modifier@^3.2.0, ember-modifier@^3.2.7:
|
||||||
version "3.2.7"
|
version "3.2.7"
|
||||||
resolved "https://registry.yarnpkg.com/ember-modifier/-/ember-modifier-3.2.7.tgz#f2d35b7c867cbfc549e1acd8d8903c5ecd02ea4b"
|
resolved "https://registry.yarnpkg.com/ember-modifier/-/ember-modifier-3.2.7.tgz#f2d35b7c867cbfc549e1acd8d8903c5ecd02ea4b"
|
||||||
integrity sha512-ezcPQhH8jUfcJQbbHji4/ZG/h0yyj1jRDknfYue/ypQS8fM8LrGcCMo0rjDZLzL1Vd11InjNs3BD7BdxFlzGoA==
|
integrity sha512-ezcPQhH8jUfcJQbbHji4/ZG/h0yyj1jRDknfYue/ypQS8fM8LrGcCMo0rjDZLzL1Vd11InjNs3BD7BdxFlzGoA==
|
||||||
|
@ -11208,6 +11234,15 @@ ember-modifier@3.2.7, "ember-modifier@^2.1.0 || ^3.0.0", ember-modifier@^3.0.0,
|
||||||
ember-cli-normalize-entity-name "^1.0.0"
|
ember-cli-normalize-entity-name "^1.0.0"
|
||||||
ember-cli-string-utils "^1.1.0"
|
ember-cli-string-utils "^1.1.0"
|
||||||
|
|
||||||
|
"ember-modifier@^3.2.7 || ^4.0.0":
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ember-modifier/-/ember-modifier-4.1.0.tgz#cb91efbf8ca4ff4a1a859767afa42dddba5a2bbd"
|
||||||
|
integrity sha512-YFCNpEYj6jdyy3EjslRb2ehNiDvaOrXTilR9+ngq+iUqSHYto2zKV0rleiA1XJQ27ELM1q8RihT29U6Lq5EyqQ==
|
||||||
|
dependencies:
|
||||||
|
"@embroider/addon-shim" "^1.8.4"
|
||||||
|
ember-cli-normalize-entity-name "^1.0.0"
|
||||||
|
ember-cli-string-utils "^1.1.0"
|
||||||
|
|
||||||
ember-moment@^9.0.1:
|
ember-moment@^9.0.1:
|
||||||
version "9.0.1"
|
version "9.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/ember-moment/-/ember-moment-9.0.1.tgz#fcf06cb8ef07c8d0108820c1639778590d613b38"
|
resolved "https://registry.yarnpkg.com/ember-moment/-/ember-moment-9.0.1.tgz#fcf06cb8ef07c8d0108820c1639778590d613b38"
|
||||||
|
@ -11219,14 +11254,6 @@ ember-moment@^9.0.1:
|
||||||
moment "^2.29.1"
|
moment "^2.29.1"
|
||||||
moment-timezone "^0.5.33"
|
moment-timezone "^0.5.33"
|
||||||
|
|
||||||
ember-named-blocks-polyfill@^0.2.5:
|
|
||||||
version "0.2.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/ember-named-blocks-polyfill/-/ember-named-blocks-polyfill-0.2.5.tgz#d5841406277026a221f479c815cfbac6cdcaeecb"
|
|
||||||
integrity sha512-OVMxzkfqJrEvmiky7gFzmuTaImCGm7DOudHWTdMBPO7E+dQSunrcRsJMgO9ZZ56suqBIz/yXbEURrmGS+avHxA==
|
|
||||||
dependencies:
|
|
||||||
ember-cli-babel "^7.19.0"
|
|
||||||
ember-cli-version-checker "^5.1.1"
|
|
||||||
|
|
||||||
ember-on-resize-modifier@^1.0.0:
|
ember-on-resize-modifier@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/ember-on-resize-modifier/-/ember-on-resize-modifier-1.0.0.tgz#b4e12dc023b4d608d7b0f4fa0100722fb860cdd4"
|
resolved "https://registry.yarnpkg.com/ember-on-resize-modifier/-/ember-on-resize-modifier-1.0.0.tgz#b4e12dc023b4d608d7b0f4fa0100722fb860cdd4"
|
||||||
|
@ -11447,13 +11474,14 @@ ember-style-modifier@^0.7.0:
|
||||||
ember-cli-babel "^7.26.6"
|
ember-cli-babel "^7.26.6"
|
||||||
ember-modifier "^3.0.0"
|
ember-modifier "^3.0.0"
|
||||||
|
|
||||||
ember-style-modifier@^0.8.0:
|
ember-style-modifier@^3.0.1:
|
||||||
version "0.8.0"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/ember-style-modifier/-/ember-style-modifier-0.8.0.tgz#ef46b3f288e63e3d850418ea8dc6f7b12edde721"
|
resolved "https://registry.yarnpkg.com/ember-style-modifier/-/ember-style-modifier-3.0.1.tgz#96aaaa2b713108725b81d8b934ec445ece6b89c3"
|
||||||
integrity sha512-I7M+oZ+poYYOP7n521rYv7kkYZbxotL8VbtHYxLQ3tasRZYQJ21qfu3vVjydSjwyE3w7EZRgKngBoMhKSAEZnw==
|
integrity sha512-WHRVIiqY/dpwDtVWlnHW0P4Z+Jha8QEwfaQdIF2ckJL77ZKdjbV2j1XZymS0Nzj61EGx5BM+YEsGL16r3hLv2A==
|
||||||
dependencies:
|
dependencies:
|
||||||
ember-cli-babel "^7.26.6"
|
ember-auto-import "^2.5.0"
|
||||||
ember-modifier "^3.2.7"
|
ember-cli-babel "^7.26.11"
|
||||||
|
ember-modifier "^3.2.7 || ^4.0.0"
|
||||||
|
|
||||||
ember-template-lint@^3.15.0:
|
ember-template-lint@^3.15.0:
|
||||||
version "3.16.0"
|
version "3.16.0"
|
||||||
|
@ -11525,6 +11553,13 @@ ember-tracked-storage-polyfill@1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ember-cli-babel "^7.22.1"
|
ember-cli-babel "^7.22.1"
|
||||||
|
|
||||||
|
ember-truth-helpers@^3.1.1:
|
||||||
|
version "3.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ember-truth-helpers/-/ember-truth-helpers-3.1.1.tgz#434715926d72bcc63b8a115dec09745fda4474dc"
|
||||||
|
integrity sha512-FHwJAx77aA5q27EhdaaiBFuy9No+8yaWNT5A7zs0sIFCmf14GbcLn69vJEp6mW7vkITezizGAWhw7gL0Wbk7DA==
|
||||||
|
dependencies:
|
||||||
|
ember-cli-babel "^7.22.1"
|
||||||
|
|
||||||
"ember-usable@https://github.com/pzuraq/ember-usable#0d03a50":
|
"ember-usable@https://github.com/pzuraq/ember-usable#0d03a50":
|
||||||
version "0.0.0"
|
version "0.0.0"
|
||||||
resolved "https://github.com/pzuraq/ember-usable#0d03a500a2f49041a4ddff0bb05b077c3907ed7d"
|
resolved "https://github.com/pzuraq/ember-usable#0d03a500a2f49041a4ddff0bb05b077c3907ed7d"
|
||||||
|
@ -18638,10 +18673,10 @@ sass@^1.17.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
chokidar ">=3.0.0 <4.0.0"
|
chokidar ">=3.0.0 <4.0.0"
|
||||||
|
|
||||||
sass@^1.58.3:
|
sass@^1.62.1:
|
||||||
version "1.63.3"
|
version "1.67.0"
|
||||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.63.3.tgz#527746aa43bf2e4eac1ab424f67f6f18a081061a"
|
resolved "https://registry.yarnpkg.com/sass/-/sass-1.67.0.tgz#fed84d74b9cd708db603b1380d6dc1f71bb24f6f"
|
||||||
integrity sha512-ySdXN+DVpfwq49jG1+hmtDslYqpS7SkOR5GpF6o2bmb1RL/xS+wvPmegMvMywyfsmAV6p7TgwXYGrCZIFFbAHg==
|
integrity sha512-SVrO9ZeX/QQyEGtuZYCVxoeAL5vGlYjJ9p4i4HFuekWl8y/LtJ7tJc10Z+ck1c8xOuoBm2MYzcLfTAffD0pl/A==
|
||||||
dependencies:
|
dependencies:
|
||||||
chokidar ">=3.0.0 <4.0.0"
|
chokidar ">=3.0.0 <4.0.0"
|
||||||
immutable "^4.0.0"
|
immutable "^4.0.0"
|
||||||
|
|
|
@ -79,7 +79,7 @@ $ curl \
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Update Token
|
## Update Role
|
||||||
|
|
||||||
This endpoint updates an existing ACL Role. The request is always forwarded to the
|
This endpoint updates an existing ACL Role. The request is always forwarded to the
|
||||||
authoritative region.
|
authoritative region.
|
||||||
|
|
Loading…
Reference in New Issue