diff --git a/changelog/15523.txt b/changelog/15523.txt new file mode 100644 index 000000000..867fe27ef --- /dev/null +++ b/changelog/15523.txt @@ -0,0 +1,3 @@ +```release-note:feature +**KeyMgmt UI**: Add UI support for managing the Key Management Secrets Engine +``` \ No newline at end of file diff --git a/ui/app/adapters/keymgmt/key.js b/ui/app/adapters/keymgmt/key.js index 8b3fcaad2..ddcb20361 100644 --- a/ui/app/adapters/keymgmt/key.js +++ b/ui/app/adapters/keymgmt/key.js @@ -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; }); diff --git a/ui/app/components/keymgmt/distribute.js b/ui/app/components/keymgmt/distribute.js index 1eaeaccf8..4216d0ffc 100644 --- a/ui/app/components/keymgmt/distribute.js +++ b/ui/app/components/keymgmt/distribute.js @@ -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, + }); } } diff --git a/ui/app/components/keymgmt/key-edit.js b/ui/app/components/keymgmt/key-edit.js index 2c46b20ae..50a1e2cd1 100644 --- a/ui/app/components/keymgmt/key-edit.js +++ b/ui/app/components/keymgmt/key-edit.js @@ -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`); diff --git a/ui/app/components/keymgmt/provider-edit.js b/ui/app/components/keymgmt/provider-edit.js index 35b6be8ae..a83c96c80 100644 --- a/ui/app/components/keymgmt/provider-edit.js +++ b/ui/app/components/keymgmt/provider-edit.js @@ -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; diff --git a/ui/app/helpers/mountable-secret-engines.js b/ui/app/helpers/mountable-secret-engines.js index 94b2a8b72..f05c1e47b 100644 --- a/ui/app/helpers/mountable-secret-engines.js +++ b/ui/app/helpers/mountable-secret-engines.js @@ -21,7 +21,7 @@ export const KEYMGMT = { value: 'keymgmt', type: 'keymgmt', glyph: 'key', - category: 'generic', + category: 'cloud', requiredFeature: 'Key Management Secrets Engine', }; diff --git a/ui/app/models/keymgmt/provider.js b/ui/app/models/keymgmt/provider.js index 58f26087e..f07faa371 100644 --- a/ui/app/models/keymgmt/provider.js +++ b/ui/app/models/keymgmt/provider.js @@ -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 = []; } } diff --git a/ui/app/templates/components/input-search.hbs b/ui/app/templates/components/input-search.hbs index e57916ed6..3caaac987 100644 --- a/ui/app/templates/components/input-search.hbs +++ b/ui/app/templates/components/input-search.hbs @@ -1,6 +1,13 @@
+ {{#if @label}} + + {{/if}} + {{#if @subText}} +

{{@subText}}

+ {{/if}} +
{{#unless @key}}
- -

Select a provider in Vault. If it doesn’t exist yet, you’ll need to add it first.

-
-
- -
-
- {{#if this.validMatchError.provider}} - - {{this.validMatchError.provider}} - To check compatibility, - refer to this table. - - {{/if}} + + {{#if this.validMatchError.provider}} + + {{this.validMatchError.provider}} + To check compatibility, + refer to this table. + + {{/if}} +
{{/unless}} @@ -139,15 +133,22 @@
+ {{#if this.formErrors}} + + {{/if}}
diff --git a/ui/app/templates/components/keymgmt/key-edit.hbs b/ui/app/templates/components/keymgmt/key-edit.hbs index e190b3ec1..eae44fc9d 100644 --- a/ui/app/templates/components/keymgmt/key-edit.hbs +++ b/ui/app/templates/components/keymgmt/key-edit.hbs @@ -24,7 +24,7 @@