mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-05 12:26:34 +02:00
Ui/transform role edit updates transformations (#9910)
* Update transform role delete button to be ConfirmAction with dropdown * Set backend on fetched record so that it saves correctly * Update transformation after role transformations changed works * Clean up transform adapter * Add role to allowed_roles on added transformations and remove from removed transformations on role save, with flash message * Add backend to transform role model, and update serializer to add backend to paginated results * Clean up error message handling * Connect backend to transform roles list response * Capabilities on transform roles is correct * Fix cancel button on transform role edit location * Fix model path * Remove unnecessary tab param from controller * Add backend to transform model
This commit is contained in:
parent
cb52dec2a2
commit
2ea627aed3
@ -66,22 +66,17 @@ export default ApplicationAdapter.extend({
|
||||
|
||||
results.forEach(result => {
|
||||
if (result.value) {
|
||||
if (result.value.data.roles) {
|
||||
// TODO: Check if this is needed and remove if not
|
||||
resp.data = assign({}, resp.data, { zero_address_roles: result.value.data.roles });
|
||||
} else {
|
||||
let d = result.value.data;
|
||||
if (d.templates) {
|
||||
// In Transformations data goes up as "template", but comes down as "templates"
|
||||
// To keep the keys consistent we're translating here
|
||||
d = {
|
||||
...d,
|
||||
template: d.templates,
|
||||
};
|
||||
delete d.templates;
|
||||
}
|
||||
resp.data = assign({}, resp.data, d);
|
||||
let d = result.value.data;
|
||||
if (d.templates) {
|
||||
// In Transformations data goes up as "template", but comes down as "templates"
|
||||
// To keep the keys consistent we're translating here
|
||||
d = {
|
||||
...d,
|
||||
template: d.templates,
|
||||
};
|
||||
delete d.templates;
|
||||
}
|
||||
resp.data = assign({}, resp.data, d);
|
||||
}
|
||||
});
|
||||
return resp;
|
||||
|
||||
@ -42,7 +42,10 @@ export default ApplicationAdapter.extend({
|
||||
fetchByQuery(query) {
|
||||
const { backend, modelName, id } = query;
|
||||
return this.ajax(this.url(backend, modelName, id), 'GET').then(resp => {
|
||||
return resp;
|
||||
return {
|
||||
...resp,
|
||||
backend,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@ -1,22 +1,17 @@
|
||||
import { inject as service } from '@ember/service';
|
||||
import { or } from '@ember/object/computed';
|
||||
import { isBlank } from '@ember/utils';
|
||||
import { task, waitForEvent } from 'ember-concurrency';
|
||||
import Component from '@ember/component';
|
||||
import { set, get } from '@ember/object';
|
||||
import FocusOnInsertMixin from 'vault/mixins/focus-on-insert';
|
||||
import keys from 'vault/lib/keycodes';
|
||||
|
||||
const LIST_ROOT_ROUTE = 'vault.cluster.secrets.backend.list-root';
|
||||
const SHOW_ROUTE = 'vault.cluster.secrets.backend.show';
|
||||
|
||||
export default Component.extend(FocusOnInsertMixin, {
|
||||
router: service(),
|
||||
wizard: service(),
|
||||
|
||||
mode: null,
|
||||
// TODO: Investigate if we need all of these
|
||||
emptyData: '{\n}',
|
||||
onDataChange() {},
|
||||
onRefresh() {},
|
||||
model: null,
|
||||
@ -34,15 +29,6 @@ export default Component.extend(FocusOnInsertMixin, {
|
||||
}
|
||||
},
|
||||
|
||||
waitForKeyUp: task(function*() {
|
||||
while (true) {
|
||||
let event = yield waitForEvent(document.body, 'keyup');
|
||||
this.onEscape(event);
|
||||
}
|
||||
})
|
||||
.on('didInsertElement')
|
||||
.cancelOn('willDestroyElement'),
|
||||
|
||||
transitionToRoute() {
|
||||
this.get('router').transitionTo(...arguments);
|
||||
},
|
||||
@ -54,45 +40,33 @@ export default Component.extend(FocusOnInsertMixin, {
|
||||
}
|
||||
return modelPrefix;
|
||||
},
|
||||
|
||||
onEscape(e) {
|
||||
if (e.keyCode !== keys.ESC || this.get('mode') !== 'show') {
|
||||
return;
|
||||
}
|
||||
this.transitionToRoute(LIST_ROOT_ROUTE);
|
||||
},
|
||||
|
||||
hasDataChanges() {
|
||||
get(this, 'onDataChange')(get(this, 'model.hasDirtyAttributes'));
|
||||
},
|
||||
|
||||
persist(method, successCallback) {
|
||||
const model = get(this, 'model');
|
||||
return model[method]().then(() => {
|
||||
if (!get(model, 'isError')) {
|
||||
if (this.get('wizard.featureState') === 'role') {
|
||||
this.get('wizard').transitionFeatureMachine('role', 'CONTINUE', this.get('backendType'));
|
||||
}
|
||||
successCallback(model);
|
||||
}
|
||||
successCallback(model);
|
||||
});
|
||||
},
|
||||
|
||||
applyChanges(type, callback = () => {}) {
|
||||
const modelId = this.get('model.id') || this.get('model.name'); // transform comes in as model.name
|
||||
const modelPrefix = this.modelPrefixFromType(this.get('model.constructor.modelName'));
|
||||
// prevent from submitting if there's no key
|
||||
// maybe do something fancier later
|
||||
if (type === 'create' && isBlank(modelId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.persist('save', () => {
|
||||
callback();
|
||||
this.transitionToRoute(SHOW_ROUTE, `${modelPrefix}${modelId}`);
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
createOrUpdate(type, event) {
|
||||
event.preventDefault();
|
||||
const modelId = this.get('model.id') || this.get('model.name'); // transform comes in as model.name
|
||||
const modelPrefix = this.modelPrefixFromType(this.get('model.constructor.modelName'));
|
||||
// prevent from submitting if there's no key
|
||||
// maybe do something fancier later
|
||||
if (type === 'create' && isBlank(modelId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.persist('save', () => {
|
||||
this.hasDataChanges();
|
||||
this.transitionToRoute(SHOW_ROUTE, `${modelPrefix}${modelId}`);
|
||||
});
|
||||
this.applyChanges(type);
|
||||
},
|
||||
|
||||
setValue(key, event) {
|
||||
|
||||
@ -1,3 +1,130 @@
|
||||
import TransformBase from './transform-edit-base';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default TransformBase.extend({});
|
||||
const addToList = (list, itemToAdd) => {
|
||||
if (!list || !Array.isArray(list)) return list;
|
||||
list.push(itemToAdd);
|
||||
return list.uniq();
|
||||
};
|
||||
|
||||
const removeFromList = (list, itemToRemove) => {
|
||||
if (!list) return list;
|
||||
const index = list.indexOf(itemToRemove);
|
||||
if (index < 0) return list;
|
||||
const newList = list.removeAt(index, 1);
|
||||
return newList.uniq();
|
||||
};
|
||||
|
||||
export default TransformBase.extend({
|
||||
store: service(),
|
||||
flashMessages: service(),
|
||||
|
||||
initialTransformations: null,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.set('initialTransformations', this.get('model.transformations'));
|
||||
},
|
||||
|
||||
handleUpdateTransformations(updateTransformations, roleId, type = 'update') {
|
||||
if (!updateTransformations) return;
|
||||
const backend = this.get('model.backend');
|
||||
const promises = updateTransformations.map(transform => {
|
||||
return this.store
|
||||
.queryRecord('transform', {
|
||||
backend,
|
||||
id: transform.id,
|
||||
})
|
||||
.then(function(transformation) {
|
||||
let roles = transformation.allowed_roles;
|
||||
if (transform.action === 'ADD') {
|
||||
roles = addToList(roles, roleId);
|
||||
} else if (transform.action === 'REMOVE') {
|
||||
roles = removeFromList(roles, roleId);
|
||||
}
|
||||
|
||||
transformation.setProperties({
|
||||
backend,
|
||||
allowed_roles: roles,
|
||||
});
|
||||
|
||||
return transformation
|
||||
.save()
|
||||
.then(() => {
|
||||
return 'Successfully saved';
|
||||
})
|
||||
.catch(e => {
|
||||
return { errorStatus: e.httpStatus, ...transform };
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Promise.all(promises).then(res => {
|
||||
let hasError = res.find(r => !!r.errorStatus);
|
||||
|
||||
if (hasError) {
|
||||
let errorAdding = res.find(r => r.errorStatus === 403 && r.action === 'ADD');
|
||||
let errorRemoving = res.find(r => r.errorStatus === 403 && r.action === 'REMOVE');
|
||||
|
||||
let message =
|
||||
'The edits to this role were successful, but allowed_roles for its transformations was not edited due to a lack of permissions.';
|
||||
if (type === 'create') {
|
||||
message =
|
||||
'Transformations have been attached to this role, but the role was not added to those transformations’ allowed_roles due to a lack of permissions.';
|
||||
} else if (errorAdding && errorRemoving) {
|
||||
message =
|
||||
'This role was edited to both add and remove transformations; however, this role was not added or removed from those transformations’ allowed_roles due to a lack of permissions.';
|
||||
} else if (errorAdding) {
|
||||
message =
|
||||
'This role was edited to include new transformations, but this role was not added to those transformations’ allowed_roles due to a lack of permissions.';
|
||||
} else if (errorRemoving) {
|
||||
message =
|
||||
'This role was edited to remove transformations, but this role was not removed from those transformations’ allowed_roles due to a lack of permissions.';
|
||||
}
|
||||
this.get('flashMessages').stickyInfo(message);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
createOrUpdate(type, event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.applyChanges('save', () => {
|
||||
const roleId = this.get('model.id');
|
||||
const newModelTransformations = this.get('model.transformations');
|
||||
|
||||
if (!this.initialTransformations) {
|
||||
this.handleUpdatedTransformations(
|
||||
newModelTransformations.map(t => ({
|
||||
id: t,
|
||||
action: 'ADD',
|
||||
})),
|
||||
roleId,
|
||||
type
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const updateTransformations = [...newModelTransformations, ...this.initialTransformations]
|
||||
.map(t => {
|
||||
if (this.initialTransformations.indexOf(t) < 0) {
|
||||
return {
|
||||
id: t,
|
||||
action: 'ADD',
|
||||
};
|
||||
}
|
||||
if (newModelTransformations.indexOf(t) < 0) {
|
||||
return {
|
||||
id: t,
|
||||
action: 'REMOVE',
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(t => !!t);
|
||||
this.handleUpdateTransformations(updateTransformations, roleId);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -90,9 +90,12 @@ const Model = DS.Model.extend({
|
||||
transformFieldAttrs: computed('transformAttrs', function() {
|
||||
return expandAttributeMeta(this, this.get('transformAttrs'));
|
||||
}),
|
||||
|
||||
backend: attr('string', {
|
||||
readOnly: true,
|
||||
}),
|
||||
});
|
||||
|
||||
export default attachCapabilities(Model, {
|
||||
// TODO: Update to dynamic backend name
|
||||
updatePath: apiPath`transform/transformation/${'id'}`,
|
||||
updatePath: apiPath`${'backend'}/transformation/${'id'}`,
|
||||
});
|
||||
|
||||
@ -36,6 +36,8 @@ const Model = DS.Model.extend({
|
||||
let keys = ['name', 'transformations'];
|
||||
return expandAttributeMeta(this, keys);
|
||||
}),
|
||||
|
||||
backend: attr('string', { readOnly: true }),
|
||||
});
|
||||
|
||||
export default attachCapabilities(Model, {
|
||||
|
||||
@ -54,7 +54,7 @@ export default Route.extend({
|
||||
beforeModel() {
|
||||
let secret = this.secretParam();
|
||||
let backend = this.enginePathParam();
|
||||
let { tab } = this.paramsFor('vault.cluster.secrets.backend');
|
||||
let { tab } = this.paramsFor('vault.cluster.secrets.backend.list-root');
|
||||
let secretEngine = this.store.peekRecord('secret-engine', backend);
|
||||
let type = secretEngine && secretEngine.get('engineType');
|
||||
if (!type || !SUPPORTED_BACKENDS.includes(type)) {
|
||||
|
||||
@ -17,7 +17,7 @@ export default Route.extend(UnloadModelRoute, {
|
||||
let { backend } = this.paramsFor('vault.cluster.secrets.backend');
|
||||
return backend;
|
||||
},
|
||||
capabilities(secret) {
|
||||
capabilities(secret, modelType) {
|
||||
const backend = this.enginePathParam();
|
||||
let backendModel = this.modelFor('vault.cluster.secrets.backend');
|
||||
let backendType = backendModel.engineType;
|
||||
@ -28,12 +28,38 @@ export default Route.extend(UnloadModelRoute, {
|
||||
path = backend + '/keys/' + secret;
|
||||
} else if (backendType === 'ssh' || backendType === 'aws') {
|
||||
path = backend + '/roles/' + secret;
|
||||
} else if (modelType.startsWith('transform/')) {
|
||||
path = this.buildTransformPath(backend, secret, modelType);
|
||||
} else {
|
||||
path = backend + '/' + secret;
|
||||
}
|
||||
return this.store.findRecord('capabilities', path);
|
||||
},
|
||||
|
||||
buildTransformPath(backend, secret, modelType) {
|
||||
let noun = modelType.split('/')[1];
|
||||
return `${backend}/${noun}/${secret}`;
|
||||
},
|
||||
|
||||
modelTypeForTransform(secretName) {
|
||||
if (!secretName) return 'transform';
|
||||
if (secretName.startsWith('role/')) {
|
||||
return 'transform/role';
|
||||
}
|
||||
if (secretName.startsWith('template/')) {
|
||||
return 'transform/template';
|
||||
}
|
||||
if (secretName.startsWith('alphabet/')) {
|
||||
return 'transform/alphabet';
|
||||
}
|
||||
return 'transform'; // TODO: transform/transformation;
|
||||
},
|
||||
|
||||
transformSecretName(secret, modelType) {
|
||||
const noun = modelType.split('/')[1];
|
||||
return secret.replace(`${noun}/`, '');
|
||||
},
|
||||
|
||||
backendType() {
|
||||
return this.modelFor('vault.cluster.secrets.backend').get('engineType');
|
||||
},
|
||||
@ -71,7 +97,7 @@ export default Route.extend(UnloadModelRoute, {
|
||||
let types = {
|
||||
transit: 'transit-key',
|
||||
ssh: 'role-ssh',
|
||||
transform: secret && secret.startsWith('role/') ? 'transform/role' : 'transform', // CBS TODO: switch out better
|
||||
transform: this.modelTypeForTransform(secret),
|
||||
aws: 'role-aws',
|
||||
pki: secret && secret.startsWith('cert/') ? 'pki-certificate' : 'role-pki',
|
||||
cubbyhole: 'secret',
|
||||
@ -199,12 +225,11 @@ export default Route.extend(UnloadModelRoute, {
|
||||
secret = secret.replace('cert/', '');
|
||||
}
|
||||
if (modelType.startsWith('transform/')) {
|
||||
// CBS TODO: we'll have more things to replace than just role/
|
||||
secret = secret.replace('role/', '');
|
||||
secret = this.transformSecretName(secret, modelType);
|
||||
}
|
||||
let secretModel;
|
||||
|
||||
let capabilities = this.capabilities(secret);
|
||||
let capabilities = this.capabilities(secret, modelType);
|
||||
try {
|
||||
secretModel = await this.store.queryRecord(modelType, { id: secret, backend });
|
||||
} catch (err) {
|
||||
|
||||
@ -16,4 +16,18 @@ export default ApplicationSerializer.extend({
|
||||
}
|
||||
return json;
|
||||
},
|
||||
|
||||
extractLazyPaginatedData(payload) {
|
||||
let ret;
|
||||
ret = payload.data.keys.map(key => {
|
||||
let model = {
|
||||
id: key,
|
||||
};
|
||||
if (payload.backend) {
|
||||
model.backend = payload.backend;
|
||||
}
|
||||
return model;
|
||||
});
|
||||
return ret;
|
||||
},
|
||||
});
|
||||
|
||||
17
ui/app/serializers/transform/role.js
Normal file
17
ui/app/serializers/transform/role.js
Normal file
@ -0,0 +1,17 @@
|
||||
import ApplicationSerializer from '../application';
|
||||
export default ApplicationSerializer.extend({
|
||||
extractLazyPaginatedData(payload) {
|
||||
// TODO: do this for transform too?
|
||||
let ret;
|
||||
ret = payload.data.keys.map(key => {
|
||||
let model = {
|
||||
id: key,
|
||||
};
|
||||
if (payload.backend) {
|
||||
model.backend = payload.backend;
|
||||
}
|
||||
return model;
|
||||
});
|
||||
return ret;
|
||||
},
|
||||
});
|
||||
@ -24,17 +24,17 @@
|
||||
{{#if (eq mode "show")}}
|
||||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
{{#if (or capabilities.canUpdate capabilities.canDelete)}}
|
||||
<div class="toolbar-separator" />
|
||||
{{/if}}
|
||||
{{#if capabilities.canDelete}}
|
||||
<a
|
||||
class="toolbar-link"
|
||||
onclick={{action "delete"}}
|
||||
<ConfirmAction
|
||||
@buttonClasses="toolbar-link"
|
||||
@onConfirmAction={{action "delete"}}
|
||||
@confirmTitle="Are you sure?"
|
||||
@confirmMessage="Deleting this role means that you’ll need to recreate it and reassign any existing transformations to use it again."
|
||||
@confirmButtonText="Delete"
|
||||
data-test-transformation-role-delete
|
||||
>
|
||||
Delete role
|
||||
</a>
|
||||
</ConfirmAction>
|
||||
{{/if}}
|
||||
{{#if capabilities.canUpdate }}
|
||||
<ToolbarSecretLink
|
||||
@ -80,7 +80,7 @@
|
||||
{{#secret-link
|
||||
mode=(if (eq mode "create") "list" "show")
|
||||
class="button"
|
||||
secret=model.id
|
||||
secret=(concat "role/" model.id)
|
||||
}}
|
||||
Cancel
|
||||
{{/secret-link}}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user