1
0

remove keymgmt (ENT)

This commit is contained in:
Konstantin Demin 2024-07-03 14:12:54 +03:00
parent a0f77417d5
commit 0567588b44
28 changed files with 5 additions and 1932 deletions

View File

@ -75,7 +75,7 @@ vault secrets enable "transit"
# Enable enterprise features
if [[ -n "${VAULT_LICENSE:-}" ]]; then
vault secrets enable "keymgmt"
:
fi
# Output OpenAPI, optionally formatted

View File

@ -1,179 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationAdapter from '../application';
import { encodePath } from 'vault/utils/path-encoding-helpers';
import ControlGroupError from '../../lib/control-group-error';
import { inject as service } from '@ember/service';
function pickKeys(obj, picklist) {
const data = {};
Object.keys(obj).forEach((key) => {
if (picklist.indexOf(key) >= 0) {
data[key] = obj[key];
}
});
return data;
}
export default class KeymgmtKeyAdapter extends ApplicationAdapter {
@service store;
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) {
if (type === 'ROTATE') {
return url + '/' + encodePath(id) + '/rotate';
} else if (type === 'PROVIDERS') {
return url + '/' + encodePath(id) + '/kms';
}
return url + '/' + encodePath(id);
}
return url;
}
_updateKey(backend, name, serialized) {
// Only these two attributes are allowed to be updated
const data = pickKeys(serialized, ['deletion_allowed', 'min_enabled_version']);
return this.ajax(this.url(backend, name), 'PUT', { data });
}
_createKey(backend, name, serialized) {
// Only type is allowed on create
const data = pickKeys(serialized, ['type']);
return this.ajax(this.url(backend, name), 'POST', { data });
}
async createRecord(store, type, snapshot) {
const data = store.serializerFor(type.modelName).serialize(snapshot);
const name = snapshot.attr('name');
const backend = snapshot.attr('backend');
// Keys must be created and then updated
await this._createKey(backend, name, data);
if (snapshot.attr('deletionAllowed')) {
try {
await this._updateKey(backend, name, data);
} catch {
throw new Error(`Key ${name} was created, but not all settings were saved`);
}
}
return {
data: {
...data,
id: name,
backend,
},
};
}
updateRecord(store, type, snapshot) {
const data = store.serializerFor(type.modelName).serialize(snapshot);
const name = snapshot.attr('name');
const backend = snapshot.attr('backend');
return this._updateKey(backend, name, data);
}
distribute(backend, kms, key, data) {
return this.ajax(`${this.buildURL()}/${backend}/kms/${encodePath(kms)}/key/${encodePath(key)}`, 'PUT', {
data: { ...data },
});
}
async getProvider(backend, name) {
try {
const resp = await this.ajax(this.url(backend, name, 'PROVIDERS'), 'GET', {
data: {
list: true,
},
});
return resp.data.keys ? resp.data.keys[0] : null;
} catch (e) {
if (e.httpStatus === 404) {
// No results, not distributed yet
return null;
} else if (e.httpStatus === 403) {
return { permissionsError: true };
}
throw e;
}
}
getDistribution(backend, kms, key) {
const url = `${this.buildURL()}/${backend}/kms/${kms}/key/${key}`;
return this.ajax(url, 'GET')
.then((res) => {
return {
...res.data,
purposeArray: res.data.purpose.split(','),
};
})
.catch((e) => {
if (e instanceof ControlGroupError) {
throw e;
}
return null;
});
}
async queryRecord(store, type, query) {
const { id, backend, recordOnly = false } = query;
const keyData = await this.ajax(this.url(backend, id), 'GET');
keyData.data.id = id;
keyData.data.backend = backend;
let provider, distribution;
if (!recordOnly) {
provider = await this.getProvider(backend, id);
if (provider && !provider.permissionsError) {
distribution = await this.getDistribution(backend, provider, id);
}
}
return { ...keyData, provider, distribution };
}
async query(store, type, query) {
const { backend, provider } = query;
const providerAdapter = store.adapterFor('keymgmt/provider');
const url = provider ? providerAdapter.buildKeysURL(query) : this.url(backend);
return this.ajax(url, 'GET', {
data: {
list: true,
},
}).then((res) => {
res.backend = backend;
return res;
});
}
async rotateKey(backend, id) {
const 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}/key/${model.name}`;
return this.ajax(url, 'DELETE').then(() => {
model.provider = null;
});
}
}

View File

@ -1,70 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationAdapter from '../application';
import { all } from 'rsvp';
export default class KeymgmtKeyAdapter extends ApplicationAdapter {
namespace = 'v1';
listPayload = { data: { list: true } };
pathForType() {
// backend name prepended in buildURL method
return 'kms';
}
buildURL(modelName, id, snapshot, requestType, query) {
let url = super.buildURL(...arguments);
if (snapshot) {
url = url.replace('kms', `${snapshot.attr('backend')}/kms`);
} else if (query) {
url = url.replace('kms', `${query.backend}/kms`);
}
return url;
}
buildKeysURL(query) {
const url = this.buildURL('keymgmt/provider', null, null, 'query', query);
return `${url}/${query.provider}/key`;
}
async createRecord(store, { modelName }, snapshot) {
// create uses PUT instead of POST
const data = store.serializerFor(modelName).serialize(snapshot);
const url = this.buildURL(modelName, snapshot.attr('name'), snapshot, 'updateRecord');
return this.ajax(url, 'PUT', { data }).then(() => data);
}
findRecord(store, type, name) {
return super.findRecord(...arguments).then((resp) => {
resp.data = { ...resp.data, name };
return resp;
});
}
async query(store, type, query) {
const { backend } = query;
const url = this.buildURL(type.modelName, null, null, 'query', query);
return this.ajax(url, 'GET', this.listPayload).then(async (resp) => {
// additional data is needed to fullfil the list view requirements
// pull in full record for listed items
const records = await all(
resp.data.keys.map((name) => this.findRecord(store, type, name, this._mockSnapshot(query.backend)))
);
resp.data.keys = records.map((record) => record.data);
resp.backend = backend;
return resp;
});
}
async queryRecord(store, type, query) {
return this.findRecord(store, type, query.id, this._mockSnapshot(query.backend));
}
// when using find in query or queryRecord overrides snapshot is not available
// ultimately buildURL requires the snapshot to pull the backend name for the dynamic segment
// since we have the backend value from the query generate a mock snapshot
_mockSnapshot(backend) {
return {
attr(prop) {
return prop === 'backend' ? backend : null;
},
};
}
}

View File

@ -1,247 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
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
* KeymgmtDistribute components are used to provide a form to distribute Keymgmt keys to a provider.
*
* @example
* ```js
* <KeymgmtDistribute @backend="keymgmt" @key="my-key" @provider="my-kms" />
* ```
* @param {string} backend - name of backend, which will be the basis of other store queries
* @param {string} [key] - key is the name of the existing key which is being distributed. Will hide the key field in UI
* @param {string} [provider] - provider is the name of the existing provider which is being distributed to. Will hide the provider field in UI
*/
class DistributionData {
@tracked key;
@tracked provider;
@tracked operations;
@tracked protection;
}
const VALID_TYPES_BY_PROVIDER = {
};
export default class KeymgmtDistribute extends Component {
@service store;
@service flashMessages;
@service router;
@tracked keyModel;
@tracked isNewKey = false;
@tracked providerType;
@tracked formData;
@tracked formErrors;
constructor() {
super(...arguments);
this.formData = new DistributionData();
// Set initial values passed in
this.formData.key = this.args.key || '';
this.formData.provider = this.args.provider || '';
// Side effects to get types of key or provider passed in
if (this.args.provider) {
this.getProviderType(this.args.provider);
}
if (this.args.key) {
this.getKeyInfo(this.args.key);
}
this.formData.operations = [];
}
get keyTypes() {
return KEY_TYPES;
}
get validMatchError() {
if (!this.providerType || !this.keyModel?.type) {
return null;
}
const valid = VALID_TYPES_BY_PROVIDER[this.providerType]?.includes(this.keyModel.type);
if (valid) return null;
// default to showing error on provider unless @provider (field hidden)
if (this.args.provider) {
return {
key: `This key type is incompatible with the ${this.providerType} provider. To distribute to this provider, change the key type or choose another key.`,
};
}
const message = `This provider is incompatible with the ${this.keyModel.type} key type. Please choose another provider`;
return {
provider: this.args.key ? `${message}.` : `${message} or change the key type.`,
};
}
get operations() {
return ['encrypt', 'decrypt', 'sign', 'verify', 'wrap', 'unwrap'];
}
get disableOperations() {
return (
this.validMatchError ||
!this.formData.provider ||
!this.formData.key ||
(this.isNewKey && !this.keyModel.type)
);
}
async getKeyInfo(keyName, isNew = false) {
let key;
if (isNew) {
this.isNewKey = true;
key = this.store.createRecord(`keymgmt/key`, {
backend: this.args.backend,
id: keyName,
name: keyName,
});
} else {
key = await this.store
.queryRecord(`keymgmt/key`, {
backend: this.args.backend,
id: keyName,
recordOnly: true,
})
.catch(() => {
// Key type isn't essential for distributing, so if
// we can't read it for some reason swallow the error
// and allow the API to respond with any key/provider
// type matching errors
});
}
this.keyModel = key;
}
async getProviderType(id) {
if (!id) {
this.providerType = '';
return;
}
const provider = await this.store
.queryRecord('keymgmt/provider', {
backend: this.args.backend,
id,
})
.catch(() => {});
this.providerType = provider?.provider;
}
destroyKey() {
if (this.isNewKey) {
// Delete record from store if it was created here
this.keyModel.destroyRecord().finally(() => {
this.keyModel = null;
});
}
this.isNewKey = false;
this.keyModel = null;
}
/**
*
* @param {DistributionData} rawData
* @returns POJO formatted how the distribution endpoint needs
*/
formatData(rawData) {
const { key, provider, operations, protection } = rawData;
if (!key || !provider || !operations || operations.length === 0) return null;
return { key, provider, purpose: operations.join(','), protection };
}
distributeKey(backend, data) {
const adapter = this.store.adapterFor('keymgmt/key');
const { key, provider, purpose, protection } = data;
return adapter
.distribute(backend, provider, key, { purpose, protection })
.then(() => {
this.flashMessages.success(`Successfully distributed key ${key} to ${provider}`);
// update keys on provider model
this.store.clearDataset('keymgmt/key');
const providerModel = this.store.peekRecord('keymgmt/provider', provider);
providerModel.fetchKeys(providerModel.keys?.meta?.currentPage || 1);
this.args.onClose();
})
.catch((e) => {
this.formErrors = `${e.errors}`;
});
}
@action
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
handleKeyType(evt) {
this.keyModel.set('type', evt.target.value);
}
@action
handleOperation(evt) {
const ops = [...this.formData.operations];
if (evt.target.checked) {
ops.push(evt.target.id);
} else {
const idx = ops.indexOf(evt.target.id);
ops.splice(idx, 1);
}
this.formData.operations = ops;
}
@action
async handleKeySelect(selected) {
const selectedKey = selected[0] || null;
if (!selectedKey) {
this.formData.key = null;
return this.destroyKey();
}
this.formData.key = selectedKey.id;
return this.getKeyInfo(selectedKey.id, selectedKey.isNew);
}
@task
@waitFor
*createDistribution(evt) {
evt.preventDefault();
const { backend } = this.args;
const data = this.formatData(this.formData);
if (!data) {
this.flashMessages.danger(`Key, provider, and operations are all required`);
return;
}
if (this.isNewKey) {
try {
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;
}
}
yield this.distributeKey(backend, data);
// Reload key to get dist info
yield this.store.queryRecord(`keymgmt/key`, {
backend: this.args.backend,
id: this.keyModel.name,
});
}
}

View File

@ -1,119 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { task } from 'ember-concurrency';
import { waitFor } from '@ember/test-waiters';
/**
* @module KeymgmtKeyEdit
* KeymgmtKeyEdit components are used to display KeyMgmt Secrets engine UI for Key items
*
* @example
* ```js
* <KeymgmtKeyEdit @model={model} @mode="show" @tab="versions" />
* ```
* @param {object} model - model is the data from the store
* @param {string} [mode=show] - mode controls which view is shown on the component
* @param {string} [tab=details] - Options are "details" or "versions" for the show mode only
*/
const LIST_ROOT_ROUTE = 'vault.cluster.secrets.backend.list-root';
const SHOW_ROUTE = 'vault.cluster.secrets.backend.show';
export default class KeymgmtKeyEdit extends Component {
@service store;
@service router;
@service flashMessages;
@tracked isDeleteModalOpen = false;
get mode() {
return this.args.mode || 'show';
}
get keyAdapter() {
return this.store.adapterFor('keymgmt/key');
}
get isMutable() {
return ['create', 'edit'].includes(this.args.mode);
}
get isCreating() {
return this.args.mode === 'create';
}
@action
toggleModal(bool) {
this.isDeleteModalOpen = bool;
}
@task
@waitFor
*saveKey(evt) {
evt.preventDefault();
const { model } = this.args;
try {
yield model.save();
this.router.transitionTo(SHOW_ROUTE, model.name);
} catch (error) {
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);
}
}
}
@task
@waitFor
*removeKey() {
try {
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('. '));
}
}
@action
deleteKey() {
const secret = this.args.model;
const backend = secret.backend;
secret
.destroyRecord()
.then(() => {
this.router.transitionTo(LIST_ROOT_ROUTE, backend);
})
.catch((e) => {
this.flashMessages.danger(e.errors?.join('. '));
});
}
@task
@waitFor
*rotateKey() {
const id = this.args.model.name;
const backend = this.args.model.backend;
const adapter = this.keyAdapter;
yield adapter
.rotateKey(backend, id)
.then(() => {
this.flashMessages.success(`Success: ${id} connection was rotated`);
})
.catch((e) => {
this.flashMessages.danger(e.errors);
});
}
}

View File

@ -1,103 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { task } from 'ember-concurrency';
import { waitFor } from '@ember/test-waiters';
/**
* @module KeymgmtProviderEdit
* ProviderKeyEdit components are used to display KeyMgmt Secrets engine UI for Key items
*
* @example
* ```js
* <KeymgmtProviderEdit @model={model} @mode="show" />
* ```
* @param {object} model - model is the data from the store
* @param {string} mode - mode controls which view is shown on the component - show | create |
* @param {string} [tab] - Options are "details" or "keys" for the show mode only
*/
export default class KeymgmtProviderEdit extends Component {
@service router;
@service flashMessages;
constructor() {
super(...arguments);
// key count displayed in details tab and keys are listed in keys tab
if (this.args.mode === 'show') {
this.fetchKeys.perform();
}
}
@tracked modelValidations;
get isShowing() {
return this.args.mode === 'show';
}
get isCreating() {
return this.args.mode === 'create';
}
get viewingKeys() {
return this.args.tab === 'keys';
}
@task
@waitFor
*saveTask() {
const { model } = this.args;
try {
yield model.save();
this.router.transitionTo('vault.cluster.secrets.backend.show', model.id, {
queryParams: { itemType: 'provider' },
});
} catch (error) {
this.flashMessages.danger(error.errors.join('. '));
}
}
@task
@waitFor
*fetchKeys(page = 1) {
try {
yield this.args.model.fetchKeys(page);
} catch (error) {
this.flashMessages.danger(error.errors.join('. '));
}
}
@action
async onSave(event) {
event.preventDefault();
const { isValid, state } = await this.args.model.validate();
if (isValid) {
this.modelValidations = null;
this.saveTask.perform();
} else {
this.modelValidations = state;
}
}
@action
async onDelete() {
try {
const { model, root } = this.args;
await model.destroyRecord();
this.router.transitionTo(root.path, root.model, { queryParams: { tab: 'provider' } });
} catch (error) {
this.flashMessages.danger(error.errors.join('. '));
}
}
@action
async onDeleteKey(model) {
try {
await model.destroyRecord();
this.args.model.keys.removeObject(model);
} catch (error) {
this.flashMessages.danger(error.errors.join('. '));
}
}
}

View File

@ -17,14 +17,6 @@
@groupName="mount-type"
@onRadioChange={{mut this.selection}}
@disabled={{if type.requiredFeature (not (has-feature type.requiredFeature)) false}}
@tooltipMessage={{if
(eq type.type "keymgmt")
(concat
type.displayName
" is part of the Advanced Data Protection module, which is not included in your enterprise license."
)
"This secret engine is not included in your license."
}}
/>
{{/each}}
</div>

View File

@ -1,32 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
export default class WizardSecretsKeymgmtComponent extends Component {
get headerText() {
return {
provider: 'Creating a provider',
displayProvider: 'Distributing a key',
distribute: 'Creating a key',
}[this.args.featureState];
}
get body() {
return {
provider: 'This process connects an external provider to Vault. You will need its credentials.',
displayProvider: 'A key can now be created and distributed to this destination.',
distribute: 'This process creates a key and distributes it to your provider.',
}[this.args.featureState];
}
get instructions() {
return {
provider: 'Enter your provider details and click “Create provider“.',
displayProvider: 'Click “Distribute key” in the toolbar.',
distribute: 'Enter your key details and click “Distribute key”.',
}[this.args.featureState];
}
}

View File

@ -6,14 +6,6 @@
import { helper as buildHelper } from '@ember/component/helper';
const ENTERPRISE_SECRET_ENGINES = [
{
displayName: 'Key Management',
type: 'keymgmt',
glyph: 'key',
category: 'cloud',
requiredFeature: 'Key Management Secrets Engine',
routeQueryParams: { tab: 'provider' },
},
];
const MOUNTABLE_SECRET_ENGINES = [

View File

@ -9,7 +9,6 @@ export function secretQueryParams([backendType, type = ''], { asQueryParams }) {
const values = {
transit: { tab: 'actions' },
database: { type },
keymgmt: { itemType: type === 'provider' ? 'provider' : 'key' },
}[backendType];
// format required when using LinkTo with positional params
if (values && asQueryParams) {

View File

@ -13,7 +13,6 @@ const SUPPORTED_SECRET_BACKENDS = [
'pki',
'ssh',
'transit',
'keymgmt',
'kubernetes',
];

View File

@ -56,9 +56,6 @@ export default {
encryption: {
cond: (type) => type === 'transit',
},
provider: {
cond: (type) => type === 'keymgmt',
},
},
},
},
@ -134,33 +131,6 @@ export default {
CONTINUE: 'display',
},
},
provider: {
onEntry: [
{ type: 'render', level: 'step', component: 'wizard/secrets-keymgmt' },
{ type: 'render', level: 'feature', component: 'wizard/mounts-wizard' },
],
on: {
CONTINUE: 'displayProvider',
},
},
displayProvider: {
onEntry: [
{ type: 'render', level: 'step', component: 'wizard/secrets-keymgmt' },
{ type: 'render', level: 'feature', component: 'wizard/mounts-wizard' },
],
on: {
CONTINUE: 'distribute',
},
},
distribute: {
onEntry: [
{ type: 'render', level: 'step', component: 'wizard/secrets-keymgmt' },
{ type: 'render', level: 'feature', component: 'wizard/mounts-wizard' },
],
on: {
CONTINUE: 'display',
},
},
display: {
onEntry: [
{ type: 'render', level: 'step', component: 'wizard/secrets-display' },
@ -184,18 +154,6 @@ export default {
cond: (type) => type === 'transit',
actions: [{ type: 'routeTransition', params: ['vault.cluster.secrets.backend.create-root'] }],
},
provider: {
cond: (type) => type === 'keymgmt',
actions: [
{
type: 'routeTransition',
params: [
'vault.cluster.secrets.backend.create-root',
{ queryParams: { itemType: 'provider' } },
],
},
],
},
},
},
},

View File

@ -1,134 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Model, { attr } from '@ember-data/model';
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
export const KEY_TYPES = [
'aes256-gcm96',
'rsa-2048',
'rsa-3072',
'rsa-4096',
'ecdsa-p256',
'ecdsa-p384',
'ecdsa-p521',
];
export default class KeymgmtKeyModel extends Model {
@attr('string', {
label: 'Key name',
subText: 'This is the name of the key that shows in Vault.',
})
name;
@attr('string')
backend;
@attr('string', {
subText: 'The type of cryptographic key that will be created.',
possibleValues: KEY_TYPES,
defaultValue: 'rsa-2048',
})
type;
@attr('boolean', {
label: 'Allow deletion',
defaultValue: false,
})
deletionAllowed;
@attr('number', {
label: 'Current version',
})
latestVersion;
@attr('number', {
defaultValue: 0,
defaultShown: 'All versions enabled',
})
minEnabledVersion;
@attr('array')
versions;
// The following are calculated in serializer
@attr('date')
created;
@attr('date', {
defaultShown: 'Not yet rotated',
})
lastRotated;
// The following are from endpoints other than the main read one
@attr() provider; // string, or object with permissions error
@attr() distribution;
icon = 'key';
get hasVersions() {
return this.versions.length > 1;
}
get createFields() {
const createFields = ['name', 'type', 'deletionAllowed'];
return expandAttributeMeta(this, createFields);
}
get updateFields() {
return expandAttributeMeta(this, ['minEnabledVersion', 'deletionAllowed']);
}
get showFields() {
return expandAttributeMeta(this, [
'name',
'created',
'type',
'deletionAllowed',
'latestVersion',
'minEnabledVersion',
'lastRotated',
]);
}
get keyTypeOptions() {
return expandAttributeMeta(this, ['type'])[0];
}
get distFields() {
return [
{
name: 'name',
type: 'string',
label: 'Distributed name',
subText: 'The name given to the key by the provider.',
},
{ name: 'purpose', type: 'string', label: 'Key Purpose' },
{ name: 'protection', type: 'string', subText: 'Where cryptographic operations are performed.' },
];
}
@lazyCapabilities(apiPath`${'backend'}/key/${'id'}`, 'backend', 'id') keyPath;
@lazyCapabilities(apiPath`${'backend'}/key`, 'backend') keysPath;
@lazyCapabilities(apiPath`${'backend'}/key/${'id'}/kms`, 'backend', 'id') keyProvidersPath;
get canCreate() {
return this.keyPath.get('canCreate');
}
get canDelete() {
return this.keyPath.get('canDelete');
}
get canEdit() {
return this.keyPath.get('canUpdate');
}
get canRead() {
return this.keyPath.get('canRead');
}
get canList() {
return this.keysPath.get('canList');
}
get canListProviders() {
return this.keyProvidersPath.get('canList');
}
}

View File

@ -1,162 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Model, { attr } from '@ember-data/model';
import { tracked } from '@glimmer/tracking';
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
import { withModelValidations } from 'vault/decorators/model-validations';
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
import { inject as service } from '@ember/service';
const CRED_PROPS = {
};
const OPTIONAL_CRED_PROPS = ['session_token', 'endpoint'];
// since we have dynamic credential attributes based on provider we need a dynamic presence validator
// add validators for all cred props and return true for value if not associated with selected provider
const credValidators = Object.keys(CRED_PROPS).reduce((obj, providerKey) => {
CRED_PROPS[providerKey].forEach((prop) => {
if (!OPTIONAL_CRED_PROPS.includes(prop)) {
obj[`credentials.${prop}`] = [
{
message: `${prop} is required`,
validator(model) {
return model.credentialProps.includes(prop) ? model.credentials[prop] : true;
},
},
];
}
});
return obj;
}, {});
const validations = {
name: [{ type: 'presence', message: 'Provider name is required' }],
keyCollection: [{ type: 'presence', message: 'Key Vault instance name' }],
...credValidators,
};
@withModelValidations(validations)
export default class KeymgmtProviderModel extends Model {
@service store;
@attr('string') backend;
@attr('string', {
label: 'Provider name',
subText: 'This is the name of the provider that will be displayed in Vault. This cannot be edited later.',
})
name;
@attr('string', {
label: 'Type',
subText: 'Choose the provider type.',
possibleValues: [],
noDefault: true,
})
provider;
@attr('string', {
label: 'Key Vault instance name',
subText: 'The name of a Key Vault instance must be supplied. This cannot be edited later.',
})
keyCollection;
idPrefix = 'provider/';
type = 'provider';
@tracked keys = [];
@tracked credentials = null; // never returned from API -- set only during create/edit
get icon() {
return {
}[this.provider];
}
get typeName() {
return {
}[this.provider];
}
get showFields() {
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
? `${l} ${l > 1 ? 'keys' : 'key'}`
: this.canListKeys
? 'None'
: 'You do not have permission to list keys';
attrs.push({ hasBlock: true, isLink: l, label: 'Keys', value });
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;
const 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];
},
[{}, []]
);
this.credentials = creds;
return fields;
}
get createFields() {
return expandAttributeMeta(this, ['provider', 'name', 'keyCollection']);
}
async fetchKeys(page) {
if (this.canListKeys === false) {
this.keys = [];
} else {
// try unless capabilities returns false
try {
this.keys = await this.store.lazyPaginatedQuery('keymgmt/key', {
backend: this.backend,
provider: this.name,
responsePath: 'data.keys',
page,
});
} catch (error) {
this.keys = [];
if (error.httpStatus !== 404) {
throw error;
}
}
}
}
@lazyCapabilities(apiPath`${'backend'}/kms/${'id'}`, 'backend', 'id') providerPath;
@lazyCapabilities(apiPath`${'backend'}/kms`, 'backend') providersPath;
@lazyCapabilities(apiPath`${'backend'}/kms/${'id'}/key`, 'backend', 'id') providerKeysPath;
get canCreate() {
return this.providerPath.get('canCreate');
}
get canDelete() {
return this.providerPath.get('canDelete');
}
get canEdit() {
return this.providerPath.get('canUpdate');
}
get canRead() {
return this.providerPath.get('canRead');
}
get canList() {
return this.providersPath.get('canList');
}
get canListKeys() {
return this.providerKeysPath.get('canList');
}
get canCreateKeys() {
return this.providerKeysPath.get('canCreate');
}
}

View File

@ -129,9 +129,6 @@ export default class SecretEngineModel extends Model {
}
get icon() {
if (this.engineType === 'keymgmt') {
return 'key';
}
return this.engineType;
}
@ -168,11 +165,9 @@ export default class SecretEngineModel extends Model {
get formFields() {
const type = this.engineType;
const fields = ['type', 'path', 'description', 'accessor', 'local', 'sealWrap'];
// no ttl options for keymgmt
if (type !== 'keymgmt') {
fields.push('config.defaultLeaseTtl', 'config.maxLeaseTtl');
}
fields.push(
'config.defaultLeaseTtl',
'config.maxLeaseTtl',
'config.allowedManagedKeys',
'config.auditNonHmacRequestKeys',
'config.auditNonHmacResponseKeys',
@ -231,10 +226,6 @@ export default class SecretEngineModel extends Model {
defaultFields = ['path', 'config.defaultLeaseTtl', 'config.maxLeaseTtl', 'config.allowedManagedKeys'];
optionFields = [...CORE_OPTIONS, ...STANDARD_CONFIG];
break;
case 'keymgmt':
// no ttl options for keymgmt
optionFields = [...CORE_OPTIONS, 'config.allowedManagedKeys', ...STANDARD_CONFIG];
break;
default:
defaultFields = ['path'];
optionFields = [

View File

@ -80,7 +80,6 @@ export default Route.extend({
// secret or secret-v2
cubbyhole: 'secret',
kv: secretEngine.modelTypeForKV,
keymgmt: `keymgmt/${tab || 'key'}`,
generic: secretEngine.modelTypeForKV,
};
return types[type];

View File

@ -84,7 +84,6 @@ export default Route.extend(UnloadModelRoute, {
pki: secret && secret.startsWith('cert/') ? 'pki/cert' : 'pki/pki-role',
cubbyhole: 'secret',
kv: backendModel.modelTypeForKV,
keymgmt: `keymgmt/${options.queryParams?.itemType || 'key'}`,
generic: backendModel.modelTypeForKV,
};
return types[type];
@ -194,16 +193,7 @@ export default Route.extend(UnloadModelRoute, {
return secretModel;
},
// wizard will pause unless we manually continue it
updateWizard(params) {
// verify that keymgmt tutorial is in progress
if (params.itemType === 'provider' && this.wizard.nextStep === 'displayProvider') {
this.wizard.transitionFeatureMachine(this.wizard.featureState, 'CONTINUE', 'keymgmt');
}
},
async model(params, { to: { queryParams } }) {
this.updateWizard(params);
let secret = this.secretParam();
const backend = this.enginePathParam();
const modelType = this.modelType(backend, secret, { queryParams });

View File

@ -1,39 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationSerializer from '../application';
export default class KeymgmtKeySerializer extends ApplicationSerializer {
normalizeItems(payload) {
const normalized = super.normalizeItems(payload);
// Transform versions from object with number keys to array with key ids
if (normalized.versions) {
let lastRotated;
let created;
const versions = [];
Object.keys(normalized.versions).forEach((key, i, arr) => {
versions.push({
id: parseInt(key, 10),
...normalized.versions[key],
});
if (i === 0) {
created = normalized.versions[key].creation_time;
} else if (arr.length - 1 === i) {
// Set lastRotated to the last key
lastRotated = normalized.versions[key].creation_time;
}
});
normalized.versions = versions;
return { ...normalized, last_rotated: lastRotated, created };
} else if (Array.isArray(normalized)) {
return normalized.map((key) => ({
id: key.id,
name: key.id,
backend: payload.backend,
}));
}
return normalized;
}
}

View File

@ -1,29 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationSerializer from '../application';
export default class KeymgmtProviderSerializer extends ApplicationSerializer {
primaryKey = 'name';
normalizeItems(payload) {
const normalized = super.normalizeItems(payload);
if (Array.isArray(normalized)) {
normalized.forEach((provider) => {
provider.id = provider.name;
provider.backend = payload.backend;
});
}
return normalized;
}
serialize(snapshot) {
const json = super.serialize(...arguments);
return {
...json,
credentials: snapshot.record.credentials,
};
}
}

View File

@ -1,173 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
{{#if @backend}}
<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
@id="key"
@models={{array "keymgmt/key"}}
@onChange={{this.handleKeySelect}}
@passObject={{true}}
@inputValue={{this.formData.key}}
@subText="Type to use the name of an existing key that youd like to add to this provider, or to create one."
@wildcardLabel="key"
@label="Key name"
@fallbackComponent="string-list"
@selectLimit="1"
@backend={{@backend}}
@disallowNewItems={{false}}
>
{{#if (and this.validMatchError.key (not this.isNewKey))}}
<AlertInline @paddingTop={{true}} @sizeSmall={{true}} @type="danger" data-test-keymgmt-error="key">
{{this.validMatchError.key}}
To check compatibility,
<DocLink class="doc-link-subtle" @path="/vault/docs/secrets/key-management#compatibility">refer to this table</DocLink>.
</AlertInline>
{{/if}}
</SearchSelect>
</div>
{{/unless}}
{{#if this.isNewKey}}
<div class="field">
<label class="is-label" for="keyType">Key Type</label>
<p class="sub-text">The type of cryptographic key that will be created.</p>
<div class="control is-expanded">
<div class="select is-fullwidth">
<select
name="keyType"
id="keyType"
{{on "change" this.handleKeyType}}
class={{if this.validMatchError.key "has-error-border"}}
data-test-keymgmt-dist-keytype
>
<option value="">
Select one
</option>
{{#each this.keyTypes as |val|}}
<option selected={{eq this.keyType val}} value={{val}}>
{{val}}
</option>
{{/each}}
</select>
</div>
{{#if this.validMatchError.key}}
<AlertInline @paddingTop={{true}} @sizeSmall={{true}} @type="danger" data-test-keymgmt-error="new-key">
{{this.validMatchError.key}}
To check compatibility,
<DocLink class="doc-link-subtle" @path="/vault/docs/secrets/key-management#compatibility">refer to this table</DocLink>.
</AlertInline>
{{/if}}
</div>
</div>
{{/if}}
{{#unless @provider}}
<div class="field">
<SearchSelect
@id="provider"
@models={{array "keymgmt/provider"}}
@onChange={{this.handleProvider}}
@passObject={{false}}
@inputValue={{this.formData.provider}}
@subText="Select a provider in Vault. If it doesnt exist yet, youll need to add it first."
@label="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="/vault/docs/secrets/key-management#compatibility">refer to this table</DocLink>.
</AlertInline>
{{/if}}
</SearchSelect>
</div>
{{/unless}}
<fieldset
class="field form-fieldset"
id="operations"
disabled={{this.disableOperations}}
data-test-keymgmt-dist-operations
>
<legend class="is-label">Operations</legend>
<p class="sub-text">The types of operations this key can perform in the provider.</p>
{{#each this.operations as |op|}}
<div class="b-checkbox">
<Input
@type="checkbox"
id={{op}}
class="styled"
@checked={{false}}
{{on "input" this.handleOperation}}
data-test-operation={{op}}
/>
<label for={{op}}>{{capitalize op}}</label>
</div>
{{/each}}
</fieldset>
<fieldset class="field form-fieldset" id="protection" data-test-keymgmt-dist-protections>
<legend class="is-label">Protection</legend>
<p class="sub-text">Specifies the protection of the key.</p>
<div>
<RadioButton
id="protection-hsm"
name="hsm"
class="radio"
data-test-protection="hsm"
@value="hsm"
@groupValue={{this.formData.protection}}
@onChange={{fn (mut this.formData.protection)}}
/>
<label for="protection-hsm">HSM</label>
</div>
<div>
<RadioButton
id="protection-software"
name="software"
class="radio"
data-test-protection="software"
@value="software"
@groupValue={{this.formData.protection}}
@onChange={{fn (mut this.formData.protection)}}
/>
<label for="protection-software">Software</label>
</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.createDistribution.isRunning}}
class="button is-primary"
data-test-secret-save={{true}}
>
{{#if this.createDistribution.isRunning}}
<span class="loader is-inline-block"></span>
{{else}}
{{if (or (not @key) this.isNewKey) "Add key" "Distribute key"}}
{{/if}}
</button>
</div>
<div class="control">
<button type="button" class="button" {{on "click" @onClose}}>
Cancel
</button>
</div>
</div>
</form>
{{/if}}

View File

@ -1,272 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
<PageHeader as |p|>
<p.top>
<KeyValueHeader @path="vault.cluster.secrets.backend.show" @mode={{this.mode}} @root={{@root}} @showCurrent={{true}} />
</p.top>
<p.levelLeft>
<h1 class="title is-3" data-test-secret-header="true">
{{#if this.isDistributing}}
Distribute key
{{else if (eq @mode "create")}}
Create key
{{else if (eq @mode "edit")}}
Edit key
{{else}}
{{@model.id}}
{{/if}}
</h1>
</p.levelLeft>
</PageHeader>
{{#if this.isDistributing}}
<Keymgmt::Distribute @backend={{@model.backend}} @key={{@model.id}} @onClose={{fn (mut this.isDistributing) false}} />
{{else}}
{{#if (eq this.mode "show")}}
<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") "active"}}>
<LinkTo
@route="vault.cluster.secrets.backend.show"
@model={{@model.id}}
@query={{hash tab=""}}
data-test-tab="Details"
>
Details
</LinkTo>
</li>
<li class={{if (eq @tab "versions") "active"}}>
<LinkTo
@route="vault.cluster.secrets.backend.show"
@model={{@model.id}}
@query={{hash tab="versions"}}
data-test-tab="Versions"
>
Versions
</LinkTo>
</li>
</ul>
</nav>
</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"
class="toolbar-link"
disabled={{not @model.deletionAllowed}}
{{on "click" (fn (mut this.isDeleteModalOpen) true)}}
data-test-keymgmt-key-destroy
>
Destroy key
</button>
{{/if}}
{{#if @model.provider}}
<ConfirmAction
@buttonClasses="toolbar-link"
@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
</ConfirmAction>
{{/if}}
{{#if (or @model.canDelete @model.provider)}}
<div class="toolbar-separator"></div>
{{/if}}
<ConfirmAction
@buttonClasses="toolbar-link"
@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
</ConfirmAction>
{{#if @model.canEdit}}
<ToolbarSecretLink
@secret={{@model.id}}
@mode="edit"
@replace={{true}}
@queryParams={{hash itemType="key"}}
@data-test-edit-link={{true}}
>
Edit key
</ToolbarSecretLink>
{{/if}}
</ToolbarActions>
</Toolbar>
{{/if}}
{{#if this.isMutable}}
<form {{on "submit" (perform this.saveKey)}}>
<div class="box is-sideless is-fullwidth is-marginless">
{{#let (if (eq @mode "create") "createFields" "updateFields") as |fieldsKey|}}
{{#each (get @model fieldsKey) as |attr|}}
<FormField data-test-field={{true}} @attr={{attr}} @model={{@model}} />
{{/each}}
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">
<button
type="submit"
disabled={{this.saveTask.isRunning}}
class="button is-primary {{if this.saveTask.isRunning 'is-loading'}}"
data-test-keymgmt-key-submit
>
{{if this.isCreating "Create key" "Update"}}
</button>
</div>
<div class="control">
<LinkTo
@route={{if this.isCreating @root.path "vault.cluster.secrets.backend.show"}}
@model={{if this.isCreating @root.model @model.id}}
@query={{unless this.isCreating (hash itemType="key") (hash itemType="")}}
@disabled={{this.savekey.isRunning}}
class="button"
data-test-keymgmt-key-cancel
>
Cancel
</LinkTo>
</div>
</div>
{{/let}}
</div>
</form>
{{else if (eq @tab "versions")}}
{{#each @model.versions as |version|}}
<div class="list-item-row" data-test-keymgmt-key-version>
<div class="columns is-mobile">
<div class="column is-3 has-text-weight-bold">
<Icon @name="history" class="has-text-grey-light" />
<span>Version {{version.id}}</span>
</div>
<div class="column is-3 has-text-grey">
{{date-from-now version.creation_time addSuffix=true}}
</div>
<div class="column is-6 is-flex-center">
{{#if (eq @model.minEnabledVersion version.id)}}
<Icon @name="check-circle-fill" class="has-text-success" />
<span data-test-keymgmt-key-current-min>Current mininum enabled version</span>
{{/if}}
</div>
</div>
</div>
{{/each}}
{{else}}
<div class="has-top-margin-xl has-bottom-margin-s">
<h2 class="title is-5 has-border-bottom-light">Key Details</h2>
{{#each @model.showFields as |attr|}}
<InfoTableRow
@alwaysRender={{true}}
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{get @model attr.name}}
@defaultShown={{attr.options.defaultShown}}
@formatDate={{if (eq attr.type "date") "MMM d yyyy, h:mm:ss aaa"}}
/>
{{/each}}
</div>
<div class="has-top-margin-xl has-bottom-margin-s">
<h2 class="title is-5 {{if @model.distribution 'has-border-bottom-light' 'is-borderless'}}">
Distribution Details
</h2>
{{#if @model.provider.permissionsError}}
<EmptyState
@title="You are not authorized"
@subTitle="Error 403"
@message={{concat
"You must be granted permissions to see whether this key is distributed. Ask your administrator if you think you should have access to LIST /"
@model.backend
"/key/"
@model.name
"/kms."
}}
@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={{@model.provider}} @query={{hash itemType="provider"}}>
<Icon @name="check-circle-fill" class="has-text-success" />{{@model.provider}}
</LinkTo>
</InfoTableRow>
{{#if @model.distribution}}
{{#each @model.distFields as |attr|}}
<InfoTableRow
@alwaysRender={{true}}
@label={{capitalize (or attr.label (humanize (dasherize attr.name)))}}
@value={{if
(eq attr.name "protection")
(uppercase (get @model.distribution attr.name))
(get @model.distribution attr.name)
}}
@defaultShown={{attr.defaultShown}}
@helperText={{attr.subText}}
@formatDate={{if (eq attr.type "date") "MMM d yyyy, h:mm:ss aaa"}}
/>
{{/each}}
{{else}}
<EmptyState
@title="You are not authorized"
@subTitle="Error 403"
@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}}
{{/if}}
</div>
{{/if}}
{{/if}}
<ConfirmationModal
@title="Destroy key?"
@onClose={{fn (mut this.isDeleteModalOpen) false}}
@isActive={{this.isDeleteModalOpen}}
@confirmText={{@model.name}}
@toConfirmMsg="deleting the key"
@onConfirm={{fn this.deleteKey @model.id}}
>
<p>
Destroying the
<strong>{{@model.name}}</strong>
key means that the underlying data will be lost and the key will become unusable for cryptographic operations. It is
unrecoverable.
</p>
<MessageError @model={{this.model}} @errorMessage={{this.error}} />
</ConfirmationModal>

View File

@ -1,224 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
<PageHeader as |p|>
<p.top>
<KeyValueHeader @path="vault.cluster.secrets.backend.show" @mode={{@mode}} @root={{@root}} @showCurrent={{true}} />
</p.top>
<p.levelLeft>
<h1 class="title is-3" data-test-kms-provider-header>
{{#if this.isDistributing}}
Destribute key to provider
{{else if this.isShowing}}
Provider
<span class="has-font-weight-normal">{{@model.id}}</span>
{{else}}
{{if this.isCreating "Create provider" "Update credentials"}}
{{/if}}
</h1>
</p.levelLeft>
</PageHeader>
{{#if this.isDistributing}}
<Keymgmt::Distribute @backend={{@model.backend}} @provider={{@model.id}} @onClose={{fn (mut this.isDistributing) false}} />
{{else}}
{{#if this.isShowing}}
<div class="tabs-container box is-sideless is-fullwidth is-paddingless is-marginless">
<nav class="tabs">
<ul>
<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 "active"}} data-test-kms-provider-tab="keys">
<LinkTo @route="vault.cluster.secrets.backend.show" @model={{@model.id}} @query={{hash tab="keys"}}>
Keys
</LinkTo>
</li>
{{/if}}
</ul>
</nav>
</div>
{{#unless this.viewingKeys}}
<Toolbar data-test-kms-provider-details-actions>
<ToolbarActions>
{{#if @model.canDelete}}
<ToolTip @verticalPosition="above" @horizontalPosition="center" as |T|>
<T.Trigger data-test-tooltip-trigger>
<ConfirmAction
@buttonClasses="toolbar-link"
@onConfirmAction={{this.onDelete}}
@disabled={{@model.keys.length}}
data-test-kms-provider-delete={{true}}
>
Delete provider
</ConfirmAction>
</T.Trigger>
{{#if @model.keys.length}}
<T.Content class="tool-tip">
<div class="box" data-test-kms-provider-delete-tooltip>
This provider cannot be deleted until all
{{@model.keys.length}}
key(s) distributed to it are revoked. This can be done from the Keys tab.
</div>
</T.Content>
{{/if}}
</ToolTip>
{{/if}}
{{#if (and @model.canDelete (or @model.canListKeys @model.canEdit))}}
<div class="toolbar-separator"></div>
{{/if}}
{{#if (or @model.canListKeys @model.canCreateKeys)}}
<button
type="button"
class="toolbar-link"
{{on "click" (fn (mut this.isDistributing) true)}}
data-test-distribute-key
>
Distribute key
<Icon @name="chevron-right" />
</button>
{{/if}}
{{#if @model.canEdit}}
<ToolbarSecretLink
@secret={{@model.id}}
@mode="edit"
@replace={{true}}
@queryParams={{hash itemType="provider"}}
disabled={{(not @model.canEdit)}}
>
Update credentials
</ToolbarSecretLink>
{{/if}}
</ToolbarActions>
</Toolbar>
{{/unless}}
{{else}}
<form aria-label="update credentials" {{on "submit" this.onSave}}>
<div class="box is-sideless is-fullwidth is-marginless">
{{#if this.isCreating}}
{{#each @model.createFields as |attr index|}}
{{#if (eq index 2)}}
<div class="has-border-top-light">
<h2 class="title is-5 has-top-margin-l has-bottom-margin-m" data-test-kms-provider-config-title>
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}}
{{/each}}
{{/if}}
{{#unless this.isCreating}}
<h2 class="title is-5" data-test-kms-provider-creds-title>
New credentials
</h2>
<p class="sub-text has-bottom-margin-m">
Old credentials cannot be read and will be lost as soon as new ones are added. Do this carefully.
</p>
{{/unless}}
{{#each @model.credentialFields as |cred|}}
<FormField @attr={{cred}} @model={{@model}} @modelValidations={{this.modelValidations}} />
{{/each}}
</div>
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">
<button
type="submit"
disabled={{this.saveTask.isRunning}}
class="button is-primary {{if this.saveTask.isRunning 'is-loading'}}"
data-test-kms-provider-submit
>
{{if this.isCreating "Create provider" "Update"}}
</button>
</div>
<div class="control">
<LinkTo
@route={{if this.isCreating @root.path "vault.cluster.secrets.backend.show"}}
@model={{if this.isCreating @root.model @model.id}}
@query={{if this.isCreating (hash tab="provider") (hash itemType="provider")}}
@disabled={{this.saveTask.isRunning}}
class="button"
data-test-kms-provider-cancel
>
Cancel
</LinkTo>
</div>
</div>
</form>
{{/if}}
{{#if this.isShowing}}
<div class="has-bottom-margin-s">
{{#if this.viewingKeys}}
{{#let (options-for-backend "keymgmt" "key") as |options|}}
{{#if @model.keys.meta.total}}
{{#each @model.keys as |key|}}
<SecretList::Item
@item={{key}}
@backendModel={{@root}}
@backendType="keymgmt"
@delete={{fn this.onDeleteKey key}}
@itemPath={{concat options.modelPrefix key.id}}
@itemType={{options.item}}
@modelType={{@modelType}}
@options={{options}}
/>
{{/each}}
{{#if (gt @model.keys.meta.lastPage 1)}}
<PaginationControls
@total={{@model.keys.meta.total}}
@onChange={{perform this.fetchKeys}}
class="has-top-margin-xl has-bottom-margin-l"
/>
{{/if}}
{{else}}
<EmptyState
@title="No keys for this provider"
@message="Keys for this provider will be listed here. Add a key to get started."
>
<SecretLink @mode="create" @secret="" @queryParams={{hash itemType="key"}} class="link">
Create key
</SecretLink>
</EmptyState>
{{/if}}
{{/let}}
{{else}}
{{#each @model.showFields as |attr|}}
{{#if attr.hasBlock}}
<InfoTableRow @label={{attr.label}} @value={{attr.value}} data-test-kms-provider-field={{attr.name}}>
{{#if attr.icon}}
<Icon @name={{attr.icon}} class="icon" />
{{/if}}
{{#if attr.isLink}}
<LinkTo @route="vault.cluster.secrets.backend.show" @model={{@model.id}} @query={{hash tab="keys"}}>
{{attr.value}}
</LinkTo>
{{else}}
{{attr.value}}
{{/if}}
</InfoTableRow>
{{else}}
<InfoTableRow
@alwaysRender={{true}}
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{get @model attr.name}}
@defaultShown={{attr.options.defaultShown}}
@formatDate={{if (eq attr.type "date") "MMM d yyyy, h:mm:ss aaa"}}
/>
{{/if}}
{{/each}}
{{/if}}
</div>
{{/if}}
{{/if}}

View File

@ -1,16 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
<WizardSection
@headerText="Key Management"
@headerIcon="key"
@docText="Docs: Key Management Secrets Engine"
@docPath="/docs/secrets/key-management"
>
<p>
Key Management is a secrets engine that allows key generation, lifecycle management, and secure distribution of
cryptographic keys into cloud key management services.
</p>
</WizardSection>

View File

@ -5,10 +5,6 @@
<WizardSection @headerText="Your Secrets Engine" @instructions="Click on the link to add a {{@nextStep}} in the page header">
<p>
{{#if (eq @mountSubtype "keymgmt")}}
This secrets engine manages keys and distributes them to external destinations. We recommend that you create a provider
to which you can distribute keys.
{{else}}
{{#if @needsEncryption}}
The Transit Secrets Engine uses encryption keys to provide "encryption as a service". Click on "Create Encryption
Key" at the top to create one.
@ -17,6 +13,5 @@
Now that the engine has been mounted, lets connect a
{{@mountSubtype}}.
{{/if}}
{{/if}}
</p>
</WizardSection>

View File

@ -5,17 +5,10 @@
{{#if @isSupported}}
<WizardSection
@headerText={{if
(eq @mountSubtype "keymgmt")
"Your key and provider"
(unless @actionText "All set!" "Generate Credential")
}}
@headerText={{unless @actionText "All set!" "Generate Credential"}}
>
<p>
{{#if (eq @mountSubtype "keymgmt")}}
Your key and your provider have been created and connected. From here, you can click the key name to view the key
Youre now ready to start using the secrets engine.
{{else if @actionText}}
{{#if @actionText}}
Here is your generated credential. As you can see, we can only show the credential once, so you'll want to be sure to
save it. If you need another credential in the future, just come back and generate a new one.
{{else}}

View File

@ -1,10 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
<WizardSection @headerText={{this.headerText}} @instructions={{this.instructions}}>
<p>
{{this.body}}
</p>
</WizardSection>

View File

@ -18,7 +18,6 @@ const POSSIBLE_FEATURES = [
'Seal Wrapping',
'Control Groups',
'Namespaces',
'Key Management Secrets Engine',
];
export function hasFeature(featureName, features) {

View File

@ -90,31 +90,6 @@ const SECRET_BACKENDS = {
},
],
},
keymgmt: {
displayName: 'Key Management',
navigateTree: false,
listItemPartial: 'secret-list/item',
tabs: [
{
name: 'key',
label: 'Keys',
searchPlaceholder: 'Filter keys',
item: 'key',
create: 'Create key',
editComponent: 'keymgmt/key-edit',
},
{
name: 'provider',
modelPrefix: 'provider/',
label: 'Providers',
searchPlaceholder: 'Filter providers',
item: 'provider',
create: 'Create provider',
tab: 'provider',
editComponent: 'keymgmt/provider-edit',
},
],
},
transit: {
searchPlaceholder: 'Filter keys',
item: 'key',