mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-06 04:46:25 +02:00
Ui/transformation edit update roles (#9955)
* Update or create new role after allowed_roles on transformation updated * Update tests to include transformation create/edit and role create scenarios
This commit is contained in:
parent
a08e9b41f9
commit
71c57dc4e2
@ -1,3 +1,3 @@
|
||||
import TransformBase from './transform-edit-base';
|
||||
import TransformationEdit from './transformation-edit';
|
||||
|
||||
export default TransformBase.extend({});
|
||||
export default TransformationEdit.extend({});
|
||||
|
||||
@ -8,7 +8,23 @@ import FocusOnInsertMixin from 'vault/mixins/focus-on-insert';
|
||||
const LIST_ROOT_ROUTE = 'vault.cluster.secrets.backend.list-root';
|
||||
const SHOW_ROUTE = 'vault.cluster.secrets.backend.show';
|
||||
|
||||
export const addToList = (list, itemToAdd) => {
|
||||
if (!list || !Array.isArray(list)) return list;
|
||||
list.push(itemToAdd);
|
||||
return list.uniq();
|
||||
};
|
||||
|
||||
export 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 Component.extend(FocusOnInsertMixin, {
|
||||
store: service(),
|
||||
flashMessages: service(),
|
||||
router: service(),
|
||||
|
||||
mode: null,
|
||||
@ -79,7 +95,7 @@ export default Component.extend(FocusOnInsertMixin, {
|
||||
|
||||
delete() {
|
||||
this.persist('destroyRecord', () => {
|
||||
this.hasDataChanges();
|
||||
this.onDataChange();
|
||||
this.transitionToRoute(LIST_ROOT_ROUTE);
|
||||
});
|
||||
},
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
import TransformBase from './transform-edit-base';
|
||||
import TransformationEdit from './transformation-edit';
|
||||
|
||||
export default TransformBase.extend({});
|
||||
export default TransformationEdit.extend({});
|
||||
|
||||
@ -1,24 +1,6 @@
|
||||
import TransformBase from './transform-edit-base';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
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();
|
||||
};
|
||||
import TransformBase, { addToList, removeFromList } from './transform-edit-base';
|
||||
|
||||
export default TransformBase.extend({
|
||||
store: service(),
|
||||
flashMessages: service(),
|
||||
|
||||
initialTransformations: null,
|
||||
|
||||
init() {
|
||||
@ -48,14 +30,9 @@ export default TransformBase.extend({
|
||||
allowed_roles: roles,
|
||||
});
|
||||
|
||||
return transformation
|
||||
.save()
|
||||
.then(() => {
|
||||
return 'Successfully saved';
|
||||
})
|
||||
.catch(e => {
|
||||
return { errorStatus: e.httpStatus, ...transform };
|
||||
});
|
||||
return transformation.save().catch(e => {
|
||||
return { errorStatus: e.httpStatus, ...transform };
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -1,3 +1,112 @@
|
||||
import TransformBase from './transform-edit-base';
|
||||
import TransformBase, { addToList, removeFromList } from './transform-edit-base';
|
||||
|
||||
export default TransformBase.extend({});
|
||||
export default TransformBase.extend({
|
||||
initialRoles: null,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.set('initialRoles', this.get('model.allowed_roles'));
|
||||
},
|
||||
|
||||
updateOrCreateRole(role, transformationId, backend) {
|
||||
return this.store
|
||||
.queryRecord('transform/role', {
|
||||
backend,
|
||||
id: role.id,
|
||||
})
|
||||
.then(roleStore => {
|
||||
let transformations = roleStore.transformations;
|
||||
if (role.action === 'ADD') {
|
||||
transformations = addToList(transformations, transformationId);
|
||||
} else if (role.action === 'REMOVE') {
|
||||
transformations = removeFromList(transformations, transformationId);
|
||||
}
|
||||
roleStore.setProperties({
|
||||
backend,
|
||||
transformations,
|
||||
});
|
||||
return roleStore.save().catch(e => {
|
||||
return {
|
||||
errorStatus: e.httpStatus,
|
||||
...role,
|
||||
};
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
if (e.httpStatus !== 403 && role.action === 'ADD') {
|
||||
// If role doesn't yet exist, create it with this transformation attached
|
||||
var newRole = this.store.createRecord('transform/role', {
|
||||
id: role.id,
|
||||
name: role.id,
|
||||
transformations: [transformationId],
|
||||
backend,
|
||||
});
|
||||
return newRole.save().catch(e => {
|
||||
return {
|
||||
errorStatus: e.httpStatus,
|
||||
...role,
|
||||
action: 'CREATE',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...role,
|
||||
errorStatus: e.httpStatus,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
handleUpdateRoles(updateRoles, transformationId) {
|
||||
if (!updateRoles) return;
|
||||
const backend = this.get('model.backend');
|
||||
const promises = updateRoles.map(r => this.updateOrCreateRole(r, transformationId, backend));
|
||||
|
||||
Promise.all(promises).then(results => {
|
||||
let hasError = results.find(role => !!role.errorStatus);
|
||||
|
||||
if (hasError) {
|
||||
let message =
|
||||
'The edits to this transformation were successful, but transformations for its roles was not edited due to a lack of permissions.';
|
||||
if (results.find(e => !!e.errorStatus && e.errorStatus !== 403)) {
|
||||
// if the errors weren't all due to permissions show generic message
|
||||
// eg. trying to update a role with empty array as transformations
|
||||
message = `You've edited the allowed_roles for this transformation. However, the corresponding edits to some roles' transformations were not made`;
|
||||
}
|
||||
this.get('flashMessages').stickyInfo(message);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
createOrUpdate(type, event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.applyChanges('save', () => {
|
||||
const transformationId = this.get('model.id');
|
||||
const newModelRoles = this.get('model.allowed_roles') || [];
|
||||
const initialRoles = this.get('initialRoles') || [];
|
||||
|
||||
const updateRoles = [...newModelRoles, ...initialRoles]
|
||||
.filter(r => r.indexOf('*') < 0) // TODO: expand wildcards into included roles instead
|
||||
.map(role => {
|
||||
if (initialRoles.indexOf(role) < 0) {
|
||||
return {
|
||||
id: role,
|
||||
action: 'ADD',
|
||||
};
|
||||
}
|
||||
if (newModelRoles.indexOf(role) < 0) {
|
||||
return {
|
||||
id: role,
|
||||
action: 'REMOVE',
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(r => !!r);
|
||||
this.handleUpdateRoles(updateRoles, transformationId);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
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 = {
|
||||
|
||||
@ -40,7 +40,7 @@
|
||||
type="submit"
|
||||
disabled={{buttonDisabled}}
|
||||
class="button is-primary"
|
||||
data-test-role-ssh-create=true
|
||||
data-test-transformation-save-button=true
|
||||
>
|
||||
{{#if (eq mode 'create')}}
|
||||
Create transformation
|
||||
|
||||
@ -62,6 +62,7 @@
|
||||
<a
|
||||
class="toolbar-link"
|
||||
onclick={{action (mut isEditModalActive) true}}
|
||||
data-test-edit-link
|
||||
>
|
||||
Edit transformation
|
||||
</a>
|
||||
@ -117,7 +118,12 @@
|
||||
</p>
|
||||
</section>
|
||||
<footer class="modal-card-foot modal-card-foot-outlined">
|
||||
{{#link-to "vault.cluster.secrets.backend.edit" model.id tagName="button" class="button is-primary"}}
|
||||
{{#link-to
|
||||
"vault.cluster.secrets.backend.edit" model.id
|
||||
tagName="button"
|
||||
class="button is-primary"
|
||||
data-test-edit-confirm-button=true
|
||||
}}
|
||||
Confirm
|
||||
{{/link-to}}
|
||||
<button
|
||||
|
||||
@ -2,7 +2,7 @@ import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { currentURL, click } from '@ember/test-helpers';
|
||||
import { create } from 'ember-cli-page-object';
|
||||
import { selectChoose, clickTrigger } from 'ember-power-select/test-support/helpers';
|
||||
import { typeInSearch, selectChoose, clickTrigger } from 'ember-power-select/test-support/helpers';
|
||||
|
||||
import authPage from 'vault/tests/pages/auth';
|
||||
import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend';
|
||||
@ -18,6 +18,29 @@ const mount = async () => {
|
||||
return path;
|
||||
};
|
||||
|
||||
const newTransformation = async (backend, name, submit = false) => {
|
||||
const transformationName = name || 'foo';
|
||||
await transformationsPage.visitCreate({ backend });
|
||||
await transformationsPage.name(transformationName);
|
||||
await clickTrigger('#template');
|
||||
await selectChoose('#template', '.ember-power-select-option', 0);
|
||||
// Don't automatically choose role because we might be testing that
|
||||
if (submit) {
|
||||
await transformationsPage.submit();
|
||||
}
|
||||
return transformationName;
|
||||
};
|
||||
|
||||
const newRole = async (backend, name) => {
|
||||
const roleName = name || 'bar';
|
||||
await rolesPage.visitCreate({ backend });
|
||||
await rolesPage.name(roleName);
|
||||
await clickTrigger('#transformations');
|
||||
await selectChoose('#transformations', '.ember-power-select-option', 0);
|
||||
await rolesPage.submit();
|
||||
return roleName;
|
||||
};
|
||||
|
||||
module('Acceptance | Enterprise | Transform secrets', function(hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
|
||||
@ -26,11 +49,11 @@ module('Acceptance | Enterprise | Transform secrets', function(hooks) {
|
||||
});
|
||||
|
||||
test('it enables Transform secrets engine and shows tabs', async function(assert) {
|
||||
let path = `transform-${Date.now()}`;
|
||||
await mountSecrets.enable('transform', path);
|
||||
let backend = `transform-${Date.now()}`;
|
||||
await mountSecrets.enable('transform', backend);
|
||||
assert.equal(
|
||||
currentURL(),
|
||||
`/vault/secrets/${path}/list`,
|
||||
`/vault/secrets/${backend}/list`,
|
||||
'mounts and redirects to the transformations list page'
|
||||
);
|
||||
assert.ok(transformationsPage.isEmpty, 'renders empty state');
|
||||
@ -42,12 +65,12 @@ module('Acceptance | Enterprise | Transform secrets', function(hooks) {
|
||||
assert.dom('[data-test-tab="Alphabets"]').exists('Has Alphabets tab');
|
||||
});
|
||||
|
||||
test('it can create a transformation and role', async function(assert) {
|
||||
let path = await mount();
|
||||
test('it can create a transformation and add itself to the role attached', async function(assert) {
|
||||
let backend = await mount();
|
||||
const transformationName = 'foo';
|
||||
const roleName = 'foo-role';
|
||||
await transformationsPage.createLink();
|
||||
assert.equal(currentURL(), `/vault/secrets/${path}/create`, 'redirects to create transformation page');
|
||||
await transformationsPage.createLink({ backend });
|
||||
assert.equal(currentURL(), `/vault/secrets/${backend}/create`, 'redirects to create transformation page');
|
||||
await transformationsPage.name(transformationName);
|
||||
|
||||
assert.dom('[data-test-input="type"').hasValue('fpe', 'Has type FPE by default');
|
||||
@ -60,31 +83,91 @@ module('Acceptance | Enterprise | Transform secrets', function(hooks) {
|
||||
await clickTrigger('#template');
|
||||
assert.equal(searchSelectComponent.options.length, 2, 'list shows two builtin options by default');
|
||||
await selectChoose('#template', '.ember-power-select-option', 0);
|
||||
await clickTrigger('#allowed_roles');
|
||||
await typeInSearch(roleName);
|
||||
await selectChoose('#allowed_roles', '.ember-power-select-option', 0);
|
||||
await transformationsPage.submit();
|
||||
assert.equal(
|
||||
currentURL(),
|
||||
`/vault/secrets/${path}/show/${transformationName}`,
|
||||
`/vault/secrets/${backend}/show/${transformationName}`,
|
||||
'redirects to show transformation page after submit'
|
||||
);
|
||||
await click(`[data-test-secret-breadcrumb="${path}"]`);
|
||||
assert.equal(currentURL(), `/vault/secrets/${path}/list`, 'Links back to list view from breadcrumb');
|
||||
await click(`[data-test-secret-breadcrumb="${backend}"]`);
|
||||
assert.equal(currentURL(), `/vault/secrets/${backend}/list`, 'Links back to list view from breadcrumb');
|
||||
});
|
||||
|
||||
test('it can create a role and add itself to the transformation attached', async function(assert) {
|
||||
const roleName = 'my-role';
|
||||
let backend = await mount();
|
||||
// create transformation without role
|
||||
await newTransformation(backend, 'a-transformation', true);
|
||||
await click(`[data-test-secret-breadcrumb="${backend}"]`);
|
||||
assert.equal(currentURL(), `/vault/secrets/${backend}/list`, 'Links back to list view from breadcrumb');
|
||||
await click('[data-test-tab="Roles"]');
|
||||
assert.equal(currentURL(), `/vault/secrets/${path}/list?tab=role`, 'links to role list page');
|
||||
assert.equal(currentURL(), `/vault/secrets/${backend}/list?tab=role`, 'links to role list page');
|
||||
// create role with transformation attached
|
||||
await rolesPage.createLink();
|
||||
assert.equal(
|
||||
currentURL(),
|
||||
`/vault/secrets/${path}/create?itemType=role`,
|
||||
`/vault/secrets/${backend}/create?itemType=role`,
|
||||
'redirects to create role page'
|
||||
);
|
||||
await rolesPage.name(roleName);
|
||||
await clickTrigger('#transformations');
|
||||
assert.equal(searchSelectComponent.options.length, 1, 'lists the transformation that was just created');
|
||||
assert.equal(searchSelectComponent.options.length, 1, 'lists the transformation');
|
||||
await selectChoose('#transformations', '.ember-power-select-option', 0);
|
||||
await rolesPage.submit();
|
||||
assert.equal(
|
||||
currentURL(),
|
||||
`/vault/secrets/${path}/show/role/${roleName}`,
|
||||
`/vault/secrets/${backend}/show/role/${roleName}`,
|
||||
'redirects to show role page after submit'
|
||||
);
|
||||
await click(`[data-test-secret-breadcrumb="${backend}"]`);
|
||||
assert.equal(
|
||||
currentURL(),
|
||||
`/vault/secrets/${backend}/list?tab=role`,
|
||||
'Links back to role list view from breadcrumb'
|
||||
);
|
||||
});
|
||||
|
||||
test('it adds a role to a transformation when added to a role', async function(assert) {
|
||||
const roleName = 'role-test';
|
||||
let backend = await mount();
|
||||
let transformation = await newTransformation(backend, 'b-transformation', true);
|
||||
await newRole(backend, roleName);
|
||||
await transformationsPage.visitShow({ backend, id: transformation });
|
||||
assert.dom('[data-test-row-value="Allowed roles"]').hasText(roleName);
|
||||
});
|
||||
|
||||
test('it shows a message if an update fails after save', async function(assert) {
|
||||
const roleName = 'role-remove';
|
||||
let backend = await mount();
|
||||
// Create transformation
|
||||
let transformation = await newTransformation(backend, 'c-transformation', true);
|
||||
// create role
|
||||
await newRole(backend, roleName);
|
||||
await transformationsPage.visitShow({ backend, id: transformation });
|
||||
assert.dom('[data-test-row-value="Allowed roles"]').hasText(roleName);
|
||||
// Edit transformation
|
||||
await click('[data-test-edit-link]');
|
||||
assert.dom('.modal.is-active').exists('Confirmation modal appears');
|
||||
await rolesPage.modalConfirm();
|
||||
assert.equal(
|
||||
currentURL(),
|
||||
`/vault/secrets/${backend}/edit/${transformation}`,
|
||||
'Correctly links to edit page for secret'
|
||||
);
|
||||
// remove role
|
||||
await click('#allowed_roles [data-test-selected-list-button="delete"]');
|
||||
await transformationsPage.save();
|
||||
assert.dom('.flash-message.is-info').exists('Shows info message since role could not be updated');
|
||||
assert.equal(
|
||||
currentURL(),
|
||||
`/vault/secrets/${backend}/show/${transformation}`,
|
||||
'Correctly links to show page for secret'
|
||||
);
|
||||
assert
|
||||
.dom('[data-test-row-value="Allowed roles"]')
|
||||
.doesNotExist('Allowed roles are no longer on the transformation');
|
||||
});
|
||||
});
|
||||
|
||||
@ -9,4 +9,5 @@ export default create({
|
||||
name: fillable('[data-test-input="name"]'),
|
||||
transformations: fillable('[data-test-input="transformations"'),
|
||||
submit: clickable('[data-test-role-transform-create="true"]'),
|
||||
modalConfirm: clickable('[data-test-edit-confirm-button]'),
|
||||
});
|
||||
|
||||
@ -4,6 +4,7 @@ import ListView from 'vault/tests/pages/components/list-view';
|
||||
export default create({
|
||||
...ListView,
|
||||
visit: visitable('/vault/secrets/:backend/list'),
|
||||
visitShow: visitable('/vault/secrets/:backend/show/:id'),
|
||||
visitCreate: visitable('/vault/secrets/:backend/create'),
|
||||
createLink: clickable('[data-test-secret-create="true"]'),
|
||||
name: fillable('[data-test-input="name"]'),
|
||||
@ -11,4 +12,5 @@ export default create({
|
||||
type: fillable('[data-test-input="type"'),
|
||||
tweakSource: fillable('[data-test-input="tweak_source"'),
|
||||
maskingChar: fillable('[data-test-input="masking_character"'),
|
||||
save: clickable('[data-test-transformation-save-button]'),
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user