remove keymgmt (ENT)
This commit is contained in:
parent
a0f77417d5
commit
0567588b44
@ -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
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
@ -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('. '));
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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];
|
||||
}
|
||||
}
|
@ -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 = [
|
||||
|
@ -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) {
|
||||
|
@ -13,7 +13,6 @@ const SUPPORTED_SECRET_BACKENDS = [
|
||||
'pki',
|
||||
'ssh',
|
||||
'transit',
|
||||
'keymgmt',
|
||||
'kubernetes',
|
||||
];
|
||||
|
||||
|
@ -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' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
}
|
@ -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 = [
|
||||
|
@ -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];
|
||||
|
@ -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 });
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
@ -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 you’d 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 doesn’t exist yet, you’ll 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}}
|
@ -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>
|
@ -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}}
|
@ -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>
|
@ -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, let’s connect a
|
||||
{{@mountSubtype}}.
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</p>
|
||||
</WizardSection>
|
@ -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
|
||||
You’re 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}}
|
||||
|
@ -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>
|
@ -18,7 +18,6 @@ const POSSIBLE_FEATURES = [
|
||||
'Seal Wrapping',
|
||||
'Control Groups',
|
||||
'Namespaces',
|
||||
'Key Management Secrets Engine',
|
||||
];
|
||||
|
||||
export function hasFeature(featureName, features) {
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user