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:
parent
892d4d1e37
commit
81105e6209
|
@ -0,0 +1,3 @@
|
|||
```release-note:feature
|
||||
**KeyMgmt UI**: Add UI support for managing the Key Management Secrets Engine
|
||||
```
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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`);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -21,7 +21,7 @@ export const KEYMGMT = {
|
|||
value: 'keymgmt',
|
||||
type: 'keymgmt',
|
||||
glyph: 'key',
|
||||
category: 'generic',
|
||||
category: 'cloud',
|
||||
requiredFeature: 'Key Management Secrets Engine',
|
||||
};
|
||||
|
||||
|
|
|
@ -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 = [];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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,35 +64,29 @@
|
|||
|
||||
{{#unless @provider}}
|
||||
<div class="field">
|
||||
<label class="is-label" for="provider">Provider</label>
|
||||
<p class="sub-text">Select a provider in Vault. If it doesn’t exist yet, you’ll 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"}}
|
||||
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}}
|
||||
To check compatibility,
|
||||
<DocLink class="doc-link-subtle" @path="/docs/secrets/key-management#compatibility">refer to this table</DocLink>.
|
||||
</AlertInline>
|
||||
{{/if}}
|
||||
<SearchSelect
|
||||
@id="provider"
|
||||
@models={{array "keymgmt/provider"}}
|
||||
@onChange={{this.handleProvider}}
|
||||
@passObject={{false}}
|
||||
@inputValue={{this.formData.provider}}
|
||||
@subText="Select a provider in Vault. If it doesn’t exist yet, you’ll need to add it first."
|
||||
@label=""
|
||||
@subLabel="Provider"
|
||||
@fallbackComponent="input-search"
|
||||
@selectLimit="1"
|
||||
@backend={{@backend}}
|
||||
@disallowNewItems={{true}}
|
||||
data-test-keymgmt-dist-provider
|
||||
>
|
||||
{{#if this.validMatchError.provider}}
|
||||
<AlertInline @paddingTop={{true}} @sizeSmall={{true}} @type="danger" data-test-keymgmt-error="provider">
|
||||
{{this.validMatchError.provider}}
|
||||
To check compatibility,
|
||||
<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 (or (not @key) this.isNewKey) "Add key" "Distribute key"}}
|
||||
{{#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">
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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 @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}}
|
||||
<FormField @attr={{attr}} @model={{@model}} @modelValidations={{this.modelValidations}} />
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{#unless this.isCreating}}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -33,6 +33,7 @@ export default Component.extend({
|
|||
cancelButtonText: 'Cancel',
|
||||
horizontalPosition: 'auto-right',
|
||||
verticalPosition: 'below',
|
||||
isRunning: false,
|
||||
disabled: false,
|
||||
showConfirm: false,
|
||||
onConfirmAction: null,
|
||||
|
|
|
@ -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"}}
|
||||
>
|
||||
{{this.confirmButtonText}}
|
||||
{{#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}}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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: {
|
||||
|
|
Loading…
Reference in New Issue