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 ApplicationAdapter from '../application';
|
||||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||||
|
import ControlGroupError from '../../lib/control-group-error';
|
||||||
|
|
||||||
function pickKeys(obj, picklist) {
|
function pickKeys(obj, picklist) {
|
||||||
const data = {};
|
const data = {};
|
||||||
|
@ -13,6 +14,21 @@ function pickKeys(obj, picklist) {
|
||||||
export default class KeymgmtKeyAdapter extends ApplicationAdapter {
|
export default class KeymgmtKeyAdapter extends ApplicationAdapter {
|
||||||
namespace = 'v1';
|
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) {
|
url(backend, id, type) {
|
||||||
const url = `${this.buildURL()}/${backend}/key`;
|
const url = `${this.buildURL()}/${backend}/key`;
|
||||||
if (id) {
|
if (id) {
|
||||||
|
@ -26,12 +42,6 @@ export default class KeymgmtKeyAdapter extends ApplicationAdapter {
|
||||||
return url;
|
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) {
|
_updateKey(backend, name, serialized) {
|
||||||
// Only these two attributes are allowed to be updated
|
// Only these two attributes are allowed to be updated
|
||||||
let data = pickKeys(serialized, ['deletion_allowed', 'min_enabled_version']);
|
let data = pickKeys(serialized, ['deletion_allowed', 'min_enabled_version']);
|
||||||
|
@ -53,8 +63,7 @@ export default class KeymgmtKeyAdapter extends ApplicationAdapter {
|
||||||
if (snapshot.attr('deletionAllowed')) {
|
if (snapshot.attr('deletionAllowed')) {
|
||||||
try {
|
try {
|
||||||
await this._updateKey(backend, name, data);
|
await this._updateKey(backend, name, data);
|
||||||
} catch (e) {
|
} catch {
|
||||||
// TODO: Test how this works with UI
|
|
||||||
throw new Error(`Key ${name} was created, but not all settings were saved`);
|
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) {
|
} else if (e.httpStatus === 403) {
|
||||||
return { permissionsError: true };
|
return { permissionsError: true };
|
||||||
}
|
}
|
||||||
// TODO: handle control group
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,8 +117,10 @@ export default class KeymgmtKeyAdapter extends ApplicationAdapter {
|
||||||
purposeArray: res.data.purpose.split(','),
|
purposeArray: res.data.purpose.split(','),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((e) => {
|
||||||
// TODO: handle control group
|
if (e instanceof ControlGroupError) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -123,7 +133,7 @@ export default class KeymgmtKeyAdapter extends ApplicationAdapter {
|
||||||
let provider, distribution;
|
let provider, distribution;
|
||||||
if (!recordOnly) {
|
if (!recordOnly) {
|
||||||
provider = await this.getProvider(backend, id);
|
provider = await this.getProvider(backend, id);
|
||||||
if (provider) {
|
if (provider && !provider.permissionsError) {
|
||||||
distribution = await this.getDistribution(backend, provider, id);
|
distribution = await this.getDistribution(backend, provider, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -145,13 +155,15 @@ export default class KeymgmtKeyAdapter extends ApplicationAdapter {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
rotateKey(backend, id) {
|
async rotateKey(backend, id) {
|
||||||
// TODO: re-fetch record data after
|
let keyModel = this.store.peekRecord('keymgmt/key', id);
|
||||||
return this.ajax(this.url(backend, id, 'ROTATE'), 'PUT');
|
const result = await this.ajax(this.url(backend, id, 'ROTATE'), 'PUT');
|
||||||
|
await keyModel.reload();
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
removeFromProvider(model) {
|
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(() => {
|
return this.ajax(url, 'DELETE').then(() => {
|
||||||
model.provider = null;
|
model.provider = null;
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,6 +3,8 @@ import { action } from '@ember/object';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import { tracked } from '@glimmer/tracking';
|
import { tracked } from '@glimmer/tracking';
|
||||||
import { KEY_TYPES } from '../../models/keymgmt/key';
|
import { KEY_TYPES } from '../../models/keymgmt/key';
|
||||||
|
import { task } from 'ember-concurrency';
|
||||||
|
import { waitFor } from '@ember/test-waiters';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module KeymgmtDistribute
|
* @module KeymgmtDistribute
|
||||||
|
@ -39,6 +41,7 @@ export default class KeymgmtDistribute extends Component {
|
||||||
@tracked isNewKey = false;
|
@tracked isNewKey = false;
|
||||||
@tracked providerType;
|
@tracked providerType;
|
||||||
@tracked formData;
|
@tracked formData;
|
||||||
|
@tracked formErrors;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(...arguments);
|
super(...arguments);
|
||||||
|
@ -196,15 +199,20 @@ export default class KeymgmtDistribute extends Component {
|
||||||
this.args.onClose();
|
this.args.onClose();
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
this.flashMessages.danger(`Error distributing key: ${e.errors}`);
|
this.formErrors = `${e.errors}`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
handleProvider(evt) {
|
handleProvider(selection) {
|
||||||
this.formData.provider = evt.target.value;
|
let providerName = selection[0];
|
||||||
if (evt.target.value) {
|
if (typeof selection === 'string') {
|
||||||
this.getProviderType(evt.target.value);
|
// Handles case if no list permissions and fallback component is used
|
||||||
|
providerName = selection;
|
||||||
|
}
|
||||||
|
this.formData.provider = providerName;
|
||||||
|
if (providerName) {
|
||||||
|
this.getProviderType(providerName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@action
|
@action
|
||||||
|
@ -235,8 +243,9 @@ export default class KeymgmtDistribute extends Component {
|
||||||
return this.getKeyInfo(selectedKey.id, selectedKey.isNew);
|
return this.getKeyInfo(selectedKey.id, selectedKey.isNew);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@task
|
||||||
async createDistribution(evt) {
|
@waitFor
|
||||||
|
*createDistribution(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const { backend } = this.args;
|
const { backend } = this.args;
|
||||||
const data = this.formatData(this.formData);
|
const data = this.formatData(this.formData);
|
||||||
|
@ -246,12 +255,18 @@ export default class KeymgmtDistribute extends Component {
|
||||||
}
|
}
|
||||||
if (this.isNewKey) {
|
if (this.isNewKey) {
|
||||||
try {
|
try {
|
||||||
await this.keyModel.save();
|
yield this.keyModel.save();
|
||||||
this.flashMessages.success(`Successfully created key ${this.keyModel.name}`);
|
this.flashMessages.success(`Successfully created key ${this.keyModel.name}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.flashMessages.danger(`Error creating new key ${this.keyModel.name}: ${e.errors}`);
|
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();
|
yield model.save();
|
||||||
this.router.transitionTo(SHOW_ROUTE, model.name);
|
this.router.transitionTo(SHOW_ROUTE, model.name);
|
||||||
} catch (error) {
|
} 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
|
@task
|
||||||
async removeKey() {
|
@waitFor
|
||||||
|
*removeKey() {
|
||||||
try {
|
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');
|
this.flashMessages.success('Key has been successfully removed from provider');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.flashMessages.danger(error.errors?.join('. '));
|
this.flashMessages.danger(error.errors?.join('. '));
|
||||||
|
@ -84,11 +96,13 @@ export default class KeymgmtKeyEdit extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@task
|
||||||
rotateKey(id) {
|
@waitFor
|
||||||
const backend = this.args.model.get('backend');
|
*rotateKey() {
|
||||||
|
const id = this.args.model.name;
|
||||||
|
const backend = this.args.model.backend;
|
||||||
const adapter = this.keyAdapter;
|
const adapter = this.keyAdapter;
|
||||||
adapter
|
yield adapter
|
||||||
.rotateKey(backend, id)
|
.rotateKey(backend, id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.flashMessages.success(`Success: ${id} connection was rotated`);
|
this.flashMessages.success(`Success: ${id} connection was rotated`);
|
||||||
|
|
|
@ -70,6 +70,7 @@ export default class KeymgmtProviderEdit extends Component {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const { isValid, state } = await this.args.model.validate();
|
const { isValid, state } = await this.args.model.validate();
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
|
this.modelValidations = null;
|
||||||
this.saveTask.perform();
|
this.saveTask.perform();
|
||||||
} else {
|
} else {
|
||||||
this.modelValidations = state;
|
this.modelValidations = state;
|
||||||
|
|
|
@ -21,7 +21,7 @@ export const KEYMGMT = {
|
||||||
value: 'keymgmt',
|
value: 'keymgmt',
|
||||||
type: 'keymgmt',
|
type: 'keymgmt',
|
||||||
glyph: 'key',
|
glyph: 'key',
|
||||||
category: 'generic',
|
category: 'cloud',
|
||||||
requiredFeature: 'Key Management Secrets Engine',
|
requiredFeature: 'Key Management Secrets Engine',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,7 @@ export default class KeymgmtProviderModel extends Model {
|
||||||
label: 'Type',
|
label: 'Type',
|
||||||
subText: 'Choose the provider type.',
|
subText: 'Choose the provider type.',
|
||||||
possibleValues: ['azurekeyvault', 'awskms', 'gcpckms'],
|
possibleValues: ['azurekeyvault', 'awskms', 'gcpckms'],
|
||||||
defaultValue: 'azurekeyvault',
|
noDefault: true,
|
||||||
})
|
})
|
||||||
provider;
|
provider;
|
||||||
|
|
||||||
|
@ -55,8 +55,6 @@ export default class KeymgmtProviderModel extends Model {
|
||||||
})
|
})
|
||||||
keyCollection;
|
keyCollection;
|
||||||
|
|
||||||
@attr('date') created;
|
|
||||||
|
|
||||||
idPrefix = 'provider/';
|
idPrefix = 'provider/';
|
||||||
type = 'provider';
|
type = 'provider';
|
||||||
|
|
||||||
|
@ -78,7 +76,7 @@ export default class KeymgmtProviderModel extends Model {
|
||||||
}[this.provider];
|
}[this.provider];
|
||||||
}
|
}
|
||||||
get showFields() {
|
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 });
|
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
|
const value = l
|
||||||
|
@ -90,13 +88,18 @@ export default class KeymgmtProviderModel extends Model {
|
||||||
return attrs;
|
return attrs;
|
||||||
}
|
}
|
||||||
get credentialProps() {
|
get credentialProps() {
|
||||||
|
if (!this.provider) return [];
|
||||||
return CRED_PROPS[this.provider];
|
return CRED_PROPS[this.provider];
|
||||||
}
|
}
|
||||||
get credentialFields() {
|
get credentialFields() {
|
||||||
const [creds, fields] = this.credentialProps.reduce(
|
const [creds, fields] = this.credentialProps.reduce(
|
||||||
([creds, fields], prop) => {
|
([creds, fields], prop) => {
|
||||||
creds[prop] = null;
|
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];
|
return [creds, fields];
|
||||||
},
|
},
|
||||||
[{}, []]
|
[{}, []]
|
||||||
|
@ -109,7 +112,10 @@ export default class KeymgmtProviderModel extends Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchKeys(page) {
|
async fetchKeys(page) {
|
||||||
if (this.canListKeys) {
|
if (this.canListKeys === false) {
|
||||||
|
this.keys = [];
|
||||||
|
} else {
|
||||||
|
// try unless capabilities returns false
|
||||||
try {
|
try {
|
||||||
this.keys = await this.store.lazyPaginatedQuery('keymgmt/key', {
|
this.keys = await this.store.lazyPaginatedQuery('keymgmt/key', {
|
||||||
backend: 'keymgmt',
|
backend: 'keymgmt',
|
||||||
|
@ -123,8 +129,6 @@ export default class KeymgmtProviderModel extends Model {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
this.keys = [];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
<div class="control is-expanded">
|
<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
|
<Input
|
||||||
|
id={{@id}}
|
||||||
class="input"
|
class="input"
|
||||||
@type="text"
|
@type="text"
|
||||||
@value={{this.searchInput}}
|
@value={{this.searchInput}}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{{#if @backend}}
|
{{#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}}
|
{{#unless @key}}
|
||||||
<div class="field" data-test-keymgmt-dist-key>
|
<div class="field" data-test-keymgmt-dist-key>
|
||||||
<SearchSelect
|
<SearchSelect
|
||||||
|
@ -64,28 +64,21 @@
|
||||||
|
|
||||||
{{#unless @provider}}
|
{{#unless @provider}}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="is-label" for="provider">Provider</label>
|
<SearchSelect
|
||||||
<p class="sub-text">Select a provider in Vault. If it doesn’t exist yet, you’ll need to add it first.</p>
|
@id="provider"
|
||||||
<div class="control is-expanded">
|
@models={{array "keymgmt/provider"}}
|
||||||
<div class="select is-fullwidth">
|
@onChange={{this.handleProvider}}
|
||||||
<select
|
@passObject={{false}}
|
||||||
name="provider"
|
@inputValue={{this.formData.provider}}
|
||||||
id="provider"
|
@subText="Select a provider in Vault. If it doesn’t exist yet, you’ll need to add it first."
|
||||||
{{on "change" this.handleProvider}}
|
@label=""
|
||||||
class={{if this.validMatchError.provider "has-error-border"}}
|
@subLabel="Provider"
|
||||||
|
@fallbackComponent="input-search"
|
||||||
|
@selectLimit="1"
|
||||||
|
@backend={{@backend}}
|
||||||
|
@disallowNewItems={{true}}
|
||||||
data-test-keymgmt-dist-provider
|
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}}
|
{{#if this.validMatchError.provider}}
|
||||||
<AlertInline @paddingTop={{true}} @sizeSmall={{true}} @type="danger" data-test-keymgmt-error="provider">
|
<AlertInline @paddingTop={{true}} @sizeSmall={{true}} @type="danger" data-test-keymgmt-error="provider">
|
||||||
{{this.validMatchError.provider}}
|
{{this.validMatchError.provider}}
|
||||||
|
@ -93,6 +86,7 @@
|
||||||
<DocLink class="doc-link-subtle" @path="/docs/secrets/key-management#compatibility">refer to this table</DocLink>.
|
<DocLink class="doc-link-subtle" @path="/docs/secrets/key-management#compatibility">refer to this table</DocLink>.
|
||||||
</AlertInline>
|
</AlertInline>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
</SearchSelect>
|
||||||
</div>
|
</div>
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
|
|
||||||
|
@ -139,15 +133,22 @@
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</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="field is-grouped box is-fullwidth is-bottomless">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={{or this.validationErrorCount this.error}}
|
disabled={{or this.validationErrorCount this.createDistribution.isRunning}}
|
||||||
class="button is-primary"
|
class="button is-primary"
|
||||||
data-test-secret-save={{true}}
|
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 (or (not @key) this.isNewKey) "Add key" "Distribute key"}}
|
||||||
|
{{/if}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="control">
|
<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>
|
<div class="tabs-container box is-sideless is-fullwidth is-paddingless is-marginless" data-test-keymgmt-key-toolbar>
|
||||||
<nav class="tabs">
|
<nav class="tabs">
|
||||||
<ul>
|
<ul>
|
||||||
<li class={{if (not-eq @tab "versions") "is-active"}}>
|
<li class={{if (not-eq @tab "versions") "active"}}>
|
||||||
<LinkTo
|
<LinkTo
|
||||||
@route="vault.cluster.secrets.backend.show"
|
@route="vault.cluster.secrets.backend.show"
|
||||||
@model={{@model.id}}
|
@model={{@model.id}}
|
||||||
|
@ -34,7 +34,7 @@
|
||||||
Details
|
Details
|
||||||
</LinkTo>
|
</LinkTo>
|
||||||
</li>
|
</li>
|
||||||
<li class={{if (eq @tab "versions") "is-active"}}>
|
<li class={{if (eq @tab "versions") "active"}}>
|
||||||
<LinkTo
|
<LinkTo
|
||||||
@route="vault.cluster.secrets.backend.show"
|
@route="vault.cluster.secrets.backend.show"
|
||||||
@model={{@model.id}}
|
@model={{@model.id}}
|
||||||
|
@ -49,6 +49,16 @@
|
||||||
</div>
|
</div>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<ToolbarActions>
|
<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}}
|
{{#if @model.canDelete}}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -63,10 +73,11 @@
|
||||||
{{#if @model.provider}}
|
{{#if @model.provider}}
|
||||||
<ConfirmAction
|
<ConfirmAction
|
||||||
@buttonClasses="toolbar-link"
|
@buttonClasses="toolbar-link"
|
||||||
@onConfirmAction={{this.removeKey}}
|
@onConfirmAction={{perform this.removeKey}}
|
||||||
@confirmTitle="Remove this key?"
|
@confirmTitle="Remove this key?"
|
||||||
@confirmMessage="This will remove all versions of the key from the KMS provider. The key will stay in Vault."
|
@confirmMessage="This will remove all versions of the key from the KMS provider. The key will stay in Vault."
|
||||||
@confirmButtonText="Remove"
|
@confirmButtonText="Remove"
|
||||||
|
@isRunning={{this.removeKey.isRunning}}
|
||||||
data-test-keymgmt-key-remove
|
data-test-keymgmt-key-remove
|
||||||
>
|
>
|
||||||
Remove key
|
Remove key
|
||||||
|
@ -77,10 +88,11 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<ConfirmAction
|
<ConfirmAction
|
||||||
@buttonClasses="toolbar-link"
|
@buttonClasses="toolbar-link"
|
||||||
@onConfirmAction={{fn this.rotateKey @model.id}}
|
@onConfirmAction={{perform this.rotateKey}}
|
||||||
@confirmTitle="Rotate this key?"
|
@confirmTitle="Rotate this key?"
|
||||||
@confirmMessage="After rotation, all key actions will default to using the newest version of the key."
|
@confirmMessage="After rotation, all key actions will default to using the newest version of the key."
|
||||||
@confirmButtonText="Rotate"
|
@confirmButtonText="Rotate"
|
||||||
|
@isRunning={{this.rotateKey.isRunning}}
|
||||||
data-test-keymgmt-key-rotate
|
data-test-keymgmt-key-rotate
|
||||||
>
|
>
|
||||||
Rotate key
|
Rotate key
|
||||||
|
@ -156,7 +168,7 @@
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{else}}
|
{{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">Key Details</h2>
|
<h2 class="title is-5 has-border-bottom-light">Key Details</h2>
|
||||||
{{#each @model.showFields as |attr|}}
|
{{#each @model.showFields as |attr|}}
|
||||||
<InfoTableRow
|
<InfoTableRow
|
||||||
@alwaysRender={{true}}
|
@alwaysRender={{true}}
|
||||||
|
@ -168,23 +180,10 @@
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
<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 {{unless @model.provider.canListKeys 'is-borderless is-marginless'}}">
|
<h2 class="title is-5 {{if @model.distribution 'has-border-bottom-light' 'is-borderless'}}">
|
||||||
Distribution Details
|
Distribution Details
|
||||||
</h2>
|
</h2>
|
||||||
{{#if (not @model.provider)}}
|
{{#if @model.provider.permissionsError}}
|
||||||
<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)}}
|
|
||||||
<EmptyState
|
<EmptyState
|
||||||
@title="You are not authorized"
|
@title="You are not authorized"
|
||||||
@subTitle="Error 403"
|
@subTitle="Error 403"
|
||||||
|
@ -197,9 +196,22 @@
|
||||||
}}
|
}}
|
||||||
@icon="minus-circle"
|
@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}}
|
{{else}}
|
||||||
<InfoTableRow @label="Distributed" @value={{@model.provider}}>
|
<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}}
|
<Icon @name="check-circle-fill" class="has-text-success" />{{@model.provider}}
|
||||||
</LinkTo>
|
</LinkTo>
|
||||||
</InfoTableRow>
|
</InfoTableRow>
|
||||||
|
@ -222,7 +234,13 @@
|
||||||
<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 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"
|
@icon="minus-circle"
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -23,13 +23,13 @@
|
||||||
<div class="tabs-container box is-sideless is-fullwidth is-paddingless is-marginless">
|
<div class="tabs-container box is-sideless is-fullwidth is-paddingless is-marginless">
|
||||||
<nav class="tabs">
|
<nav class="tabs">
|
||||||
<ul>
|
<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=""}}>
|
<LinkTo @route="vault.cluster.secrets.backend.show" @model={{@model.id}} @query={{hash tab=""}}>
|
||||||
Details
|
Details
|
||||||
</LinkTo>
|
</LinkTo>
|
||||||
</li>
|
</li>
|
||||||
{{#if @model.canListKeys}}
|
{{#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"}}>
|
<LinkTo @route="vault.cluster.secrets.backend.show" @model={{@model.id}} @query={{hash tab="keys"}}>
|
||||||
Keys
|
Keys
|
||||||
</LinkTo>
|
</LinkTo>
|
||||||
|
@ -98,8 +98,15 @@
|
||||||
Provider configuration
|
Provider configuration
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{#if @model.provider}}
|
||||||
|
{{! Only show last field if provider selected }}
|
||||||
<FormField @attr={{attr}} @model={{@model}} @modelValidations={{this.modelValidations}} />
|
<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}}
|
{{/each}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#unless this.isCreating}}
|
{{#unless this.isCreating}}
|
||||||
|
|
|
@ -46,7 +46,6 @@
|
||||||
@groupName="mount-type"
|
@groupName="mount-type"
|
||||||
@onRadioChange={{queue (action (mut this.mountModel.type)) (action "onTypeChange" "type")}}
|
@onRadioChange={{queue (action (mut this.mountModel.type)) (action "onTypeChange" "type")}}
|
||||||
@disabled={{if type.requiredFeature (not (has-feature type.requiredFeature)) false}}
|
@disabled={{if type.requiredFeature (not (has-feature type.requiredFeature)) false}}
|
||||||
{{! TODO: verify that keymgmt is in the ADP module }}
|
|
||||||
@tooltipMessage={{if
|
@tooltipMessage={{if
|
||||||
(or (eq type.type "transform") (eq type.type "kmip") (eq type.type "keymgmt"))
|
(or (eq type.type "transform") (eq type.type "kmip") (eq type.type "keymgmt"))
|
||||||
(concat
|
(concat
|
||||||
|
|
|
@ -33,6 +33,7 @@ export default Component.extend({
|
||||||
cancelButtonText: 'Cancel',
|
cancelButtonText: 'Cancel',
|
||||||
horizontalPosition: 'auto-right',
|
horizontalPosition: 'auto-right',
|
||||||
verticalPosition: 'below',
|
verticalPosition: 'below',
|
||||||
|
isRunning: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
showConfirm: false,
|
showConfirm: false,
|
||||||
onConfirmAction: null,
|
onConfirmAction: null,
|
||||||
|
|
|
@ -32,12 +32,16 @@
|
||||||
<div class="confirm-action-options">
|
<div class="confirm-action-options">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={{this.disabled}}
|
disabled={{or this.disabled this.isRunning}}
|
||||||
class="link is-destroy"
|
class="link is-destroy"
|
||||||
data-test-confirm-button="true"
|
data-test-confirm-button="true"
|
||||||
{{action "onConfirm"}}
|
{{action "onConfirm"}}
|
||||||
>
|
>
|
||||||
|
{{#if this.isRunning}}
|
||||||
|
<span class="loader is-inline-block"></span>
|
||||||
|
{{else}}
|
||||||
{{this.confirmButtonText}}
|
{{this.confirmButtonText}}
|
||||||
|
{{/if}}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="link" data-test-confirm-cancel-button="true" {{action d.actions.close}}>
|
<button type="button" class="link" data-test-confirm-cancel-button="true" {{action d.actions.close}}>
|
||||||
{{this.cancelButtonText}}
|
{{this.cancelButtonText}}
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
{{#if this.shouldUseFallback}}
|
{{#if this.shouldUseFallback}}
|
||||||
{{component
|
{{component
|
||||||
this.fallbackComponent
|
this.fallbackComponent
|
||||||
label=this.label
|
label=(or this.label this.subLabel)
|
||||||
|
subText=this.subText
|
||||||
onChange=(action "onChange")
|
onChange=(action "onChange")
|
||||||
inputValue=this.inputValue
|
inputValue=this.inputValue
|
||||||
helpText=this.helpText
|
helpText=this.helpText
|
||||||
placeHolder=this.placeHolder
|
placeHolder=this.placeHolder
|
||||||
|
id=this.id
|
||||||
}}
|
}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<label class={{if this.labelClass this.labelClass "title is-4"}} data-test-field-label>
|
<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();
|
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(
|
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');
|
assert.dom(SELECTORS.operationsSection).hasAttribute('disabled');
|
||||||
|
// Select
|
||||||
await clickTrigger();
|
await clickTrigger();
|
||||||
await settled();
|
assert.equal(ssComponent.options.length, 3, 'shows all provider options');
|
||||||
assert.equal(ssComponent.options.length, 3, 'shows all key options');
|
await typeInSearch('aws');
|
||||||
await ssComponent.selectOption();
|
await ssComponent.selectOption();
|
||||||
await settled();
|
await settled();
|
||||||
assert.dom(SELECTORS.operationsSection).hasAttribute('disabled');
|
|
||||||
await select(SELECTORS.providerInput, 'provider-aws');
|
|
||||||
await settled();
|
|
||||||
assert.dom(SELECTORS.operationsSection).doesNotHaveAttribute('disabled');
|
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.operationsSection).hasAttribute('disabled');
|
||||||
assert.dom(SELECTORS.inlineError).exists({ count: 1 }, 'only shows single error');
|
assert.dom(SELECTORS.inlineError).exists({ count: 1 }, 'only shows single error');
|
||||||
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) {
|
||||||
|
assert.expect(2);
|
||||||
await render(
|
await render(
|
||||||
hbs`<Keymgmt::Distribute @backend="keymgmt" @providers={{providers}} @onClose={{fn (mut this.onClose)}} />`
|
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();
|
||||||
await settled();
|
|
||||||
await typeInSearch('new-key');
|
await typeInSearch('new-key');
|
||||||
await ssComponent.selectOption();
|
await ssComponent.selectOption();
|
||||||
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) {
|
||||||
|
assert.expect(5);
|
||||||
await render(
|
await render(
|
||||||
hbs`<Keymgmt::Distribute @backend="keymgmt" @provider="provider-azure" @onClose={{fn (mut this.onClose)}} />`
|
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();
|
||||||
await settled();
|
|
||||||
await ssComponent.selectOption();
|
await ssComponent.selectOption();
|
||||||
await settled();
|
await settled();
|
||||||
assert.dom(SELECTORS.inlineError).exists({ count: 1 }, 'only shows single error');
|
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');
|
assert.dom(SELECTORS.errorKey).exists('Shows error on key selector when key/provider mismatch');
|
||||||
// Remove selection
|
// Remove selection
|
||||||
await ssComponent.deleteButtons.objectAt(0).click();
|
await ssComponent.deleteButtons.objectAt(0).click();
|
||||||
await settled();
|
|
||||||
// Select new key
|
// Select new key
|
||||||
await clickTrigger();
|
await clickTrigger();
|
||||||
await settled();
|
|
||||||
await typeInSearch('new-key');
|
await typeInSearch('new-key');
|
||||||
await ssComponent.selectOption();
|
await ssComponent.selectOption();
|
||||||
await select(SELECTORS.keyTypeSection, 'ecdsa-p256');
|
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');
|
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) {
|
||||||
|
assert.expect(4);
|
||||||
await render(
|
await render(
|
||||||
hbs`<Keymgmt::Distribute @backend="keymgmt" @providers={{providers}} @key="example-1" @onClose={{fn (mut this.onClose)}} />`
|
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 clickTrigger();
|
||||||
|
await typeInSearch('azure');
|
||||||
|
await ssComponent.selectOption();
|
||||||
assert.dom(SELECTORS.inlineError).exists({ count: 1 }, 'only shows single error');
|
assert.dom(SELECTORS.inlineError).exists({ count: 1 }, 'only shows single error');
|
||||||
assert.dom(SELECTORS.errorProvider).exists('Shows error due to key/provider mismatch');
|
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 = '';
|
this.tab = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: Add capabilities tests
|
||||||
test('it renders show view as default', async function (assert) {
|
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" />`);
|
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-secret-header]').hasText('Unicorns', 'Shows key name');
|
||||||
assert.dom('[data-test-keymgmt-key-toolbar]').exists('Subnav toolbar exists');
|
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="Details"]').exists('Details tab exists');
|
||||||
assert.dom('[data-test-tab="Versions"]').exists('Versions tab exists');
|
assert.dom('[data-test-tab="Versions"]').exists('Versions tab exists');
|
||||||
assert.dom('[data-test-keymgmt-key-destroy]').isDisabled('Destroy button is disabled');
|
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) {
|
test('it renders the correct elements on edit view', async function (assert) {
|
||||||
|
assert.expect(4);
|
||||||
let model = EmberObject.create({
|
let model = EmberObject.create({
|
||||||
name: 'Unicorns',
|
name: 'Unicorns',
|
||||||
id: '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) {
|
test('it renders the correct elements on create view', async function (assert) {
|
||||||
|
assert.expect(4);
|
||||||
let model = EmberObject.create({});
|
let model = EmberObject.create({});
|
||||||
this.set('mode', 'create');
|
this.set('mode', 'create');
|
||||||
this.set('model', model);
|
this.set('model', model);
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { render } from '@ember/test-helpers';
|
||||||
import { hbs } from 'ember-cli-htmlbars';
|
import { hbs } from 'ember-cli-htmlbars';
|
||||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||||
import { click, triggerEvent, settled, fillIn } from '@ember/test-helpers';
|
import { click, triggerEvent, settled, fillIn } from '@ember/test-helpers';
|
||||||
import { format } from 'date-fns';
|
|
||||||
|
|
||||||
const ts = 'data-test-kms-provider';
|
const ts = 'data-test-kms-provider';
|
||||||
const root = {
|
const root = {
|
||||||
|
@ -28,12 +27,10 @@ module('Integration | Component | keymgmt/provider-edit', function (hooks) {
|
||||||
name: 'foo-bar',
|
name: 'foo-bar',
|
||||||
provider: 'azurekeyvault',
|
provider: 'azurekeyvault',
|
||||||
keyCollection: 'keyvault-1',
|
keyCollection: 'keyvault-1',
|
||||||
created: new Date(),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.model = this.store.peekRecord('keymgmt/provider', 'foo-bar');
|
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.root = root;
|
||||||
this.owner.lookup('service:router').reopen({
|
this.owner.lookup('service:router').reopen({
|
||||||
currentURL: '/ui/vault/secrets/keymgmt/show/foo-bar',
|
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) {
|
test('it should render show view', async function (assert) {
|
||||||
assert.expect(13);
|
assert.expect(12);
|
||||||
|
|
||||||
// override capability getters
|
// override capability getters
|
||||||
Object.defineProperties(this.model, {
|
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}-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"]');
|
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[0]).hasText('Provider name foo-bar', 'Provider name field renders');
|
||||||
assert.dom(infoRows[1]).hasText('Type Azure Key Vault', 'Type 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('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[2]).hasText('Key Vault instance name keyvault-1', 'Key collection field renders');
|
||||||
assert.dom(infoRows[3]).hasText('Key Vault instance name keyvault-1', 'Key collection field renders');
|
assert.dom(infoRows[3]).hasText('Keys 2 keys', 'Keys field renders');
|
||||||
assert.dom(infoRows[4]).hasText('Keys 2 keys', 'Keys field renders');
|
|
||||||
|
|
||||||
await changeTab('keys');
|
await changeTab('keys');
|
||||||
assert.dom(`[${ts}-details-actions]`).doesNotExist('Toolbar is hidden on keys tab');
|
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) {
|
test('it should render create view', async function (assert) {
|
||||||
assert.expect(10);
|
assert.expect(14);
|
||||||
|
|
||||||
this.server.put('/keymgmt/kms/foo', (schema, req) => {
|
this.server.put('/keymgmt/kms/foo', (schema, req) => {
|
||||||
const params = {
|
const params = {
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
|
backend: 'keymgmt',
|
||||||
provider: 'gcpckms',
|
provider: 'gcpckms',
|
||||||
key_collection: 'keyvault-1',
|
key_collection: 'keyvault-1',
|
||||||
credentials: {
|
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');
|
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`
|
await render(hbs`
|
||||||
<Keymgmt::ProviderEdit
|
<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');
|
assert.dom(`[${ts}-creds-title]`).doesNotExist('New credentials header hidden in create mode');
|
||||||
|
|
||||||
await click(`[${ts}-submit]`);
|
await click(`[${ts}-submit]`);
|
||||||
assert
|
assert.dom('[data-test-inline-error-message]').exists('Validation error messages shown');
|
||||||
.dom('[data-test-inline-error-message]')
|
|
||||||
.exists({ count: 5 }, 'Required fields are shown on validation');
|
|
||||||
|
|
||||||
|
await fillIn('[data-test-input="provider"]', 'azurekeyvault');
|
||||||
['client_id', 'client_secret', 'tenant_id'].forEach((prop) => {
|
['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');
|
await fillIn('[data-test-input="provider"]', 'awskms');
|
||||||
['access_key', 'secret_key'].forEach((prop) => {
|
['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');
|
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="name"]', 'foo');
|
||||||
await fillIn('[data-test-input="keyCollection"]', 'keyvault-1');
|
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) => {
|
this.server.put('/keymgmt/kms/foo', (schema, req) => {
|
||||||
const params = {
|
const params = {
|
||||||
name: 'foo-bar',
|
name: 'foo-bar',
|
||||||
|
backend: 'keymgmt',
|
||||||
provider: 'azurekeyvault',
|
provider: 'azurekeyvault',
|
||||||
key_collection: 'keyvault-1',
|
key_collection: 'keyvault-1',
|
||||||
credentials: {
|
credentials: {
|
||||||
|
|
Loading…
Reference in New Issue