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}}
+
+ {{#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 @@