mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-18 12:37:02 +02:00
* Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License. Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUS-1.1 * Fix test that expected exact offset on hcl file --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com> Co-authored-by: Sarah Thompson <sthompson@hashicorp.com> Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
271 lines
8.2 KiB
JavaScript
271 lines
8.2 KiB
JavaScript
/**
|
|
* Copyright (c) HashiCorp, Inc.
|
|
* SPDX-License-Identifier: BUSL-1.1
|
|
*/
|
|
|
|
/**
|
|
* @module SecretCreateOrUpdate
|
|
* SecretCreateOrUpdate component displays either the form for creating a new secret or creating a new version of the secret
|
|
*
|
|
* @example
|
|
* ```js
|
|
* <SecretCreateOrUpdate
|
|
* @mode="create"
|
|
* @model={{model}}
|
|
* @showAdvancedMode=true
|
|
* @modelForData={{@modelForData}}
|
|
* @isV2=true
|
|
* @secretData={{@secretData}}
|
|
* @canCreateSecretMetadata=false
|
|
* />
|
|
* ```
|
|
* @param {string} mode - create, edit, show determines what view to display
|
|
* @param {object} model - the route model, comes from secret-v2 ember record
|
|
* @param {boolean} showAdvancedMode - whether or not to show the JSON editor
|
|
* @param {object} modelForData - a class that helps track secret data, defined in secret-edit
|
|
* @param {boolean} isV2 - whether or not KV1 or KV2
|
|
* @param {object} secretData - class that is created in secret-edit
|
|
* @param {boolean} canUpdateSecretMetadata - based on permissions to the /metadata/ endpoint. If user has secret update. create is not enough for metadata.
|
|
*/
|
|
|
|
import Component from '@glimmer/component';
|
|
import ControlGroupError from 'vault/lib/control-group-error';
|
|
import Ember from 'ember';
|
|
import keys from 'vault/lib/keycodes';
|
|
import { action, set } from '@ember/object';
|
|
import { inject as service } from '@ember/service';
|
|
import { tracked } from '@glimmer/tracking';
|
|
import { isBlank, isNone } from '@ember/utils';
|
|
import { task, waitForEvent } from 'ember-concurrency';
|
|
|
|
const LIST_ROUTE = 'vault.cluster.secrets.backend.list';
|
|
const LIST_ROOT_ROUTE = 'vault.cluster.secrets.backend.list-root';
|
|
const SHOW_ROUTE = 'vault.cluster.secrets.backend.show';
|
|
|
|
export default class SecretCreateOrUpdate extends Component {
|
|
@tracked codemirrorString = null;
|
|
@tracked error = null;
|
|
@tracked secretPaths = null;
|
|
@tracked pathWhiteSpaceWarning = false;
|
|
@tracked validationErrorCount = 0;
|
|
@tracked validationMessages = null;
|
|
|
|
@service controlGroup;
|
|
@service router;
|
|
@service store;
|
|
|
|
@action
|
|
setup(elem, [secretData, model, mode]) {
|
|
this.codemirrorString = secretData.toJSONString();
|
|
this.validationMessages = {
|
|
path: '',
|
|
};
|
|
// for validation, return array of path names already assigned
|
|
if (Ember.testing) {
|
|
this.secretPaths = ['beep', 'bop', 'boop'];
|
|
} else {
|
|
const adapter = this.store.adapterFor('secret-v2');
|
|
const type = { modelName: 'secret-v2' };
|
|
const query = { backend: model.backend };
|
|
adapter.query(this.store, type, query).then((result) => {
|
|
this.secretPaths = result.data.keys;
|
|
});
|
|
}
|
|
this.checkRows();
|
|
|
|
if (mode === 'edit') {
|
|
this.addRow();
|
|
}
|
|
}
|
|
checkRows() {
|
|
if (this.args.secretData.length === 0) {
|
|
this.addRow();
|
|
}
|
|
}
|
|
checkValidation(name, value) {
|
|
if (name === 'path') {
|
|
// check for whitespace
|
|
this.pathHasWhiteSpace(value);
|
|
!value
|
|
? set(this.validationMessages, name, `${name} can't be blank.`)
|
|
: set(this.validationMessages, name, '');
|
|
}
|
|
// check duplicate on path
|
|
if (name === 'path' && value) {
|
|
this.secretPaths?.includes(value)
|
|
? set(this.validationMessages, name, `A secret with this ${name} already exists.`)
|
|
: set(this.validationMessages, name, '');
|
|
}
|
|
const values = Object.values(this.validationMessages);
|
|
this.validationErrorCount = values.filter(Boolean).length;
|
|
}
|
|
onEscape(e) {
|
|
if (e.keyCode !== keys.ESC || this.args.mode !== 'show') {
|
|
return;
|
|
}
|
|
const parentKey = this.args.model.parentKey;
|
|
if (parentKey) {
|
|
this.transitionToRoute(LIST_ROUTE, parentKey);
|
|
} else {
|
|
this.transitionToRoute(LIST_ROOT_ROUTE);
|
|
}
|
|
}
|
|
pathHasWhiteSpace(value) {
|
|
const validation = new RegExp('\\s', 'g'); // search for whitespace
|
|
this.pathWhiteSpaceWarning = validation.test(value);
|
|
}
|
|
// successCallback is called in the context of the component
|
|
persistKey(successCallback) {
|
|
const secret = this.args.model;
|
|
const secretData = this.args.modelForData;
|
|
const isV2 = this.args.isV2;
|
|
let key = secretData.get('path') || secret.id;
|
|
|
|
if (key.startsWith('/')) {
|
|
key = key.replace(/^\/+/g, '');
|
|
secretData.set(secretData.pathAttr, key);
|
|
}
|
|
const changed = secret.changedAttributes();
|
|
const changedKeys = Object.keys(changed);
|
|
|
|
return secretData
|
|
.save()
|
|
.then(() => {
|
|
if (!this.args.canReadSecretData && secret.selectedVersion) {
|
|
delete secret.selectedVersion.secretData;
|
|
}
|
|
if (!secretData.isError) {
|
|
if (isV2) {
|
|
secret.set('id', key);
|
|
}
|
|
// this secret.save() saves to the metadata endpoint. Only saved if metadata has been added
|
|
// and if the currentVersion attr changed that's because we added it (only happens if they don't have read access to metadata on mode = update which does not allow you to change metadata)
|
|
if (isV2 && changedKeys.length > 0 && changedKeys[0] !== 'currentVersion') {
|
|
// save secret metadata
|
|
secret
|
|
.save()
|
|
.then(() => {
|
|
this.saveComplete(successCallback, key);
|
|
})
|
|
.catch((e) => {
|
|
// when mode is not create the metadata error is handled in secret-edit-metadata
|
|
if (this.args.mode === 'create') {
|
|
this.error = e.errors.join(' ');
|
|
}
|
|
return;
|
|
});
|
|
} else {
|
|
this.saveComplete(successCallback, key);
|
|
}
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
if (error instanceof ControlGroupError) {
|
|
const errorMessage = this.controlGroup.logFromError(error);
|
|
this.error = errorMessage.content;
|
|
}
|
|
throw error;
|
|
});
|
|
}
|
|
saveComplete(callback, key) {
|
|
callback(key);
|
|
}
|
|
transitionToRoute() {
|
|
return this.router.transitionTo(...arguments);
|
|
}
|
|
|
|
get isCreateNewVersionFromOldVersion() {
|
|
const model = this.args.model;
|
|
if (!model) {
|
|
return false;
|
|
}
|
|
if (
|
|
!model.failedServerRead &&
|
|
!model.selectedVersion?.failedServerRead &&
|
|
model.selectedVersion?.version !== model.currentVersion
|
|
) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@(task(function* (name, value) {
|
|
this.checkValidation(name, value);
|
|
while (true) {
|
|
const event = yield waitForEvent(document.body, 'keyup');
|
|
this.onEscape(event);
|
|
}
|
|
})
|
|
.on('didInsertElement')
|
|
.cancelOn('willDestroyElement'))
|
|
waitForKeyUp;
|
|
|
|
@action
|
|
addRow() {
|
|
const data = this.args.secretData;
|
|
// fired off on init
|
|
if (isNone(data.findBy('name', ''))) {
|
|
data.pushObject({ name: '', value: '' });
|
|
this.handleChange();
|
|
}
|
|
this.checkRows();
|
|
}
|
|
@action
|
|
codemirrorUpdated(val, codemirror) {
|
|
this.error = null;
|
|
codemirror.performLint();
|
|
const noErrors = codemirror.state.lint.marked.length === 0;
|
|
if (noErrors) {
|
|
try {
|
|
this.args.secretData.fromJSONString(val);
|
|
set(this.args.modelForData, 'secretData', this.args.secretData.toJSON());
|
|
} catch (e) {
|
|
this.error = e.message;
|
|
}
|
|
}
|
|
this.codemirrorString = val;
|
|
}
|
|
@action
|
|
createOrUpdateKey(type, event) {
|
|
event.preventDefault();
|
|
if (type === 'create' && isBlank(this.args.modelForData.path || this.args.modelForData.id)) {
|
|
this.checkValidation('path', '');
|
|
return;
|
|
}
|
|
|
|
this.persistKey(() => {
|
|
this.transitionToRoute(SHOW_ROUTE, this.args.model.path || this.args.model.id);
|
|
});
|
|
}
|
|
@action
|
|
deleteRow(name) {
|
|
const data = this.args.secretData;
|
|
const item = data.findBy('name', name);
|
|
if (isBlank(item.name)) {
|
|
return;
|
|
}
|
|
data.removeObject(item);
|
|
this.checkRows();
|
|
this.handleChange();
|
|
}
|
|
@action
|
|
formatJSON() {
|
|
this.codemirrorString = this.args.secretData.toJSONString(true);
|
|
}
|
|
@action
|
|
handleMaskedInputChange(secret, index, value) {
|
|
const row = { ...secret, value };
|
|
set(this.args.secretData, index, row);
|
|
this.handleChange();
|
|
}
|
|
@action
|
|
handleChange() {
|
|
this.codemirrorString = this.args.secretData.toJSONString(true);
|
|
set(this.args.modelForData, 'secretData', this.args.secretData.toJSON());
|
|
}
|
|
@action
|
|
updateValidationErrorCount(errorCount) {
|
|
this.validationErrorCount = errorCount;
|
|
}
|
|
}
|