Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ui] ACL Roles in the UI, plus Role, Policy and Token management #17770

Merged
merged 5 commits into from
Sep 27, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ember-test-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
with:
ref: ${{ github.event.pull_request.base.sha }}
- uses: nanasess/setup-chromedriver@6fb8f5ffa6b7dc11e631ff695fbd2fec0b04bb52 # v2.1.1
- uses: nanasess/setup-chromedriver@69cc01d772a1595b8aee87d52f53e71b3904d9d0 # v2.1.2
- name: Use Node.js
uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0
with:
Expand All @@ -35,7 +35,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- uses: nanasess/setup-chromedriver@6fb8f5ffa6b7dc11e631ff695fbd2fec0b04bb52 # v2.1.1
- uses: nanasess/setup-chromedriver@69cc01d772a1595b8aee87d52f53e71b3904d9d0 # v2.1.2
- name: Use Node.js
uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0
with:
Expand Down
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"
[email protected]
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
Loading