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:
Chelsea Shaw 2020-09-15 13:46:35 -05:00 committed by GitHub
parent a08e9b41f9
commit 71c57dc4e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 245 additions and 52 deletions

View File

@ -1,3 +1,3 @@
import TransformBase from './transform-edit-base';
import TransformationEdit from './transformation-edit';
export default TransformBase.extend({});
export default TransformationEdit.extend({});

View File

@ -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);
});
},

View File

@ -1,3 +1,3 @@
import TransformBase from './transform-edit-base';
import TransformationEdit from './transformation-edit';
export default TransformBase.extend({});
export default TransformationEdit.extend({});

View File

@ -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 };
});
});
});

View File

@ -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);
});
},
},
});

View File

@ -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 = {

View File

@ -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

View File

@ -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

View File

@ -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');
});
});

View File

@ -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]'),
});

View File

@ -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]'),
});