vault/ui/app/components/secret-create-or-update.js
Jordan Reimer d4766766f2
Ember Upgrade to 4.4 (#17086)
* runs ember-cli-update to 4.4.0

* updates yarn.lock

* updates dependencies causing runtime errors (#17135)

* Inject Store Service When Accessed Implicitly (#17345)

* adds codemod for injecting store service

* adds custom babylon parser with decorators-legacy plugin for jscodeshift transforms

* updates inject-store-service codemod to only look for .extend object expressions and adds recast options

* runs inject-store-service codemod on js files

* replace query-params helper with hash (#17404)

* Updates/removes dependencies throwing errors in Ember 4.4 (#17396)

* updates ember-responsive to latest

* updates ember-composable-helpers to latest and uses includes helper since contains was removed

* updates ember-concurrency to latest

* updates ember-cli-clipboard to latest

* temporary workaround for toolbar-link component throwing errors for using params arg with LinkTo

* adds missing store injection to auth configure route

* fixes issue with string-list component throwing error for accessing prop in same computation

* fixes non-iterable query params issue in mfa methods controller

* refactors field-to-attrs to handle belongsTo rather than fragments

* converts mount-config fragment to belongsTo on auth-method model

* removes ember-api-actions and adds tune method to auth-method adapter

* converts cluster replication attributes from fragment to relationship

* updates ember-data, removes ember-data-fragments and updates yarn to latest

* removes fragments from secret-engine model

* removes fragment from test-form-model

* removes commented out code

* minor change to inject-store-service codemod and runs again on js files

* Remove LinkTo positional params (#17421)

* updates ember-cli-page-object to latest version

* update toolbar-link to support link-to args and not positional params

* adds replace arg to toolbar-link component

* Clean up js lint errors (#17426)

* replaces assert.equal to assert.strictEqual

* update eslint no-console to error and disables invididual intended uses of console

* cleans up hbs lint warnings (#17432)

* Upgrade bug and test fixes (#17500)

* updates inject-service codemod to take arg for service name and runs for flashMessages service

* fixes hbs lint error after merging main

* fixes flash messages

* updates more deps

* bug fixes

* test fixes

* updates ember-cli-content-security-policy and prevents default form submission throwing errors

* more bug and test fixes

* removes commented out code

* fixes issue with code-mirror modifier sending change event on setup causing same computation error

* Upgrade Clean Up (#17543)

* updates deprecation workflow and filter

* cleans up build errors, removes unused ivy-codemirror and sass and updates ember-cli-sass and node-sass to latest

* fixes control groups test that was skipped after upgrade

* updates control group service tests

* addresses review feedback

* updates control group service handleError method to use router.currentURL rather that transition.intent.url

* adds changelog entry
2022-10-18 09:46:02 -06:00

276 lines
8.4 KiB
JavaScript

/**
* @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;
@service wizard;
@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, '');
}
let 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) {
let validation = new RegExp('\\s', 'g'); // search for whitespace
this.pathWhiteSpaceWarning = validation.test(value);
}
// successCallback is called in the context of the component
persistKey(successCallback) {
let secret = this.args.model;
let secretData = this.args.modelForData;
let isV2 = this.args.isV2;
let key = secretData.get('path') || secret.id;
if (key.startsWith('/')) {
key = key.replace(/^\/+/g, '');
secretData.set(secretData.pathAttr, key);
}
let changed = secret.changedAttributes();
let 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) {
let errorMessage = this.controlGroup.logFromError(error);
this.error = errorMessage.content;
}
throw error;
});
}
saveComplete(callback, key) {
if (this.wizard.featureState === 'secret') {
this.wizard.transitionFeatureMachine('secret', 'CONTINUE');
}
callback(key);
}
transitionToRoute() {
return this.router.transitionTo(...arguments);
}
get isCreateNewVersionFromOldVersion() {
let 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) {
let 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
handleChange() {
this.codemirrorString = this.args.secretData.toJSONString(true);
set(this.args.modelForData, 'secretData', this.args.secretData.toJSON());
}
//submit on shift + enter
@action
handleKeyDown(e) {
e.stopPropagation();
if (!(e.keyCode === keys.ENTER && e.metaKey)) {
return;
}
let $form = this.element.querySelector('form');
if ($form.length) {
$form.submit();
}
}
@action
updateValidationErrorCount(errorCount) {
this.validationErrorCount = errorCount;
}
}