UI: keymgmt secret engine (#15523)

* No default provider on create, add subText to service_account_file field

* Show empty state if no provider selected -- sorry for all the conditionals

* Button and distribution title styling on key edit

* Fix key distribute empty state permissions

* Don't try to fetch distribution if provider is permissionError

* Use search-select component for provider on distribute component

* Show distribution form errors on page rather than popup

* Add id, label, subtext to input-search for search-select fallback

* Remove created field from provider, default to querying for keys unless capabilities is false

* Fix link to provider from key-edit

* Search select label styling and add subText to fallback

* Refetch model after key rotate

* Create distribution method is task so we can load and disable button

* Move keymgmt to cloud group on mount options

* Key actions are tasks, fix tab active class

* Add isRunning attr to confirm-action which disables confirm button and replaces text with loader

* Fix provider active tab class

* Handle control groups on distribution

* Correctly handle error message on key-edit

* Show loading state on distribute, reload key after distribute

* Clear old validation errors if valid

* Fix tests

* Fix delete url

* Add changelog

* Address PR comments

* kick circle-ci

* Format go file breaking fmt

* Rename old changelog

* Remove resolved TODO
This commit is contained in:
Chelsea Shaw 2022-05-20 10:41:24 -05:00 committed by GitHub
parent 892d4d1e37
commit 81105e6209
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 238 additions and 135 deletions

3
changelog/15523.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
**KeyMgmt UI**: Add UI support for managing the Key Management Secrets Engine
```

View File

@ -1,5 +1,6 @@
import ApplicationAdapter from '../application';
import { encodePath } from 'vault/utils/path-encoding-helpers';
import ControlGroupError from '../../lib/control-group-error';
function pickKeys(obj, picklist) {
const data = {};
@ -13,6 +14,21 @@ function pickKeys(obj, picklist) {
export default class KeymgmtKeyAdapter extends ApplicationAdapter {
namespace = 'v1';
pathForType() {
// backend name prepended in buildURL method
return 'key';
}
buildURL(modelName, id, snapshot, requestType, query) {
let url = super.buildURL(...arguments);
if (snapshot) {
url = url.replace('key', `${snapshot.attr('backend')}/key`);
} else if (query) {
url = url.replace('key', `${query.backend}/key`);
}
return url;
}
url(backend, id, type) {
const url = `${this.buildURL()}/${backend}/key`;
if (id) {
@ -26,12 +42,6 @@ export default class KeymgmtKeyAdapter extends ApplicationAdapter {
return url;
}
urlForDeleteRecord(store, type, snapshot) {
const name = snapshot.attr('name');
const backend = snapshot.attr('backend');
return this.url(backend, name);
}
_updateKey(backend, name, serialized) {
// Only these two attributes are allowed to be updated
let data = pickKeys(serialized, ['deletion_allowed', 'min_enabled_version']);
@ -53,8 +63,7 @@ export default class KeymgmtKeyAdapter extends ApplicationAdapter {
if (snapshot.attr('deletionAllowed')) {
try {
await this._updateKey(backend, name, data);
} catch (e) {
// TODO: Test how this works with UI
} catch {
throw new Error(`Key ${name} was created, but not all settings were saved`);
}
}
@ -95,7 +104,6 @@ export default class KeymgmtKeyAdapter extends ApplicationAdapter {
} else if (e.httpStatus === 403) {
return { permissionsError: true };
}
// TODO: handle control group
throw e;
}
}
@ -109,8 +117,10 @@ export default class KeymgmtKeyAdapter extends ApplicationAdapter {
purposeArray: res.data.purpose.split(','),
};
})
.catch(() => {
// TODO: handle control group
.catch((e) => {
if (e instanceof ControlGroupError) {
throw e;
}
return null;
});
}
@ -123,7 +133,7 @@ export default class KeymgmtKeyAdapter extends ApplicationAdapter {
let provider, distribution;
if (!recordOnly) {
provider = await this.getProvider(backend, id);
if (provider) {
if (provider && !provider.permissionsError) {
distribution = await this.getDistribution(backend, provider, id);
}
}
@ -145,13 +155,15 @@ export default class KeymgmtKeyAdapter extends ApplicationAdapter {
});
}
rotateKey(backend, id) {
// TODO: re-fetch record data after
return this.ajax(this.url(backend, id, 'ROTATE'), 'PUT');
async rotateKey(backend, id) {
let keyModel = this.store.peekRecord('keymgmt/key', id);
const result = await this.ajax(this.url(backend, id, 'ROTATE'), 'PUT');
await keyModel.reload();
return result;
}
removeFromProvider(model) {
const url = `${this.buildURL()}/${model.backend}/kms/${model.provider.name}/key/${model.name}`;
const url = `${this.buildURL()}/${model.backend}/kms/${model.provider}/key/${model.name}`;
return this.ajax(url, 'DELETE').then(() => {
model.provider = null;
});

View File

@ -3,6 +3,8 @@ import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { KEY_TYPES } from '../../models/keymgmt/key';
import { task } from 'ember-concurrency';
import { waitFor } from '@ember/test-waiters';
/**
* @module KeymgmtDistribute
@ -39,6 +41,7 @@ export default class KeymgmtDistribute extends Component {
@tracked isNewKey = false;
@tracked providerType;
@tracked formData;
@tracked formErrors;
constructor() {
super(...arguments);
@ -196,15 +199,20 @@ export default class KeymgmtDistribute extends Component {
this.args.onClose();
})
.catch((e) => {
this.flashMessages.danger(`Error distributing key: ${e.errors}`);
this.formErrors = `${e.errors}`;
});
}
@action
handleProvider(evt) {
this.formData.provider = evt.target.value;
if (evt.target.value) {
this.getProviderType(evt.target.value);
handleProvider(selection) {
let providerName = selection[0];
if (typeof selection === 'string') {
// Handles case if no list permissions and fallback component is used
providerName = selection;
}
this.formData.provider = providerName;
if (providerName) {
this.getProviderType(providerName);
}
}
@action
@ -235,8 +243,9 @@ export default class KeymgmtDistribute extends Component {
return this.getKeyInfo(selectedKey.id, selectedKey.isNew);
}
@action
async createDistribution(evt) {
@task
@waitFor
*createDistribution(evt) {
evt.preventDefault();
const { backend } = this.args;
const data = this.formatData(this.formData);
@ -246,12 +255,18 @@ export default class KeymgmtDistribute extends Component {
}
if (this.isNewKey) {
try {
await this.keyModel.save();
yield this.keyModel.save();
this.flashMessages.success(`Successfully created key ${this.keyModel.name}`);
} catch (e) {
this.flashMessages.danger(`Error creating new key ${this.keyModel.name}: ${e.errors}`);
return;
}
}
this.distributeKey(backend, data);
yield this.distributeKey(backend, data);
// Reload key to get dist info
yield this.store.queryRecord(`keymgmt/key`, {
backend: this.args.backend,
id: this.keyModel.name,
});
}
}

View File

@ -56,14 +56,26 @@ export default class KeymgmtKeyEdit extends Component {
yield model.save();
this.router.transitionTo(SHOW_ROUTE, model.name);
} catch (error) {
this.flashMessages.danger(error.errors.join('. '));
let errorMessage = error;
if (error.errors) {
// if errors come directly from API they will be in this shape
errorMessage = error.errors.join('. ');
}
this.flashMessages.danger(errorMessage);
if (!error.errors) {
// If error was custom from save, only partial fail
// so it's safe to show the key
this.router.transitionTo(SHOW_ROUTE, model.name);
}
}
}
@action
async removeKey() {
@task
@waitFor
*removeKey() {
try {
await this.keyAdapter.removeFromProvider(this.args.model);
yield this.keyAdapter.removeFromProvider(this.args.model);
yield this.args.model.reload();
this.flashMessages.success('Key has been successfully removed from provider');
} catch (error) {
this.flashMessages.danger(error.errors?.join('. '));
@ -84,11 +96,13 @@ export default class KeymgmtKeyEdit extends Component {
});
}
@action
rotateKey(id) {
const backend = this.args.model.get('backend');
@task
@waitFor
*rotateKey() {
const id = this.args.model.name;
const backend = this.args.model.backend;
const adapter = this.keyAdapter;
adapter
yield adapter
.rotateKey(backend, id)
.then(() => {
this.flashMessages.success(`Success: ${id} connection was rotated`);

View File

@ -70,6 +70,7 @@ export default class KeymgmtProviderEdit extends Component {
event.preventDefault();
const { isValid, state } = await this.args.model.validate();
if (isValid) {
this.modelValidations = null;
this.saveTask.perform();
} else {
this.modelValidations = state;

View File

@ -21,7 +21,7 @@ export const KEYMGMT = {
value: 'keymgmt',
type: 'keymgmt',
glyph: 'key',
category: 'generic',
category: 'cloud',
requiredFeature: 'Key Management Secrets Engine',
};

View File

@ -45,7 +45,7 @@ export default class KeymgmtProviderModel extends Model {
label: 'Type',
subText: 'Choose the provider type.',
possibleValues: ['azurekeyvault', 'awskms', 'gcpckms'],
defaultValue: 'azurekeyvault',
noDefault: true,
})
provider;
@ -55,8 +55,6 @@ export default class KeymgmtProviderModel extends Model {
})
keyCollection;
@attr('date') created;
idPrefix = 'provider/';
type = 'provider';
@ -78,7 +76,7 @@ export default class KeymgmtProviderModel extends Model {
}[this.provider];
}
get showFields() {
const attrs = expandAttributeMeta(this, ['name', 'created', 'keyCollection']);
const attrs = expandAttributeMeta(this, ['name', 'keyCollection']);
attrs.splice(1, 0, { hasBlock: true, label: 'Type', value: this.typeName, icon: this.icon });
const l = this.keys.length;
const value = l
@ -90,13 +88,18 @@ export default class KeymgmtProviderModel extends Model {
return attrs;
}
get credentialProps() {
if (!this.provider) return [];
return CRED_PROPS[this.provider];
}
get credentialFields() {
const [creds, fields] = this.credentialProps.reduce(
([creds, fields], prop) => {
creds[prop] = null;
fields.push({ name: `credentials.${prop}`, type: 'string', options: { label: prop } });
let field = { name: `credentials.${prop}`, type: 'string', options: { label: prop } };
if (prop === 'service_account_file') {
field.options.subText = 'The path to a Google service account key file, not the file itself.';
}
fields.push(field);
return [creds, fields];
},
[{}, []]
@ -109,7 +112,10 @@ export default class KeymgmtProviderModel extends Model {
}
async fetchKeys(page) {
if (this.canListKeys) {
if (this.canListKeys === false) {
this.keys = [];
} else {
// try unless capabilities returns false
try {
this.keys = await this.store.lazyPaginatedQuery('keymgmt/key', {
backend: 'keymgmt',
@ -123,8 +129,6 @@ export default class KeymgmtProviderModel extends Model {
throw error;
}
}
} else {
this.keys = [];
}
}

View File

@ -1,6 +1,13 @@
<div class="field is-grouped">
<div class="control is-expanded">
{{#if @label}}
<label for={{@id}} class="is-label">{{@label}}</label>
{{/if}}
{{#if @subText}}
<p class="sub-text">{{@subText}}</p>
{{/if}}
<Input
id={{@id}}
class="input"
@type="text"
@value={{this.searchInput}}

View File

@ -1,5 +1,5 @@
{{#if @backend}}
<form {{on "submit" this.createDistribution}} class="form-section" data-test-keymgmt-distribution-form>
<form {{on "submit" (perform this.createDistribution)}} class="form-section" data-test-keymgmt-distribution-form>
{{#unless @key}}
<div class="field" data-test-keymgmt-dist-key>
<SearchSelect
@ -64,28 +64,21 @@
{{#unless @provider}}
<div class="field">
<label class="is-label" for="provider">Provider</label>
<p class="sub-text">Select a provider in Vault. If it doesnt exist yet, youll need to add it first.</p>
<div class="control is-expanded">
<div class="select is-fullwidth">
<select
name="provider"
id="provider"
{{on "change" this.handleProvider}}
class={{if this.validMatchError.provider "has-error-border"}}
<SearchSelect
@id="provider"
@models={{array "keymgmt/provider"}}
@onChange={{this.handleProvider}}
@passObject={{false}}
@inputValue={{this.formData.provider}}
@subText="Select a provider in Vault. If it doesnt exist yet, youll need to add it first."
@label=""
@subLabel="Provider"
@fallbackComponent="input-search"
@selectLimit="1"
@backend={{@backend}}
@disallowNewItems={{true}}
data-test-keymgmt-dist-provider
>
<option value="">
Select provider
</option>
{{#each @providers as |val|}}
<option selected={{eq @model.provider val}} value={{val}}>
{{val}}
</option>
{{/each}}
</select>
</div>
</div>
{{#if this.validMatchError.provider}}
<AlertInline @paddingTop={{true}} @sizeSmall={{true}} @type="danger" data-test-keymgmt-error="provider">
{{this.validMatchError.provider}}
@ -93,6 +86,7 @@
<DocLink class="doc-link-subtle" @path="/docs/secrets/key-management#compatibility">refer to this table</DocLink>.
</AlertInline>
{{/if}}
</SearchSelect>
</div>
{{/unless}}
@ -139,15 +133,22 @@
</div>
</fieldset>
{{#if this.formErrors}}
<AlertBanner @type="danger" @message={{this.formErrors}} data-test-keymgmt-distribute-error />
{{/if}}
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">
<button
type="submit"
disabled={{or this.validationErrorCount this.error}}
disabled={{or this.validationErrorCount this.createDistribution.isRunning}}
class="button is-primary"
data-test-secret-save={{true}}
>
{{#if this.createDistribution.isRunning}}
<span class="loader is-inline-block"></span>
{{else}}
{{if (or (not @key) this.isNewKey) "Add key" "Distribute key"}}
{{/if}}
</button>
</div>
<div class="control">

View File

@ -24,7 +24,7 @@
<div class="tabs-container box is-sideless is-fullwidth is-paddingless is-marginless" data-test-keymgmt-key-toolbar>
<nav class="tabs">
<ul>
<li class={{if (not-eq @tab "versions") "is-active"}}>
<li class={{if (not-eq @tab "versions") "active"}}>
<LinkTo
@route="vault.cluster.secrets.backend.show"
@model={{@model.id}}
@ -34,7 +34,7 @@
Details
</LinkTo>
</li>
<li class={{if (eq @tab "versions") "is-active"}}>
<li class={{if (eq @tab "versions") "active"}}>
<LinkTo
@route="vault.cluster.secrets.backend.show"
@model={{@model.id}}
@ -49,6 +49,16 @@
</div>
<Toolbar>
<ToolbarActions>
{{#unless @model.distribution}}
<button
type="button"
class="toolbar-link"
{{on "click" (fn (mut this.isDistributing) true)}}
data-test-keymgmt-key-distribute
>
Distribute key
</button>
{{/unless}}
{{#if @model.canDelete}}
<button
type="button"
@ -63,10 +73,11 @@
{{#if @model.provider}}
<ConfirmAction
@buttonClasses="toolbar-link"
@onConfirmAction={{this.removeKey}}
@onConfirmAction={{perform this.removeKey}}
@confirmTitle="Remove this key?"
@confirmMessage="This will remove all versions of the key from the KMS provider. The key will stay in Vault."
@confirmButtonText="Remove"
@isRunning={{this.removeKey.isRunning}}
data-test-keymgmt-key-remove
>
Remove key
@ -77,10 +88,11 @@
{{/if}}
<ConfirmAction
@buttonClasses="toolbar-link"
@onConfirmAction={{fn this.rotateKey @model.id}}
@onConfirmAction={{perform this.rotateKey}}
@confirmTitle="Rotate this key?"
@confirmMessage="After rotation, all key actions will default to using the newest version of the key."
@confirmButtonText="Rotate"
@isRunning={{this.rotateKey.isRunning}}
data-test-keymgmt-key-rotate
>
Rotate key
@ -156,7 +168,7 @@
{{/each}}
{{else}}
<div class="has-top-margin-xl has-bottom-margin-s">
<h2 class="title has-border-bottom-light is-5">Key Details</h2>
<h2 class="title is-5 has-border-bottom-light">Key Details</h2>
{{#each @model.showFields as |attr|}}
<InfoTableRow
@alwaysRender={{true}}
@ -168,23 +180,10 @@
{{/each}}
</div>
<div class="has-top-margin-xl has-bottom-margin-s">
<h2 class="title has-border-bottom-light is-5 {{unless @model.provider.canListKeys 'is-borderless is-marginless'}}">
<h2 class="title is-5 {{if @model.distribution 'has-border-bottom-light' 'is-borderless'}}">
Distribution Details
</h2>
{{#if (not @model.provider)}}
<EmptyState
@title="Key not distributed"
@message="When this key is distributed to a destination, those details will appear here."
data-test-keymgmt-dist-empty-state
>
{{#if @model.canListProviders}}
<button type="button" class="link" {{on "click" (fn (mut this.isDistributing) true)}}>
Distribute key
<Icon @name="chevron-right" />
</button>
{{/if}}
</EmptyState>
{{else if (not @model.provider.canListKeys)}}
{{#if @model.provider.permissionsError}}
<EmptyState
@title="You are not authorized"
@subTitle="Error 403"
@ -197,9 +196,22 @@
}}
@icon="minus-circle"
/>
{{else if (is-empty-value @model.provider)}}
<EmptyState
@title="Key not distributed"
@message="When this key is distributed to a destination, those details will appear here."
data-test-keymgmt-dist-empty-state
>
{{#if @model.canListProviders}}
<button type="button" class="link" {{on "click" (fn (mut this.isDistributing) true)}}>
Distribute key
<Icon @name="chevron-right" />
</button>
{{/if}}
</EmptyState>
{{else}}
<InfoTableRow @label="Distributed" @value={{@model.provider}}>
<LinkTo @route="vault.cluster.secrets.backend.show" @model={{concat "kms/" @model.provider}}>
<LinkTo @route="vault.cluster.secrets.backend.show" @model={{@model.provider}} @query={{hash itemType="provider"}}>
<Icon @name="check-circle-fill" class="has-text-success" />{{@model.provider}}
</LinkTo>
</InfoTableRow>
@ -222,7 +234,13 @@
<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."
@message={{concat
"You must be granted permissions to view distribution details for this key. Ask your administrator if you think you should have access to GET /"
@model.backend
"/key/"
@model.name
"/kms."
}}
@icon="minus-circle"
/>
{{/if}}

View File

@ -23,13 +23,13 @@
<div class="tabs-container box is-sideless is-fullwidth is-paddingless is-marginless">
<nav class="tabs">
<ul>
<li class={{unless this.viewingKeys "is-active"}} data-test-kms-provider-tab="details">
<li class={{unless this.viewingKeys "active"}} data-test-kms-provider-tab="details">
<LinkTo @route="vault.cluster.secrets.backend.show" @model={{@model.id}} @query={{hash tab=""}}>
Details
</LinkTo>
</li>
{{#if @model.canListKeys}}
<li class={{if this.viewingKeys "is-active"}} data-test-kms-provider-tab="keys">
<li class={{if this.viewingKeys "active"}} data-test-kms-provider-tab="keys">
<LinkTo @route="vault.cluster.secrets.backend.show" @model={{@model.id}} @query={{hash tab="keys"}}>
Keys
</LinkTo>
@ -98,8 +98,15 @@
Provider configuration
</h2>
</div>
{{/if}}
{{#if @model.provider}}
{{! Only show last field if provider selected }}
<FormField @attr={{attr}} @model={{@model}} @modelValidations={{this.modelValidations}} />
{{else}}
<EmptyState @title="No provider selected" @message="Select a provider in order to configure it." />
{{/if}}
{{else}}
<FormField @attr={{attr}} @model={{@model}} @modelValidations={{this.modelValidations}} />
{{/if}}
{{/each}}
{{/if}}
{{#unless this.isCreating}}

View File

@ -46,7 +46,6 @@
@groupName="mount-type"
@onRadioChange={{queue (action (mut this.mountModel.type)) (action "onTypeChange" "type")}}
@disabled={{if type.requiredFeature (not (has-feature type.requiredFeature)) false}}
{{! TODO: verify that keymgmt is in the ADP module }}
@tooltipMessage={{if
(or (eq type.type "transform") (eq type.type "kmip") (eq type.type "keymgmt"))
(concat

View File

@ -33,6 +33,7 @@ export default Component.extend({
cancelButtonText: 'Cancel',
horizontalPosition: 'auto-right',
verticalPosition: 'below',
isRunning: false,
disabled: false,
showConfirm: false,
onConfirmAction: null,

View File

@ -32,12 +32,16 @@
<div class="confirm-action-options">
<button
type="button"
disabled={{this.disabled}}
disabled={{or this.disabled this.isRunning}}
class="link is-destroy"
data-test-confirm-button="true"
{{action "onConfirm"}}
>
{{#if this.isRunning}}
<span class="loader is-inline-block"></span>
{{else}}
{{this.confirmButtonText}}
{{/if}}
</button>
<button type="button" class="link" data-test-confirm-cancel-button="true" {{action d.actions.close}}>
{{this.cancelButtonText}}

View File

@ -1,11 +1,13 @@
{{#if this.shouldUseFallback}}
{{component
this.fallbackComponent
label=this.label
label=(or this.label this.subLabel)
subText=this.subText
onChange=(action "onChange")
inputValue=this.inputValue
helpText=this.helpText
placeHolder=this.placeHolder
id=this.id
}}
{{else}}
<label class={{if this.labelClass this.labelClass "title is-4"}} data-test-field-label>

View File

@ -78,6 +78,17 @@ module('Integration | Component | keymgmt/distribute', function (hooks) {
}),
];
});
this.get('/v1/keymgmt/kms', (response) => {
return [
response,
{ 'Content-Type': 'application/json' },
JSON.stringify({
data: {
keys: ['provider-aws', 'provider-azure', 'provider-gcp'],
},
}),
];
});
});
});
@ -85,55 +96,58 @@ module('Integration | Component | keymgmt/distribute', function (hooks) {
this.server.shutdown();
});
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/provider combo selected', async function (assert) {
assert.expect(6);
await render(
hbs`<Keymgmt::Distribute @backend="keymgmt" @providers={{providers}} @onClose={{fn (mut this.onClose)}} />`
hbs`<Keymgmt::Distribute @backend="keymgmt" @key="example-1" @providers={{providers}} @onClose={{fn (mut this.onClose)}} />`
);
assert.dom(SELECTORS.operationsSection).hasAttribute('disabled');
// Select
await clickTrigger();
await settled();
assert.equal(ssComponent.options.length, 3, 'shows all key options');
assert.equal(ssComponent.options.length, 3, 'shows all provider options');
await typeInSearch('aws');
await ssComponent.selectOption();
await settled();
assert.dom(SELECTORS.operationsSection).hasAttribute('disabled');
await select(SELECTORS.providerInput, 'provider-aws');
await settled();
assert.dom(SELECTORS.operationsSection).doesNotHaveAttribute('disabled');
await select(SELECTORS.providerInput, 'provider-azure');
// Remove selection
await ssComponent.deleteButtons.objectAt(0).click();
// Select Azure
await clickTrigger();
await typeInSearch('azure');
await ssComponent.selectOption();
// await select(SELECTORS.providerInput, 'provider-azure');
assert.dom(SELECTORS.operationsSection).hasAttribute('disabled');
assert.dom(SELECTORS.inlineError).exists({ count: 1 }, 'only shows single error');
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) {
assert.expect(2);
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');
// Add new item on search-select
await clickTrigger();
await settled();
await typeInSearch('new-key');
await ssComponent.selectOption();
assert.dom(SELECTORS.keyTypeSection).exists('Key Type selector is shown');
});
test('it hides the provider field if passed from the parent', async function (assert) {
assert.expect(5);
await render(
hbs`<Keymgmt::Distribute @backend="keymgmt" @provider="provider-azure" @onClose={{fn (mut this.onClose)}} />`
);
assert.dom(SELECTORS.providerInput).doesNotExist('Provider input is hidden');
// Select existing key
await clickTrigger();
await settled();
await ssComponent.selectOption();
await settled();
assert.dom(SELECTORS.inlineError).exists({ count: 1 }, 'only shows single error');
assert.dom(SELECTORS.errorKey).exists('Shows error on key selector when key/provider mismatch');
// Remove selection
await ssComponent.deleteButtons.objectAt(0).click();
await settled();
// Select new key
await clickTrigger();
await settled();
await typeInSearch('new-key');
await ssComponent.selectOption();
await select(SELECTORS.keyTypeSection, 'ecdsa-p256');
@ -141,15 +155,16 @@ module('Integration | Component | keymgmt/distribute', function (hooks) {
assert.dom(SELECTORS.errorNewKey).exists('Shows error on key type');
});
test('it hides the key field if passed from the parent', async function (assert) {
assert.expect(4);
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.keySection).doesNotExist('Key input not shown');
await select(SELECTORS.providerInput, 'provider-azure');
await clickTrigger();
await typeInSearch('azure');
await ssComponent.selectOption();
assert.dom(SELECTORS.inlineError).exists({ count: 1 }, 'only shows single error');
assert.dom(SELECTORS.errorProvider).exists('Shows error due to key/provider mismatch');
await select(SELECTORS.providerInput, 'provider-aws');
assert.dom(SELECTORS.inlineError).doesNotExist('Error goes away when key/provider compatible');
});
});

View File

@ -29,11 +29,12 @@ module('Integration | Component | keymgmt/key-edit', function (hooks) {
this.tab = '';
});
// TODO: Add capabilities tests
test('it renders show view as default', async function (assert) {
assert.expect(8);
await render(hbs`<Keymgmt::KeyEdit @model={{model}} @tab={{tab}} /><div id="modal-wormhole" />`);
assert.dom('[data-test-secret-header]').hasText('Unicorns', 'Shows key name');
assert.dom('[data-test-keymgmt-key-toolbar]').exists('Subnav toolbar exists');
// TODO: Add capabilities tests
assert.dom('[data-test-tab="Details"]').exists('Details tab exists');
assert.dom('[data-test-tab="Versions"]').exists('Versions tab exists');
assert.dom('[data-test-keymgmt-key-destroy]').isDisabled('Destroy button is disabled');
@ -47,6 +48,7 @@ module('Integration | Component | keymgmt/key-edit', function (hooks) {
});
test('it renders the correct elements on edit view', async function (assert) {
assert.expect(4);
let model = EmberObject.create({
name: 'Unicorns',
id: 'Unicorns',
@ -62,6 +64,7 @@ module('Integration | Component | keymgmt/key-edit', function (hooks) {
});
test('it renders the correct elements on create view', async function (assert) {
assert.expect(4);
let model = EmberObject.create({});
this.set('mode', 'create');
this.set('model', model);

View File

@ -4,7 +4,6 @@ import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { click, triggerEvent, settled, fillIn } from '@ember/test-helpers';
import { format } from 'date-fns';
const ts = 'data-test-kms-provider';
const root = {
@ -28,12 +27,10 @@ module('Integration | Component | keymgmt/provider-edit', function (hooks) {
name: 'foo-bar',
provider: 'azurekeyvault',
keyCollection: 'keyvault-1',
created: new Date(),
},
},
});
this.model = this.store.peekRecord('keymgmt/provider', 'foo-bar');
this.created = format(this.model.created, 'MMM d yyyy, h:mm:ss aaa');
this.root = root;
this.owner.lookup('service:router').reopen({
currentURL: '/ui/vault/secrets/keymgmt/show/foo-bar',
@ -45,7 +42,7 @@ module('Integration | Component | keymgmt/provider-edit', function (hooks) {
});
test('it should render show view', async function (assert) {
assert.expect(13);
assert.expect(12);
// override capability getters
Object.defineProperties(this.model, {
@ -86,15 +83,14 @@ module('Integration | Component | keymgmt/provider-edit', function (hooks) {
/>`);
assert.dom(`[${ts}-header]`).hasText('Provider foo-bar', 'Page header renders');
assert.dom(`[${ts}-tab="details"]`).hasClass('is-active', 'Details tab is active');
assert.dom(`[${ts}-tab="details"]`).hasClass('active', 'Details tab is active');
const infoRows = this.element.querySelectorAll('[data-test-component="info-table-row"]');
assert.dom(infoRows[0]).hasText('Provider name foo-bar', 'Provider name field renders');
assert.dom(infoRows[1]).hasText('Type Azure Key Vault', 'Type field renders');
assert.dom('svg', infoRows[1]).hasAttribute('data-test-icon', 'azure-color', 'Icon renders for type');
assert.dom(infoRows[2]).hasText(`Created ${this.created}`, 'Created field renders');
assert.dom(infoRows[3]).hasText('Key Vault instance name keyvault-1', 'Key collection field renders');
assert.dom(infoRows[4]).hasText('Keys 2 keys', 'Keys field renders');
assert.dom(infoRows[2]).hasText('Key Vault instance name keyvault-1', 'Key collection field renders');
assert.dom(infoRows[3]).hasText('Keys 2 keys', 'Keys field renders');
await changeTab('keys');
assert.dom(`[${ts}-details-actions]`).doesNotExist('Toolbar is hidden on keys tab');
@ -115,11 +111,12 @@ module('Integration | Component | keymgmt/provider-edit', function (hooks) {
});
test('it should render create view', async function (assert) {
assert.expect(10);
assert.expect(14);
this.server.put('/keymgmt/kms/foo', (schema, req) => {
const params = {
name: 'foo',
backend: 'keymgmt',
provider: 'gcpckms',
key_collection: 'keyvault-1',
credentials: {
@ -136,7 +133,7 @@ module('Integration | Component | keymgmt/provider-edit', function (hooks) {
assert.deepEqual(itemType, 'provider', 'Correct query params sent in transitionTo on save');
},
});
this.model = this.store.createRecord('keymgmt/provider');
this.model = this.store.createRecord('keymgmt/provider', { backend: 'keymgmt' });
await render(hbs`
<Keymgmt::ProviderEdit
@ -150,21 +147,20 @@ module('Integration | Component | keymgmt/provider-edit', function (hooks) {
assert.dom(`[${ts}-creds-title]`).doesNotExist('New credentials header hidden in create mode');
await click(`[${ts}-submit]`);
assert
.dom('[data-test-inline-error-message]')
.exists({ count: 5 }, 'Required fields are shown on validation');
assert.dom('[data-test-inline-error-message]').exists('Validation error messages shown');
await fillIn('[data-test-input="provider"]', 'azurekeyvault');
['client_id', 'client_secret', 'tenant_id'].forEach((prop) => {
assert.dom(`[data-test-input="credentials.${prop}"]`).exists(`Azure ${prop} field renders`);
assert.dom(`[data-test-input="credentials.${prop}"]`).exists(`Azure - ${prop} field renders`);
});
await fillIn('[data-test-input="provider"]', 'awskms');
['access_key', 'secret_key'].forEach((prop) => {
assert.dom(`[data-test-input="credentials.${prop}"]`).exists(`AWS ${prop} field renders`);
assert.dom(`[data-test-input="credentials.${prop}"]`).exists(`AWS - ${prop} field renders`);
});
await fillIn('[data-test-input="provider"]', 'gcpckms');
assert.dom(`[data-test-input="credentials.service_account_file"]`).exists(`GCP cred field renders`);
assert.dom(`[data-test-input="credentials.service_account_file"]`).exists(`GCP - cred field renders`);
await fillIn('[data-test-input="name"]', 'foo');
await fillIn('[data-test-input="keyCollection"]', 'keyvault-1');
@ -178,6 +174,7 @@ module('Integration | Component | keymgmt/provider-edit', function (hooks) {
this.server.put('/keymgmt/kms/foo', (schema, req) => {
const params = {
name: 'foo-bar',
backend: 'keymgmt',
provider: 'azurekeyvault',
key_collection: 'keyvault-1',
credentials: {