KMSE Capabilities & Phase 1 Cleanup (#15143)

* fixes issues in key-edit component

* adds capabilities checks for keys and providers

* adds distribute component to key and provider edit
This commit is contained in:
Jordan Reimer 2022-04-26 08:23:31 -06:00 committed by GitHub
parent cc531c793d
commit d6933e9ef4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 531 additions and 388 deletions

View File

@ -149,4 +149,11 @@ export default class KeymgmtKeyAdapter extends ApplicationAdapter {
// TODO: re-fetch record data after // TODO: re-fetch record data after
return this.ajax(this.url(backend, id, 'ROTATE'), 'PUT'); return this.ajax(this.url(backend, id, 'ROTATE'), 'PUT');
} }
removeFromProvider(model) {
const url = `${this.buildURL()}/${model.backend}/kms/${model.provider.name}/key/${model.name}`;
return this.ajax(url, 'DELETE').then(() => {
model.provider = null;
});
}
} }

View File

@ -181,7 +181,7 @@ export default class KeymgmtDistribute extends Component {
.distribute(backend, kms, key, data) .distribute(backend, kms, key, data)
.then(() => { .then(() => {
this.flashMessages.success(`Successfully distributed key ${key} to ${kms}`); this.flashMessages.success(`Successfully distributed key ${key} to ${kms}`);
this.router.transitionTo('vault.cluster.secrets.backend.show', key); this.args.onClose();
}) })
.catch((e) => { .catch((e) => {
this.flashMessages.danger(`Error distributing key: ${e.errors}`); this.flashMessages.danger(`Error distributing key: ${e.errors}`);

View File

@ -2,6 +2,8 @@ import Component from '@glimmer/component';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { action } from '@ember/object'; import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking'; import { tracked } from '@glimmer/tracking';
import { task } from 'ember-concurrency';
import { waitFor } from '@ember/test-waiters';
/** /**
* @module KeymgmtKeyEdit * @module KeymgmtKeyEdit
@ -32,50 +34,50 @@ export default class KeymgmtKeyEdit extends Component {
return this.store.adapterFor('keymgmt/key'); return this.store.adapterFor('keymgmt/key');
} }
get isMutable() {
return ['create', 'edit'].includes(this.args.mode);
}
get isCreating() {
return this.args.mode === 'create';
}
@action @action
toggleModal(bool) { toggleModal(bool) {
this.isDeleteModalOpen = bool; this.isDeleteModalOpen = bool;
} }
@action @task
createKey(evt) { @waitFor
*saveKey(evt) {
evt.preventDefault(); evt.preventDefault();
this.args.model.save(); const { model } = this.args;
try {
yield model.save();
this.router.transitionTo(SHOW_ROUTE, model.name);
} catch (error) {
this.flashMessages.danger(error.errors.join('. '));
}
} }
@action @action
updateKey(evt) { async removeKey() {
evt.preventDefault(); try {
const name = this.args.model.name; await this.keyAdapter.removeFromProvider(this.args.model);
this.args.model this.flashMessages.success('Key has been successfully removed from provider');
.save() } catch (error) {
.then(() => { this.flashMessages.danger(error.errors?.join('. '));
this.router.transitionTo(SHOW_ROUTE, name); }
})
.catch((e) => {
this.flashMessages.danger(e.errors.join('. '));
});
}
@action
removeKey(id) {
// TODO: remove action
console.log('remove', id);
} }
@action @action
deleteKey() { deleteKey() {
const secret = this.args.model; const secret = this.args.model;
const backend = secret.backend; const backend = secret.backend;
console.log({ secret });
secret secret
.destroyRecord() .destroyRecord()
.then(() => { .then(() => {
try { this.router.transitionTo(LIST_ROOT_ROUTE, backend);
this.router.transitionTo(LIST_ROOT_ROUTE, backend, { queryParams: { tab: 'key' } });
} catch (e) {
console.debug(e);
}
}) })
.catch((e) => { .catch((e) => {
this.flashMessages.danger(e.errors?.join('. ')); this.flashMessages.danger(e.errors?.join('. '));

View File

@ -1,5 +1,6 @@
import Model, { attr } from '@ember-data/model'; import Model, { attr } from '@ember-data/model';
import { expandAttributeMeta } from 'vault/utils/field-to-attrs'; import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
export const KEY_TYPES = [ export const KEY_TYPES = [
'aes256-gcm96', 'aes256-gcm96',
@ -11,15 +12,23 @@ export const KEY_TYPES = [
'ecdsa-p521', 'ecdsa-p521',
]; ];
export default class KeymgmtKeyModel extends Model { export default class KeymgmtKeyModel extends Model {
@attr('string') name; @attr('string', {
@attr('string') backend; label: 'Key name',
subText: 'This is the name of the key that shows in Vault.',
})
name;
@attr('string')
backend;
@attr('string', { @attr('string', {
subText: 'The type of cryptographic key that will be created.',
possibleValues: KEY_TYPES, possibleValues: KEY_TYPES,
}) })
type; type;
@attr('boolean', { @attr('boolean', {
label: 'Allow deletion',
defaultValue: false, defaultValue: false,
}) })
deletionAllowed; deletionAllowed;
@ -93,4 +102,27 @@ export default class KeymgmtKeyModel extends Model {
{ name: 'protection', type: 'string', subText: 'Where cryptographic operations are performed.' }, { name: 'protection', type: 'string', subText: 'Where cryptographic operations are performed.' },
]; ];
} }
@lazyCapabilities(apiPath`${'backend'}/key/${'id'}`, 'backend', 'id') keyPath;
@lazyCapabilities(apiPath`${'backend'}/key`, 'backend') keysPath;
@lazyCapabilities(apiPath`${'backend'}/key/${'id'}/kms`, 'backend', 'id') keyProvidersPath;
get canCreate() {
return this.keyPath.get('canCreate');
}
get canDelete() {
return this.keyPath.get('canDelete');
}
get canEdit() {
return this.keyPath.get('canUpdate');
}
get canRead() {
return this.keyPath.get('canRead');
}
get canList() {
return this.keysPath.get('canList');
}
get canListProviders() {
return this.keyProvidersPath.get('canList');
}
} }

View File

@ -2,6 +2,7 @@ import Model, { attr } from '@ember-data/model';
import { tracked } from '@glimmer/tracking'; import { tracked } from '@glimmer/tracking';
import { expandAttributeMeta } from 'vault/utils/field-to-attrs'; import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
import { withModelValidations } from 'vault/decorators/model-validations'; import { withModelValidations } from 'vault/decorators/model-validations';
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
const CRED_PROPS = { const CRED_PROPS = {
azurekeyvault: ['client_id', 'client_secret', 'tenant_id'], azurekeyvault: ['client_id', 'client_secret', 'tenant_id'],
@ -80,7 +81,11 @@ export default class KeymgmtProviderModel extends Model {
const attrs = expandAttributeMeta(this, ['name', 'created', 'keyCollection']); const attrs = expandAttributeMeta(this, ['name', 'created', 'keyCollection']);
attrs.splice(1, 0, { hasBlock: true, label: 'Type', value: this.typeName, icon: this.icon }); attrs.splice(1, 0, { hasBlock: true, label: 'Type', value: this.typeName, icon: this.icon });
const l = this.keys.length; const l = this.keys.length;
const value = l ? `${l} ${l > 1 ? 'keys' : 'key'}` : 'None'; const value = l
? `${l} ${l > 1 ? 'keys' : 'key'}`
: this.canListKeys
? 'None'
: 'You do not have permission to list keys';
attrs.push({ hasBlock: true, isLink: l, label: 'Keys', value }); attrs.push({ hasBlock: true, isLink: l, label: 'Keys', value });
return attrs; return attrs;
} }
@ -104,18 +109,48 @@ export default class KeymgmtProviderModel extends Model {
} }
async fetchKeys(page) { async fetchKeys(page) {
try { if (this.canListKeys) {
this.keys = await this.store.lazyPaginatedQuery('keymgmt/key', { try {
backend: 'keymgmt', this.keys = await this.store.lazyPaginatedQuery('keymgmt/key', {
provider: this.name, backend: 'keymgmt',
responsePath: 'data.keys', provider: this.name,
page, responsePath: 'data.keys',
}); page,
} catch (error) { });
this.keys = []; } catch (error) {
if (error.httpStatus !== 404) { this.keys = [];
throw error; if (error.httpStatus !== 404) {
throw error;
}
} }
} else {
this.keys = [];
} }
} }
@lazyCapabilities(apiPath`${'backend'}/kms/${'id'}`, 'backend', 'id') providerPath;
@lazyCapabilities(apiPath`${'backend'}/kms`, 'backend') providersPath;
@lazyCapabilities(apiPath`${'backend'}/kms/${'id'}/key`, 'backend', 'id') providerKeysPath;
get canCreate() {
return this.providerPath.get('canCreate');
}
get canDelete() {
return this.providerPath.get('canDelete');
}
get canEdit() {
return this.providerPath.get('canUpdate');
}
get canRead() {
return this.providerPath.get('canRead');
}
get canList() {
return this.providersPath.get('canList');
}
get canListKeys() {
return this.providerKeysPath.get('canList');
}
get canCreateKeys() {
return this.providerKeysPath.get('canCreate');
}
} }

View File

@ -147,7 +147,12 @@
class="button is-primary" class="button is-primary"
data-test-secret-save={{true}} data-test-secret-save={{true}}
> >
Save {{if (or (not @key) this.isNewKey) "Add key" "Distribute key"}}
</button>
</div>
<div class="control">
<button type="button" class="button" {{on "click" @onClose}}>
Cancel
</button> </button>
</div> </div>
</div> </div>

View File

@ -4,7 +4,9 @@
</p.top> </p.top>
<p.levelLeft> <p.levelLeft>
<h1 class="title is-3" data-test-secret-header="true"> <h1 class="title is-3" data-test-secret-header="true">
{{#if (eq @mode "create")}} {{#if this.isDistributing}}
Distribute key
{{else if (eq @mode "create")}}
Create key Create key
{{else if (eq @mode "edit")}} {{else if (eq @mode "edit")}}
Edit key Edit key
@ -15,185 +17,218 @@
</p.levelLeft> </p.levelLeft>
</PageHeader> </PageHeader>
{{#if (eq this.mode "show")}} {{#if this.isDistributing}}
<div class="tabs-container box is-sideless is-fullwidth is-paddingless is-marginless" data-test-keymgmt-key-toolbar> <Keymgmt::Distribute @backend={{@model.backend}} @key={{@model}} @onClose={{fn (mut this.isDistributing) false}} />
<nav class="tabs"> {{else}}
<ul> {{#if (eq this.mode "show")}}
<li class={{if (not-eq @tab "versions") "is-active"}}> <div class="tabs-container box is-sideless is-fullwidth is-paddingless is-marginless" data-test-keymgmt-key-toolbar>
<LinkTo <nav class="tabs">
@route="vault.cluster.secrets.backend.show" <ul>
@model={{@model.id}} <li class={{if (not-eq @tab "versions") "is-active"}}>
@query={{hash tab=""}} <LinkTo
data-test-tab="Details" @route="vault.cluster.secrets.backend.show"
@model={{@model.id}}
@query={{hash tab=""}}
data-test-tab="Details"
>
Details
</LinkTo>
</li>
<li class={{if (eq @tab "versions") "is-active"}}>
<LinkTo
@route="vault.cluster.secrets.backend.show"
@model={{@model.id}}
@query={{hash tab="versions"}}
data-test-tab="Versions"
>
Versions
</LinkTo>
</li>
</ul>
</nav>
</div>
<Toolbar>
<ToolbarActions>
{{#if @model.canDelete}}
<button
type="button"
class="toolbar-link"
disabled={{not @model.deletionAllowed}}
{{on "click" (fn (mut this.isDeleteModalOpen) true)}}
data-test-keymgmt-key-destroy
> >
Details Destroy key
</LinkTo> </button>
</li> {{/if}}
<li class={{if (eq @tab "versions") "is-active"}}> {{#if @model.provider}}
<LinkTo <ConfirmAction
@route="vault.cluster.secrets.backend.show" @buttonClasses="toolbar-link"
@model={{@model.id}} @onConfirmAction={{this.removeKey}}
@query={{hash tab="versions"}} @confirmTitle="Remove this key?"
data-test-tab="Versions" @confirmMessage="This will remove all versions of the key from the KMS provider. The key will stay in Vault."
@confirmButtonText="Remove"
data-test-keymgmt-key-remove
> >
Versions Remove key
</LinkTo> </ConfirmAction>
</li> {{/if}}
</ul> {{#if (or @model.canDelete @model.provider)}}
</nav> <div class="toolbar-separator"></div>
</div> {{/if}}
<Toolbar> <ConfirmAction
<ToolbarActions> @buttonClasses="toolbar-link"
<button @onConfirmAction={{fn this.rotateKey @model.id}}
type="button" @confirmTitle="Rotate this key?"
class="toolbar-link" @confirmMessage="After rotation, all key actions will default to using the newest version of the key."
disabled={{not @model.deletionAllowed}} @confirmButtonText="Rotate"
{{on "click" (fn (mut this.isDeleteModalOpen) true)}} data-test-keymgmt-key-rotate
data-test-keymgmt-key-destroy >
> Rotate key
Destroy key </ConfirmAction>
</button> {{#if @model.canEdit}}
<ConfirmAction <ToolbarSecretLink
@buttonClasses="toolbar-link" @secret={{@model.id}}
@onConfirmAction={{fn this.removeKey @model.id}} @mode="edit"
@confirmTitle="Remove this key?" @replace={{true}}
@confirmMessage="This will remove all versions of the key from the KMS provider. The key will stay in Vault." @queryParams={{query-params itemType="key"}}
@confirmButtonText="Remove" @data-test-edit-link={{true}}
data-test-keymgmt-key-remove >
> Edit key
Remove key </ToolbarSecretLink>
</ConfirmAction> {{/if}}
<div class="toolbar-separator"></div> </ToolbarActions>
<ConfirmAction </Toolbar>
@buttonClasses="toolbar-link" {{/if}}
@onConfirmAction={{fn this.rotateKey @model.id}}
@confirmTitle="Rotate this key?"
@confirmMessage="After rotation, all key actions will default to using the newest version of the key."
@confirmButtonText="Rotate"
data-test-keymgmt-key-rotate
>
Rotate key
</ConfirmAction>
<ToolbarSecretLink
@secret={{@model.id}}
@mode="edit"
@replace={{true}}
@queryParams={{query-params itemType="key"}}
@data-test-edit-link={{true}}
>
Edit key
</ToolbarSecretLink>
</ToolbarActions>
</Toolbar>
{{/if}}
{{#if (eq this.mode "create")}} {{#if this.isMutable}}
<form {{on "submit" this.createKey}}> <form {{on "submit" (perform this.saveKey)}}>
{{#each @model.createFields as |attr|}} <div class="box is-sideless is-fullwidth is-marginless">
<FormField @attr={{attr}} @model={{@model}} /> {{#let (if (eq @mode "create") "createFields" "updateFields") as |fieldsKey|}}
{{/each}} {{#each (get @model fieldsKey) as |attr|}}
<input type="submit" value="Create key" /> <FormField data-test-field={{true}} @attr={{attr}} @model={{@model}} />
</form> {{/each}}
{{else if (eq this.mode "edit")}} <div class="field is-grouped box is-fullwidth is-bottomless">
<form {{on "submit" this.updateKey}}> <div class="control">
{{#each @model.updateFields as |attr|}} <button
<FormField data-test-field={{true}} @attr={{attr}} @model={{@model}} /> type="submit"
{{/each}} disabled={{this.saveTask.isRunning}}
<input type="submit" value="Update" /> class="button is-primary {{if this.saveTask.isRunning 'is-loading'}}"
</form> data-test-keymgmt-key-submit
{{else if (eq @tab "versions")}} >
{{#each @model.versions as |version|}} {{if this.isCreating "Create key" "Update"}}
<div class="list-item-row" data-test-keymgmt-key-version> </button>
<div class="columns is-mobile"> </div>
<div class="column is-3 has-text-weight-bold"> <div class="control">
<Icon @name="history" class="has-text-grey-light" /> <LinkTo
<span>Version {{version.id}}</span> @route={{if this.isCreating @root.path "vault.cluster.secrets.backend.show"}}
</div> @model={{if this.isCreating @root.model @model.id}}
<div class="column is-3 has-text-grey"> @query={{unless this.isCreating (hash itemType="key")}}
{{date-from-now version.creation_time addSuffix=true}} @disabled={{this.savekey.isRunning}}
</div> class="button"
<div class="column is-6 is-flex-center"> data-test-keymgmt-key-cancel
{{#if (eq @model.minEnabledVersion version.id)}} >
<Icon @name="check-circle-fill" class="has-text-success" /> Cancel
<span data-test-keymgmt-key-current-min>Current mininum enabled version</span> </LinkTo>
{{/if}} </div>
</div>
{{/let}}
</div>
</form>
{{else if (eq @tab "versions")}}
{{#each @model.versions as |version|}}
<div class="list-item-row" data-test-keymgmt-key-version>
<div class="columns is-mobile">
<div class="column is-3 has-text-weight-bold">
<Icon @name="history" class="has-text-grey-light" />
<span>Version {{version.id}}</span>
</div>
<div class="column is-3 has-text-grey">
{{date-from-now version.creation_time addSuffix=true}}
</div>
<div class="column is-6 is-flex-center">
{{#if (eq @model.minEnabledVersion version.id)}}
<Icon @name="check-circle-fill" class="has-text-success" />
<span data-test-keymgmt-key-current-min>Current mininum enabled version</span>
{{/if}}
</div>
</div> </div>
</div> </div>
</div>
{{/each}}
{{else}}
<div class="has-top-margin-xl has-bottom-margin-s">
<h2 class="title has-border-bottom-light is-5">Key Details</h2>
{{#each @model.showFields as |attr|}}
<InfoTableRow
@alwaysRender={{true}}
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{get @model attr.name}}
@defaultShown={{attr.options.defaultShown}}
@formatDate={{if (eq attr.type "date") "MMM d yyyy, h:mm:ss aaa"}}
/>
{{/each}} {{/each}}
</div> {{else}}
<div class="has-top-margin-xl has-bottom-margin-s"> <div class="has-top-margin-xl has-bottom-margin-s">
<h2 class="title has-border-bottom-light is-5 {{if @model.provider.permissionsError 'is-borderless is-marginless'}}"> <h2 class="title has-border-bottom-light is-5">Key Details</h2>
Distribution Details {{#each @model.showFields as |attr|}}
</h2> <InfoTableRow
{{! TODO: Use capabilities to tell if it's not distributed vs no permissions }} @alwaysRender={{true}}
{{#if @model.provider.permissionsError}} @label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
<EmptyState @value={{get @model attr.name}}
@title="You are not authorized" @defaultShown={{attr.options.defaultShown}}
@subTitle="Error 403" @formatDate={{if (eq attr.type "date") "MMM d yyyy, h:mm:ss aaa"}}
@message={{concat />
"You must be granted permissions to see whether this key is distributed. Ask your administrator if you think you should have access to LIST /" {{/each}}
@model.backend </div>
"/key/" <div class="has-top-margin-xl has-bottom-margin-s">
@model.name <h2 class="title has-border-bottom-light is-5 {{unless @model.provider.canListKeys 'is-borderless is-marginless'}}">
"/kms." Distribution Details
}} </h2>
@icon="minus-circle" {{#if (not @model.provider)}}
/> <EmptyState
{{else if @model.provider}} @title="Key not distributed"
<InfoTableRow @label="Distributed" @value={{@model.provider}}> @message="When this key is distributed to a destination, those details will appear here."
<LinkTo @route="vault.cluster.secrets.backend.show" @model={{concat "kms/" @model.provider}}> data-test-keymgmt-dist-empty-state
<Icon @name="check-circle-fill" class="has-text-success" />{{@model.provider}} >
</LinkTo> {{#if @model.canListProviders}}
</InfoTableRow> <button type="button" class="link" {{on "click" (fn (mut this.isDistributing) true)}}>
{{#if @model.distribution}} Distribute key
{{#each @model.distFields as |attr|}} <Icon @name="chevron-right" />
<InfoTableRow </button>
@alwaysRender={{true}} {{/if}}
@label={{capitalize (or attr.label (humanize (dasherize attr.name)))}} </EmptyState>
@value={{if {{else if (not @model.provider.canListKeys)}}
(eq attr.name "protection")
(uppercase (get @model.distribution attr.name))
(get @model.distribution attr.name)
}}
@defaultShown={{attr.defaultShown}}
@helperText={{attr.subText}}
@formatDate={{if (eq attr.type "date") "MMM d yyyy, h:mm:ss aaa"}}
/>
{{/each}}
{{else}}
<EmptyState <EmptyState
@title="You are not authorized" @title="You are not authorized"
@subTitle="Error 403" @subTitle="Error 403"
@message="You must be granted permissions to view distribution details for this key. Ask your administrator if you think you should have access to GET /keymgmt/keymgmt/key/example." @message={{concat
"You must be granted permissions to see whether this key is distributed. Ask your administrator if you think you should have access to LIST /"
@model.backend
"/key/"
@model.name
"/kms."
}}
@icon="minus-circle" @icon="minus-circle"
/> />
{{else}}
<InfoTableRow @label="Distributed" @value={{@model.provider}}>
<LinkTo @route="vault.cluster.secrets.backend.show" @model={{concat "kms/" @model.provider}}>
<Icon @name="check-circle-fill" class="has-text-success" />{{@model.provider}}
</LinkTo>
</InfoTableRow>
{{#if @model.distribution}}
{{#each @model.distFields as |attr|}}
<InfoTableRow
@alwaysRender={{true}}
@label={{capitalize (or attr.label (humanize (dasherize attr.name)))}}
@value={{if
(eq attr.name "protection")
(uppercase (get @model.distribution attr.name))
(get @model.distribution attr.name)
}}
@defaultShown={{attr.defaultShown}}
@helperText={{attr.subText}}
@formatDate={{if (eq attr.type "date") "MMM d yyyy, h:mm:ss aaa"}}
/>
{{/each}}
{{else}}
<EmptyState
@title="You are not authorized"
@subTitle="Error 403"
@message="You must be granted permissions to view distribution details for this key. Ask your administrator if you think you should have access to GET /keymgmt/keymgmt/key/example."
@icon="minus-circle"
/>
{{/if}}
{{/if}} {{/if}}
{{else}} </div>
<EmptyState {{/if}}
@title="Key not distributed"
@message="When this key is distributed to a destination, those details will appear here."
data-test-keymgmt-dist-empty-state
>
{{! TODO: Distribute link
<LinkTo @route="vault.cluster.secrets.backend.distribute">
Distribute
</LinkTo> }}
</EmptyState>
{{/if}}
</div>
{{/if}} {{/if}}
<ConfirmationModal <ConfirmationModal

View File

@ -4,7 +4,9 @@
</p.top> </p.top>
<p.levelLeft> <p.levelLeft>
<h1 class="title is-3" data-test-kms-provider-header> <h1 class="title is-3" data-test-kms-provider-header>
{{#if this.isShowing}} {{#if this.isDistributing}}
Destribute key to provider
{{else if this.isShowing}}
Provider Provider
<span class="has-font-weight-normal">{{@model.id}}</span> <span class="has-font-weight-normal">{{@model.id}}</span>
{{else}} {{else}}
@ -14,177 +16,192 @@
</p.levelLeft> </p.levelLeft>
</PageHeader> </PageHeader>
{{#if this.isShowing}} {{#if this.isDistributing}}
<div class="tabs-container box is-sideless is-fullwidth is-paddingless is-marginless"> <Keymgmt::Distribute @backend={{@model.backend}} @provider={{@model}} @onClose={{fn (mut this.isDistributing) false}} />
<nav class="tabs">
<ul>
<li class={{unless this.viewingKeys "is-active"}} data-test-kms-provider-tab="details">
<LinkTo @route="vault.cluster.secrets.backend.show" @model={{@model.id}} @query={{hash tab=""}}>
Details
</LinkTo>
</li>
<li class={{if this.viewingKeys "is-active"}} data-test-kms-provider-tab="keys">
<LinkTo @route="vault.cluster.secrets.backend.show" @model={{@model.id}} @query={{hash tab="keys"}}>
Keys
</LinkTo>
</li>
</ul>
</nav>
</div>
{{#unless this.viewingKeys}}
<Toolbar data-test-kms-provider-details-actions>
<ToolbarActions>
<ToolTip @verticalPosition="above" @horizontalPosition="center" as |T|>
<T.Trigger data-test-tooltip-trigger>
<ConfirmAction
@buttonClasses="toolbar-link"
@onConfirmAction={{this.onDelete}}
@disabled={{@model.keys.length}}
data-test-kms-provider-delete={{true}}
>
Delete provider
</ConfirmAction>
</T.Trigger>
{{#if @model.keys.length}}
<T.Content class="tool-tip">
<div class="box" data-test-kms-provider-delete-tooltip>
This provider cannot be deleted until all 20 keys distributed to it are revoked. This can be done from the
Keys tab.
</div>
</T.Content>
{{/if}}
</ToolTip>
<div class="toolbar-separator"></div>
{{! Update once distribute route has been created }}
{{! <LinkTo @route="vault.cluster.secrets.backend.kms-distribute">
Distribute key
<Icon @name="chevron-right" />
</LinkTo> }}
<ToolbarSecretLink
@secret={{@model.id}}
@mode="edit"
@replace={{true}}
@queryParams={{query-params itemType="provider"}}
>
Update credentials
</ToolbarSecretLink>
</ToolbarActions>
</Toolbar>
{{/unless}}
{{else}} {{else}}
<form aria-label="update credentials" {{on "submit" this.onSave}}> {{#if this.isShowing}}
<div class="box is-sideless is-fullwidth is-marginless"> <div class="tabs-container box is-sideless is-fullwidth is-paddingless is-marginless">
{{#if this.isCreating}} <nav class="tabs">
{{#each @model.createFields as |attr index|}} <ul>
{{#if (eq index 2)}} <li class={{unless this.viewingKeys "is-active"}} data-test-kms-provider-tab="details">
<div class="has-border-top-light"> <LinkTo @route="vault.cluster.secrets.backend.show" @model={{@model.id}} @query={{hash tab=""}}>
<h2 class="title is-5 has-top-margin-l has-bottom-margin-m" data-test-kms-provider-config-title> Details
Provider configuration </LinkTo>
</h2> </li>
</div> {{#if @model.canListKeys}}
<li class={{if this.viewingKeys "is-active"}} data-test-kms-provider-tab="keys">
<LinkTo @route="vault.cluster.secrets.backend.show" @model={{@model.id}} @query={{hash tab="keys"}}>
Keys
</LinkTo>
</li>
{{/if}}
</ul>
</nav>
</div>
{{#unless this.viewingKeys}}
<Toolbar data-test-kms-provider-details-actions>
<ToolbarActions>
{{#if @model.canDelete}}
<ToolTip @verticalPosition="above" @horizontalPosition="center" as |T|>
<T.Trigger data-test-tooltip-trigger>
<ConfirmAction
@buttonClasses="toolbar-link"
@onConfirmAction={{this.onDelete}}
@disabled={{@model.keys.length}}
data-test-kms-provider-delete={{true}}
>
Delete provider
</ConfirmAction>
</T.Trigger>
{{#if @model.keys.length}}
<T.Content class="tool-tip">
<div class="box" data-test-kms-provider-delete-tooltip>
This provider cannot be deleted until all
{{@model.keys.length}}
key(s) distributed to it are revoked. This can be done from the Keys tab.
</div>
</T.Content>
{{/if}}
</ToolTip>
{{/if}}
{{#if (and @model.canDelete (or @model.canListKeys @model.canEdit))}}
<div class="toolbar-separator"></div>
{{/if}}
{{#if (or @model.canListKeys @model.canCreateKeys)}}
<button type="button" class="toolbar-link" {{on "click" (fn (mut this.isDistributing) true)}}>
Distribute key
<Icon @name="chevron-right" />
</button>
{{/if}}
{{#if @model.canEdit}}
<ToolbarSecretLink
@secret={{@model.id}}
@mode="edit"
@replace={{true}}
@queryParams={{query-params itemType="provider"}}
disabled={{(not @model.canEdit)}}
>
Update credentials
</ToolbarSecretLink>
{{/if}}
</ToolbarActions>
</Toolbar>
{{/unless}}
{{else}}
<form aria-label="update credentials" {{on "submit" this.onSave}}>
<div class="box is-sideless is-fullwidth is-marginless">
{{#if this.isCreating}}
{{#each @model.createFields as |attr index|}}
{{#if (eq index 2)}}
<div class="has-border-top-light">
<h2 class="title is-5 has-top-margin-l has-bottom-margin-m" data-test-kms-provider-config-title>
Provider configuration
</h2>
</div>
{{/if}}
<FormField @attr={{attr}} @model={{@model}} @modelValidations={{this.modelValidations}} />
{{/each}}
{{/if}}
{{#unless this.isCreating}}
<h2 class="title is-5" data-test-kms-provider-creds-title>
New credentials
</h2>
<p class="sub-text has-bottom-margin-m">
Old credentials cannot be read and will be lost as soon as new ones are added. Do this carefully.
</p>
{{/unless}}
{{#each @model.credentialFields as |cred|}}
<FormField @attr={{cred}} @model={{@model}} @modelValidations={{this.modelValidations}} />
{{/each}}
</div>
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">
<button
type="submit"
disabled={{this.saveTask.isRunning}}
class="button is-primary {{if this.saveTask.isRunning 'is-loading'}}"
data-test-kms-provider-submit
>
{{if this.isCreating "Create provider" "Update"}}
</button>
</div>
<div class="control">
<LinkTo
@route={{if this.isCreating @root.path "vault.cluster.secrets.backend.show"}}
@model={{if this.isCreating @root.model @model.id}}
@query={{if this.isCreating (hash tab="provider") (hash itemType="provider")}}
@disabled={{this.saveTask.isRunning}}
class="button"
data-test-kms-provider-cancel
>
Cancel
</LinkTo>
</div>
</div>
</form>
{{/if}}
{{#if this.isShowing}}
<div class="has-bottom-margin-s">
{{#if this.viewingKeys}}
{{#let (options-for-backend "keymgmt" "key") as |options|}}
{{#if @model.keys.meta.total}}
{{#each @model.keys as |key|}}
<SecretList::Item
@item={{key}}
@backendModel={{@root}}
@backendType="keymgmt"
@delete={{fn this.onDeleteKey key}}
@itemPath={{concat options.modelPrefix key.id}}
@itemType={{options.item}}
@modelType={{@modelType}}
@options={{options}}
/>
{{/each}}
{{#if (gt @model.keys.meta.lastPage 1)}}
<PaginationControls
@total={{@model.keys.meta.total}}
@onChange={{perform this.fetchKeys}}
class="has-top-margin-xl has-bottom-margin-l"
/>
{{/if}}
{{else}}
<EmptyState
@title="No keys for this provider"
@message="Keys for this provider will be listed here. Add a key to get started."
>
<SecretLink @mode="create" @secret="" @queryParams={{query-params itemType="key"}} class="link">
Create key
</SecretLink>
</EmptyState>
{{/if}}
{{/let}}
{{else}}
{{#each @model.showFields as |attr|}}
{{#if attr.hasBlock}}
<InfoTableRow @label={{attr.label}} @value={{attr.value}} data-test-kms-provider-field={{attr.name}}>
{{#if attr.icon}}
<Icon @name={{attr.icon}} class="icon" />
{{/if}}
{{#if attr.isLink}}
<LinkTo @route="vault.cluster.secrets.backend.show" @model={{@model.id}} @query={{hash tab="keys"}}>
{{attr.value}}
</LinkTo>
{{else}}
{{attr.value}}
{{/if}}
</InfoTableRow>
{{else}}
<InfoTableRow
@alwaysRender={{true}}
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{get @model attr.name}}
@defaultShown={{attr.options.defaultShown}}
@formatDate={{if (eq attr.type "date") "MMM d yyyy, h:mm:ss aaa"}}
/>
{{/if}} {{/if}}
<FormField @attr={{attr}} @model={{@model}} @modelValidations={{this.modelValidations}} />
{{/each}} {{/each}}
{{/if}} {{/if}}
{{#unless this.isCreating}}
<h2 class="title is-5" data-test-kms-provider-creds-title>
New credentials
</h2>
<p class="sub-text has-bottom-margin-m">
Old credentials cannot be read and will be lost as soon as new ones are added. Do this carefully.
</p>
{{/unless}}
{{#each @model.credentialFields as |cred|}}
<FormField @attr={{cred}} @model={{@model}} @modelValidations={{this.modelValidations}} />
{{/each}}
</div> </div>
<div class="field is-grouped box is-fullwidth is-bottomless"> {{/if}}
<div class="control">
<button
type="submit"
disabled={{this.saveTask.isRunning}}
class="button is-primary {{if this.saveTask.isRunning 'is-loading'}}"
data-test-kms-provider-submit
>
{{if this.isCreating "Create provider" "Update"}}
</button>
</div>
<div class="control">
<LinkTo
@route={{if this.isCreating @root.path "vault.cluster.secrets.backend.show"}}
@model={{if this.isCreating @root.model @model.id}}
@query={{if this.isCreating (hash tab="provider") (hash itemType="provider")}}
@disabled={{this.saveTask.isRunning}}
class="button"
data-test-kms-provider-cancel
>
Cancel
</LinkTo>
</div>
</div>
</form>
{{/if}}
{{#if this.isShowing}}
<div class="has-bottom-margin-s">
{{#if this.viewingKeys}}
{{#let (options-for-backend "keymgmt" "key") as |options|}}
{{#if @model.keys.meta.total}}
{{#each @model.keys as |key|}}
<SecretList::Item
@item={{key}}
@backendModel={{@root}}
@backendType="keymgmt"
@delete={{fn this.onDeleteKey key}}
@itemPath={{concat options.modelPrefix key.id}}
@itemType={{options.item}}
@modelType={{@modelType}}
@options={{options}}
/>
{{/each}}
{{#if (gt @model.keys.meta.lastPage 1)}}
<PaginationControls
@total={{@model.keys.meta.total}}
@onChange={{perform this.fetchKeys}}
class="has-top-margin-xl has-bottom-margin-l"
/>
{{/if}}
{{else}}
<EmptyState
@title="No keys for this provider"
@message="Keys for this provider will be listed here. Add a key to get started."
>
<SecretLink @mode="create" @secret="" @queryParams={{query-params itemType="key"}} class="link">
Create key
</SecretLink>
</EmptyState>
{{/if}}
{{/let}}
{{else}}
{{#each @model.showFields as |attr|}}
{{#if attr.hasBlock}}
<InfoTableRow @label={{attr.label}} @value={{attr.value}} data-test-kms-provider-field={{attr.name}}>
{{#if attr.icon}}
<Icon @name={{attr.icon}} class="icon" />
{{/if}}
{{#if attr.isLink}}
<LinkTo @route="vault.cluster.secrets.backend.show" @model={{@model.id}} @query={{hash tab="keys"}}>
{{attr.value}}
</LinkTo>
{{else}}
{{attr.value}}
{{/if}}
</InfoTableRow>
{{else}}
<InfoTableRow
@alwaysRender={{true}}
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{get @model attr.name}}
@defaultShown={{attr.options.defaultShown}}
@formatDate={{if (eq attr.type "date") "MMM d yyyy, h:mm:ss aaa"}}
/>
{{/if}}
{{/each}}
{{/if}}
</div>
{{/if}} {{/if}}

View File

@ -85,13 +85,10 @@ module('Integration | Component | keymgmt/distribute', function (hooks) {
this.server.shutdown(); this.server.shutdown();
}); });
test('it does not render without @backend attr', async function (assert) {
await render(hbs`<Keymgmt::Distribute />`);
assert.dom(SELECTORS.form).doesNotExist('Form does not exist');
});
test('it does not allow operation selection until valid key and provider selected', async function (assert) { test('it does not allow operation selection until valid key and provider selected', async function (assert) {
await render(hbs`<Keymgmt::Distribute @backend="keymgmt" @providers={{providers}} />`); await render(
hbs`<Keymgmt::Distribute @backend="keymgmt" @providers={{providers}} @onClose={{fn (mut this.onClose)}} />`
);
assert.dom(SELECTORS.operationsSection).hasAttribute('disabled'); assert.dom(SELECTORS.operationsSection).hasAttribute('disabled');
await clickTrigger(); await clickTrigger();
await settled(); await settled();
@ -108,7 +105,9 @@ module('Integration | Component | keymgmt/distribute', function (hooks) {
assert.dom(SELECTORS.errorProvider).exists('Shows key/provider match error on provider'); assert.dom(SELECTORS.errorProvider).exists('Shows key/provider match error on provider');
}); });
test('it shows key type select field if new key created', async function (assert) { test('it shows key type select field if new key created', async function (assert) {
await render(hbs`<Keymgmt::Distribute @backend="keymgmt" @providers={{providers}} />`); await render(
hbs`<Keymgmt::Distribute @backend="keymgmt" @providers={{providers}} @onClose={{fn (mut this.onClose)}} />`
);
assert.dom(SELECTORS.keyTypeSection).doesNotExist('Key Type section is not rendered by default'); assert.dom(SELECTORS.keyTypeSection).doesNotExist('Key Type section is not rendered by default');
// Add new item on search-select // Add new item on search-select
await clickTrigger(); await clickTrigger();
@ -118,7 +117,9 @@ module('Integration | Component | keymgmt/distribute', function (hooks) {
assert.dom(SELECTORS.keyTypeSection).exists('Key Type selector is shown'); assert.dom(SELECTORS.keyTypeSection).exists('Key Type selector is shown');
}); });
test('it hides the provider field if passed from the parent', async function (assert) { test('it hides the provider field if passed from the parent', async function (assert) {
await render(hbs`<Keymgmt::Distribute @backend="keymgmt" @provider="provider-azure" />`); await render(
hbs`<Keymgmt::Distribute @backend="keymgmt" @provider="provider-azure" @onClose={{fn (mut this.onClose)}} />`
);
assert.dom(SELECTORS.providerInput).doesNotExist('Provider input is hidden'); assert.dom(SELECTORS.providerInput).doesNotExist('Provider input is hidden');
// Select existing key // Select existing key
await clickTrigger(); await clickTrigger();
@ -140,7 +141,9 @@ module('Integration | Component | keymgmt/distribute', function (hooks) {
assert.dom(SELECTORS.errorNewKey).exists('Shows error on key type'); assert.dom(SELECTORS.errorNewKey).exists('Shows error on key type');
}); });
test('it hides the key field if passed from the parent', async function (assert) { test('it hides the key field if passed from the parent', async function (assert) {
await render(hbs`<Keymgmt::Distribute @backend="keymgmt" @providers={{providers}} @key="example-1" />`); await render(
hbs`<Keymgmt::Distribute @backend="keymgmt" @providers={{providers}} @key="example-1" @onClose={{fn (mut this.onClose)}} />`
);
assert.dom(SELECTORS.providerInput).exists('Provider input shown'); assert.dom(SELECTORS.providerInput).exists('Provider input shown');
assert.dom(SELECTORS.keySection).doesNotExist('Key input not shown'); assert.dom(SELECTORS.keySection).doesNotExist('Key input not shown');
await select(SELECTORS.providerInput, 'provider-azure'); await select(SELECTORS.providerInput, 'provider-azure');

View File

@ -23,6 +23,7 @@ module('Integration | Component | keymgmt/key-edit', function (hooks) {
creation_time: now, creation_time: now,
}, },
], ],
canDelete: true,
}); });
this.model = model; this.model = model;
this.tab = ''; this.tab = '';

View File

@ -47,6 +47,12 @@ module('Integration | Component | keymgmt/provider-edit', function (hooks) {
test('it should render show view', async function (assert) { test('it should render show view', async function (assert) {
assert.expect(13); assert.expect(13);
// override capability getters
Object.defineProperties(this.model, {
canDelete: { value: true },
canListKeys: { value: true },
});
this.server.get('/keymgmt/kms/foo-bar/key', () => { this.server.get('/keymgmt/kms/foo-bar/key', () => {
return { return {
data: { data: {