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: add capabilities to pki key model #18412

Merged
merged 14 commits into from
Dec 16, 2022
26 changes: 26 additions & 0 deletions ui/app/models/pki/key.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Model, { attr } from '@ember-data/model';
import { inject as service } from '@ember/service';
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
import { withFormFields } from 'vault/decorators/model-form-fields';
import { withModelValidations } from 'vault/decorators/model-validations';

Expand Down Expand Up @@ -40,4 +41,29 @@ export default class PkiKeyModel extends Model {
get backend() {
return this.secretMountPath.currentPath;
}

/* CAPABILITIES
* Default to show UI elements unless we know they can't access the given path
*/

@lazyCapabilities(apiPath`${'backend'}/key/${'key_id'}`, 'backend', 'key_id') keyPath;
get canRead() {
return this.keyPath.get('canRead') !== false;
}
get canEdit() {
return this.keyPath.get('canUpdate') !== false;
}
get canDelete() {
return this.keyPath.get('canDelete') !== false;
}

@lazyCapabilities(apiPath`${'backend'}/keys/generate`, 'backend') generatePath;
get canGenerateKey() {
return this.generatePath.get('canUpdate') !== false;
}

@lazyCapabilities(apiPath`${'backend'}/keys/import`, 'backend') importPath;
get canImportKey() {
return this.importPath.get('canUpdate') !== false;
}
}
30 changes: 17 additions & 13 deletions ui/lib/pki/addon/components/page/pki-key-details.hbs
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
<Toolbar>
<ToolbarActions>
<ConfirmAction
@buttonClasses="toolbar-link"
@onConfirmAction={{this.deleteKey}}
@confirmTitle="Delete key?"
@confirmButtonText="Delete"
data-test-pki-key-delete
>
Delete
</ConfirmAction>
<div class="toolbar-separator"></div>
<ToolbarLink @route="keys.key.edit" @model={{@key.model.name}}>
Edit key
</ToolbarLink>
{{#if @canDelete}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super excited about this pattern 🤩

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same! great idea 🥇

<ConfirmAction
@buttonClasses="toolbar-link"
@onConfirmAction={{this.deleteKey}}
@confirmTitle="Delete key?"
@confirmButtonText="Delete"
data-test-pki-key-delete
>
Delete
</ConfirmAction>
<div class="toolbar-separator"></div>
{{/if}}
{{#if @canEdit}}
<ToolbarLink @route="keys.key.edit" @model={{@key.keyId}} data-test-pki-key-edit>
Edit key
</ToolbarLink>
{{/if}}
</ToolbarActions>
</Toolbar>

Expand Down
61 changes: 61 additions & 0 deletions ui/lib/pki/addon/components/page/pki-key-list.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<Toolbar>
<ToolbarActions>
{{#if @canImportKey}}
<ToolbarLink @route="keys.import" @type="download">
Import
</ToolbarLink>
{{/if}}
{{#if @canGenerateKey}}
<ToolbarLink @route="keys.create" @type="add">
Generate
</ToolbarLink>
{{/if}}
</ToolbarActions>
</Toolbar>
<p class="has-padding">Below is information about the private keys used by the issuers to sign certificates. While
certificates represent a public assertion of an identity, private keys represent the private part of that identity, a
secret used to prove who they are and who they trust.</p>

{{#if @keyModels.length}}
{{#each @keyModels as |pkiKey|}}
<LinkedBlock class="list-item-row" @params={{array "keys.key.details" pkiKey.keyId}} @linkPrefix={{@mountPoint}}>
<div class="level is-mobile">
<div class="level-left">
<div>
<Icon @name="certificate" class="has-text-grey-light" />
<span class="has-text-weight-semibold is-underline">
{{pkiKey.id}}
</span>
<div class="is-flex-row has-left-margin-l has-top-margin-xs">
{{#if pkiKey.keyName}}
<span class="tag has-text-grey-dark">{{pkiKey.keyName}}</span>
{{/if}}
</div>
</div>
</div>
<div class="level-right is-flex is-paddingless is-marginless">
<div class="level-item">
<PopupMenu>
<nav class="menu">
<ul class="menu-list">
<li>
<LinkTo @route="keys.key.details" @model={{pkiKey.keyId}} @disabled={{unless @canRead}}>
Details
</LinkTo>
</li>
<li>
<LinkTo @route="keys.key.edit" @model={{pkiKey.keyId}} @disabled={{unless @canEdit}}>
Edit
</LinkTo>
</li>
</ul>
</nav>
</PopupMenu>
</div>
</div>
</div>
</LinkedBlock>
{{/each}}
{{else}}
<EmptyState @title="No keys yet" @message="There are no keys in this PKI mount. You can generate or create one." />
{{/if}}
1 change: 0 additions & 1 deletion ui/lib/pki/addon/routes/keys/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { withConfirmLeave } from 'core/decorators/confirm-leave';
@withConfirmLeave()
export default class PkiKeysCreateRoute extends PkiKeysIndexRoute {
@service store;
@service secretMountPath;

model() {
return this.store.createRecord('pki/key');
Expand Down
21 changes: 10 additions & 11 deletions ui/lib/pki/addon/routes/keys/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Route from '@ember/routing/route';
import PkiOverviewRoute from '../overview';
import { inject as service } from '@ember/service';

export default class PkiKeysIndexRoute extends Route {
import { hash } from 'rsvp';
export default class PkiKeysIndexRoute extends PkiOverviewRoute {
@service store;
@service secretMountPath;
@service pathHelp;
Expand All @@ -12,18 +12,17 @@ export default class PkiKeysIndexRoute extends Route {
}

model() {
return this.store
.query('pki/key', { backend: this.secretMountPath.currentPath })
.then((keyModel) => {
return { keyModel, parentModel: this.modelFor('keys') };
})
.catch((err) => {
return hash({
hasConfig: this.hasConfig(),
parentModel: this.modelFor('keys'),
keyModels: this.store.query('pki/key', { backend: this.secretMountPath.currentPath }).catch((err) => {
if (err.httpStatus === 404) {
return { parentModel: this.modelFor('keys') };
return [];
} else {
throw err;
}
});
}),
});
}

setupController(controller, resolvedModel) {
Expand Down
63 changes: 10 additions & 53 deletions ui/lib/pki/addon/templates/keys/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -8,60 +8,17 @@
}}
@isEngine={{true}}
/>
<Toolbar>
<ToolbarActions>
<ToolbarLink @route="keys.import" @type="download">
Import
</ToolbarLink>
<ToolbarLink @route="keys.create" @type="add">
Generate
</ToolbarLink>
</ToolbarActions>
</Toolbar>
<p class="has-padding">Below is information about the private keys used by the issuers to sign certificates. While
certificates represent a public assertion of an identity, private keys represent the private part of that identity, a
secret used to prove who they are and who they trust.</p>
{{#if this.model.keyModel.length}}
{{#each this.model.keyModel as |pkiKey|}}
<LinkedBlock class="list-item-row" @params={{array "keys.key.details" pkiKey.keyId}} @linkPrefix={{this.mountPoint}}>
<div class="level is-mobile">
<div class="level-left">
<div>
<Icon @name="certificate" class="has-text-grey-light" />
<span class="has-text-weight-semibold is-underline">
{{pkiKey.id}}
</span>
<div class="is-flex-row has-left-margin-l has-top-margin-xs">
{{#if pkiKey.keyName}}
<span class="tag has-text-grey-dark">{{pkiKey.keyName}}</span>
{{/if}}
</div>
</div>
</div>
<div class="level-right is-flex is-paddingless is-marginless">
<div class="level-item">
<PopupMenu>
<nav class="menu">
<ul class="menu-list">
<li>
<LinkTo @route="keys.key.details" @model={{pkiKey.keyId}}>
Details
</LinkTo>
</li>
<li>
<LinkTo @route="keys.key.edit" @model={{pkiKey.keyId}}>
Edit
</LinkTo>
</li>
</ul>
</nav>
</PopupMenu>
</div>
</div>
</div>
</LinkedBlock>
{{/each}}
{{#if this.model.hasConfig}}
<Page::PkiKeyList
@keyModels={{this.model.keyModels}}
@mountPoint={{this.mountPoint}}
@canImportKey={{this.model.keyModels.firstObject.canImportKey}}
@canGenerateKey={{this.model.keyModels.firstObject.canGenerateKey}}
@canRead={{this.model.keyModels.firstObject.canRead}}
@canEdit={{this.model.keyModels.firstObject.canEdit}}
/>
{{else}}
<Toolbar />
<EmptyState @title="PKI not configured" @message="This PKI mount hasn’t yet been configured with a certificate issuer.">
<LinkTo @route="configuration.create">
Configure PKI
Expand Down
2 changes: 1 addition & 1 deletion ui/lib/pki/addon/templates/keys/key/details.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@
</p.levelLeft>
</PageHeader>

<Page::PkiKeyDetails @key={{this.model}} />
<Page::PkiKeyDetails @key={{this.model}} @canDelete={{this.model.canDelete}} @canEdit={{this.model.canEdit}} />
1 change: 1 addition & 0 deletions ui/tests/helpers/pki/keys/page-details.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ export const SELECTORS = {
keyTypeValue: '[data-test-value-div="Key type"]',
keyBitsValue: '[data-test-value-div="Key bits"]',
keyDeleteButton: '[data-test-pki-key-delete] button',
keyEditLink: '[data-test-pki-key-edit]',
confirmDelete: '[data-test-confirm-button]',
};
33 changes: 31 additions & 2 deletions ui/tests/integration/components/pki/keys/page-details-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,18 @@ module('Integration | Component | pki key details page', function (hooks) {
});

test('it renders the page component and deletes a key', async function (assert) {
assert.expect(6);
assert.expect(7);
this.server.delete(`${this.backend}/key/${this.model.keyId}`, () => {
assert.ok(true, 'confirming delete fires off destroyRecord()');
});

await render(
hbs`
<Page::PkiKeyDetails @key={{this.model}} />
<Page::PkiKeyDetails
@key={{this.model}}
@canDelete={{true}}
@canEdit={{true}}
/>
`,
{ owner: this.engine }
);
Expand All @@ -43,8 +47,33 @@ module('Integration | Component | pki key details page', function (hooks) {
assert.dom(SELECTORS.keyNameValue).hasText('test-key', 'key name renders');
assert.dom(SELECTORS.keyTypeValue).hasText('ec', 'key type renders');
assert.dom(SELECTORS.keyBitsValue).doesNotExist('does not render empty value');
assert
.dom(SELECTORS.keyEditLink)
.hasAttribute(
'href',
`/ui/vault/secrets/${this.backend}/pki/keys/${this.model.keyId}/edit`,
'renders edit link with correct href'
);
assert.dom(SELECTORS.keyDeleteButton).exists('renders delete button');
await click(SELECTORS.keyDeleteButton);
await click(SELECTORS.confirmDelete);
});

test('it does not render actions when capabilities are false', async function (assert) {
assert.expect(2);

await render(
hbs`
<Page::PkiKeyDetails
@key={{this.model}}
@canDelete={{false}}
@canEdit={{false}}
/>
`,
{ owner: this.engine }
);

assert.dom(SELECTORS.keyDeleteButton).doesNotExist('does not render delete button if no permission');
assert.dom(SELECTORS.keyEditLink).doesNotExist('does not render edit button if no permission');
});
});