Skip to content

Commit

Permalink
[ui] ACL Roles in the UI, plus Role, Policy and Token management (#17770
Browse files Browse the repository at this point in the history
) (#18599)

* 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
  • Loading branch information
philrenaud authored Sep 27, 2023
1 parent 6877591 commit 183decc
Show file tree
Hide file tree
Showing 78 changed files with 4,076 additions and 454 deletions.
3 changes: 3 additions & 0 deletions .changelog/17770.txt
Original file line number Diff line number Diff line change
@@ -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
```
7 changes: 5 additions & 2 deletions nomad/structs/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,9 +363,12 @@ func (a *ACLToken) Validate(minTTL, maxTTL time.Duration, existing *ACLToken) er
if existing.ExpirationTTL != a.ExpirationTTL {
mErr.Errors = append(mErr.Errors, errors.New("cannot update expiration TTL"))
}
if existing.ExpirationTime != a.ExpirationTime {
mErr.Errors = append(mErr.Errors, errors.New("cannot update expiration time"))
if a.ExpirationTime != nil {
if !existing.ExpirationTime.Equal(*a.ExpirationTime) {
mErr.Errors = append(mErr.Errors, errors.New("cannot update expiration time"))
}
}

}

return mErr.ErrorOrNil()
Expand Down
17 changes: 17 additions & 0 deletions ui/app/abilities/role.js
Original file line number Diff line number Diff line change
@@ -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;
}
22 changes: 22 additions & 0 deletions ui/app/adapters/role.js
Original file line number Diff line number Diff line change
@@ -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 changes: 14 additions & 1 deletion ui/app/adapters/token.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,22 @@ export default class TokenAdapter extends ApplicationAdapter {

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) {
let data = this.serialize(snapshot);
data.Policies = data.PolicyIDs;
return this.ajax(`${this.buildURL()}/token`, 'POST', { data });
}

Expand Down
13 changes: 13 additions & 0 deletions ui/app/components/access-control-subnav.js
Original file line number Diff line number Diff line change
@@ -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;
}
4 changes: 2 additions & 2 deletions ui/app/components/editable-variable-link.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
{{#if (can "write variable")}}
{{#with (editable-variable-link @path existingPaths=@existingPaths namespace=@namespace) as |link|}}
{{#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}}
<LinkTo @route={{link.route}} @query={{link.query}}>{{@path}}</LinkTo>
<Hds::Link::Inline @route={{link.route}} @query={{link.query}}>{{@path}}</Hds::Link::Inline>
{{/if}}
{{/with}}
{{else}}
Expand Down
16 changes: 8 additions & 8 deletions ui/app/components/policy-editor.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
@type="text"
@value={{@policy.name}}
class="input"
{{autofocus}}
{{autofocus}}
/>
</label>
{{/if}}
Expand All @@ -34,7 +34,7 @@
mode="ruby"
content=@policy.rules
onUpdate=this.updatePolicyRules
autofocus=(not @policy.isNew)
autofocus=false
extraKeys=(hash Cmd-Enter=this.save)
}} />
</div>
Expand All @@ -55,12 +55,12 @@

<footer>
{{#if (can "update policy")}}
<button
class="button is-primary"
type="submit"
>
Save Policy
</button>
<Hds::Button
@text="Save Policy"
@type="submit"
data-test-save-policy
{{on "click" this.save}}
/>
{{/if}}
</footer>
</form>
19 changes: 12 additions & 7 deletions ui/app/components/policy-editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.`
);
}

const shouldRedirectAfterSave = this.policy.isNew;

// Because we set the ID for adapter/serialization reasons just before save here,
// that becomes a barrier to our Unique Name validation. So we explicltly exclude
// the current policy when checking for uniqueness.
if (
this.policy.isNew &&
this.store.peekRecord('policy', this.policy.name)
this.store
.peekAll('policy')
.filter((policy) => policy !== this.policy)
.findBy('name', this.policy.name)
) {
throw new Error(
`A policy with name ${this.policy.name} already exists.`
);
}

this.policy.id = this.policy.name;

this.policy.set('id', this.policy.name);
await this.policy.save();

this.notifications.add({
Expand All @@ -52,7 +54,10 @@ export default class PolicyEditorComponent extends Component {
});

if (shouldRedirectAfterSave) {
this.router.transitionTo('policies.policy', this.policy.id);
this.router.transitionTo(
'access-control.policies.policy',
this.policy.id
);
}
} catch (error) {
this.notifications.add({
Expand Down
26 changes: 10 additions & 16 deletions ui/app/components/profile-navbar-item.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,16 @@
~}}

{{#if this.token.selfToken}}
<PowerSelect
data-test-header-profile-dropdown
{{keyboard-shortcut menuLevel=true pattern=(array "g" "p") }}
@options={{this.profileOptions}}
@onChange={{action (queue
(fn (mut this.profileSelection))
this.profileSelection.action
)}}
@dropdownClass="dropdown-options"
@matchTriggerWidth={{false}}
@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>
<Hds::Dropdown @color="secondary" class="profile-dropdown"
{{keyboard-shortcut menuLevel=true pattern=(array "g" "p") }}
as |dd|>
<dd.ToggleIcon @color="secondary" @icon="user-circle" @text="user menu" @size="small" data-test-header-profile-dropdown />
<dd.Title @text="Signed In" />
<dd.Description @text={{this.token.selfToken.name}} />
<dd.Separator />
<dd.Interactive @route="settings.tokens" @text="Profile" data-test-profile-dropdown-profile-link />
<dd.Interactive {{on "click" this.signOut}} @text="Sign Out" @color="critical" data-test-profile-dropdown-sign-out-link />
</Hds::Dropdown>
{{else}}
<LinkTo data-test-header-signin-link @route="settings.tokens" class="navbar-item" {{keyboard-shortcut menuLevel=true pattern=(array "g" "p") }}>
Sign In
Expand Down
36 changes: 11 additions & 25 deletions ui/app/components/profile-navbar-item.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,24 @@
*/

// @ts-check

import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';

export default class ProfileNavbarItemComponent extends Component {
@service token;
@service router;
@service store;

profileOptions = [
{
label: 'Authorization',
key: 'authorization',
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
this.store.unloadAll();
this.token.reset();
this.router.transitionTo('jobs.index');
},
},
];
@action
signOut() {
this.token.setProperties({
secret: undefined,
});

profileSelection = this.profileOptions[0];
// Clear out all data to ensure only data the anonymous token is privileged to see is shown
this.store.unloadAll();
this.token.reset();
this.router.transitionTo('jobs.index');
}
}
78 changes: 78 additions & 0 deletions ui/app/components/role-editor.hbs
Original file line number Diff line number Diff line change
@@ -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>
Loading

0 comments on commit 183decc

Please sign in to comment.