Ui/role transform: tests and edit page (#9887)

* Set up acceptance tests for transform secrets engine

* Update search-select to optionally disallow new items

* role model transformations list does not allow new on search select

* Add test for creating a transform role

* Role edit extends TransformBase, roles list uses generic transform list item

* Fix edit role not populating transformations

* Role list item links to role show page correctly, and page has edit and delete buttons
This commit is contained in:
Chelsea Shaw 2020-09-03 14:44:37 -05:00 committed by GitHub
parent 1d066276d0
commit 4f1de3f76c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 222 additions and 8 deletions

View File

@ -58,7 +58,7 @@ const SECRET_BACKENDS = {
transform: {
displayName: 'Transformation',
navigateTree: false,
listItemPartial: 'partials/secret-list/transform-transformation-item',
listItemPartial: 'partials/secret-list/transform-list-item',
tabs: [
{
name: 'transformations',
@ -67,6 +67,7 @@ const SECRET_BACKENDS = {
item: 'transformation',
create: 'Create transformation',
editComponent: 'transformation-edit',
listItemPartial: 'partials/secret-list/transform-transformation-item',
},
{
name: 'role',

View File

@ -22,13 +22,14 @@ const Model = DS.Model.extend({
readOnly: true,
subText: 'The name for your role. This cannot be edited later.',
}),
transformations: attr('string', {
transformations: attr('array', {
editType: 'searchSelect',
fallbackComponent: 'string-list',
label: 'Transformations',
models: ['transform'],
subLabel: 'Transformations',
subText: 'Select which transformations this role will have access to. It must already exist.',
onlyAllowExisting: true,
}),
attrs: computed('transformations', function() {

View File

@ -17,7 +17,7 @@
type="submit"
disabled={{buttonDisabled}}
class="button is-primary"
data-test-role-ssh-create=true
data-test-transform-create=true
>
{{#if (eq mode 'create')}}
Create transformation

View File

@ -24,7 +24,6 @@
{{#if (eq mode "show")}}
<Toolbar>
<ToolbarActions>
{{!-- TODO: Ability to delete and edit role
{{#if (or capabilities.canUpdate capabilities.canDelete)}}
<div class="toolbar-separator" />
{{/if}}
@ -39,14 +38,14 @@
{{/if}}
{{#if capabilities.canUpdate }}
<ToolbarSecretLink
@secret={{model.id}}
@secret={{concat model.idPrefix model.id}}
@mode="edit"
@data-test-edit-link=true
@replace=true
>
Edit role
</ToolbarSecretLink>
{{/if}} --}}
{{/if}}
</ToolbarActions>
</Toolbar>
{{/if}}
@ -70,10 +69,10 @@
type="submit"
disabled={{buttonDisabled}}
class="button is-primary"
data-test-role-ssh-create=true
data-test-role-transform-create=true
>
{{#if (eq mode 'create')}}
Create transformation
Create role
{{else if (eq mode 'edit')}}
Save
{{/if}}

View File

@ -0,0 +1,77 @@
{{!-- TODO do not let click if !canRead --}}
{{#if (eq options.item "role")}}
{{#let (concat options.modelPrefix item.id) as |itemPath|}}
{{#linked-block
"vault.cluster.secrets.backend.show"
itemPath
class="list-item-row"
data-test-secret-link=itemPath
encode=true
queryParams=(secret-query-params backendModel.type)
}}
<div class="columns is-mobile">
<div class="column is-10">
<SecretLink
@mode="show"
@secret={{item.id}}
@queryParams={{if (eq backendModel.type "transform") (query-params tab="actions") ""}}
@class="has-text-black has-text-weight-semibold">
<Icon
@glyph='file-outline'
@class="has-text-grey-light"/>
{{if (eq item.id ' ') '(self)' (or item.keyWithoutParent item.id)}}
</SecretLink>
</div>
<div class="column has-text-right">
{{#if (or item.updatePath.canRead item.updatePath.canUpdate)}}
<PopupMenu name="secret-menu">
<nav class="menu">
<ul class="menu-list">
{{#if (or item.versionPath.isLoading item.secretPath.isLoading)}}
<li class="action">
<button disabled type="button" class="link button is-loading is-transparent">
loading
</button>
</li>
{{else}}
{{#if item.updatePath.canRead}}
<li class="action">
<SecretLink
@mode="show"
@secret={{itemPath}}
@class="has-text-black has-text-weight-semibold">
Details
</SecretLink>
</li>
{{/if}}
{{#if item.updatePath.canUpdate}}
<li class="action">
<SecretLink
@mode="edit"
@secret={{itemPath}}
@class="has-text-black has-text-weight-semibold">
Edit
</SecretLink>
</li>
{{/if}}
{{/if}}
</ul>
</nav>
</PopupMenu>
{{/if}}
</div>
</div>
{{/linked-block}}
{{/let}}
{{else}}
<div class="list-item-row">
<div class="columns is-mobile">
<div class="column is-12 has-text-grey has-text-weight-semibold">
<Icon
@glyph='file-outline'
@class="has-text-grey-light"/>
{{if (eq item.id ' ') '(self)' (or item.keyWithoutParent item.id)}}
</div>
</div>
</div>
{{/if}}

View File

@ -22,6 +22,7 @@ import layout from '../templates/components/search-select';
* @param [subLabel] {String} - a smaller label below the main Label
* @param fallbackComponent {String} - name of component to be rendered if the API call 403s
* @param [backend] {String} - name of the backend if the query for options needs additional information (eg. secret backend)
* @param [disallowNewItems=false] {Boolean} - Controls whether or not the user can add a new item if none found
*
* @param options {Array} - *Advanced usage* - `options` can be passed directly from the outside to the
* power-select component. If doing this, `models` should not also be passed as that will overwrite the
@ -44,6 +45,7 @@ export default Component.extend({
options: null, //all possible options
shouldUseFallback: false,
shouldRenderName: false,
disallowNewItems: false,
init() {
this._super(...arguments);
this.set('selectedOptions', this.inputValue || []);
@ -149,6 +151,9 @@ export default Component.extend({
return !options.some(group => group.options.findBy('id', id));
}
let existingOption = this.options && (this.options.findBy('id', id) || this.options.findBy('name', id));
if (this.disallowNewItems && !existingOption) {
return false;
}
return !existingOption;
},
},

View File

@ -79,6 +79,7 @@
@fallbackComponent={{attr.options.fallbackComponent}}
@selectLimit={{attr.options.selectLimit}}
@backend={{model.backend}}
@disallowNewItems={{attr.options.onlyAllowExisting}}
/>
</div>
{{else if (eq attr.options.editType "mountAccessor")}}

View File

@ -0,0 +1,90 @@
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 authPage from 'vault/tests/pages/auth';
import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend';
import transformationsPage from 'vault/tests/pages/secrets/backend/transform/transformations';
import rolesPage from 'vault/tests/pages/secrets/backend/transform/roles';
import searchSelect from 'vault/tests/pages/components/search-select';
const searchSelectComponent = create(searchSelect);
const mount = async () => {
let path = `transform-${Date.now()}`;
await mountSecrets.enable('transform', path);
return path;
};
module('Acceptance | Enterprise | Transform secrets', function(hooks) {
setupApplicationTest(hooks);
hooks.beforeEach(function() {
return authPage.login();
});
test('it enables Transform secrets engine and shows tabs', async function(assert) {
let path = `transform-${Date.now()}`;
await mountSecrets.enable('transform', path);
assert.equal(
currentURL(),
`/vault/secrets/${path}/list`,
'mounts and redirects to the transformations list page'
);
assert.ok(transformationsPage.isEmpty, 'renders empty state');
assert
.dom('.is-active[data-test-tab="Transformations"]')
.exists('Has Transformations tab which is active');
assert.dom('[data-test-tab="Roles"]').exists('Has Roles tab');
assert.dom('[data-test-tab="Templates"]').exists('Has Templates tab');
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();
const transformationName = 'foo';
const roleName = 'foo-role';
await transformationsPage.createLink();
assert.equal(currentURL(), `/vault/secrets/${path}/create`, 'redirects to create transformation page');
await transformationsPage.name(transformationName);
assert.dom('[data-test-input="type"').hasValue('fpe', 'Has type FPE by default');
assert.dom('[data-test-input="tweak_source"]').exists('Shows tweak source when FPE');
await transformationsPage.type('masking');
assert
.dom('[data-test-input="masking_character"]')
.exists('Shows masking character input when changed to masking type');
assert.dom('[data-test-input="tweak_source"]').doesNotExist('Does not show tweak source when masking');
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 transformationsPage.submit();
assert.equal(
currentURL(),
`/vault/secrets/${path}/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-tab="Roles"]');
assert.equal(currentURL(), `/vault/secrets/${path}/list?tab=role`, 'links to role list page');
await rolesPage.createLink();
assert.equal(
currentURL(),
`/vault/secrets/${path}/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');
await selectChoose('#transformations', '.ember-power-select-option', 0);
await rolesPage.submit();
assert.equal(
currentURL(),
`/vault/secrets/${path}/show/role/${roleName}`,
'redirects to show role page after submit'
);
});
});

View File

@ -89,6 +89,20 @@ module('Integration | Component | search select', function(hooks) {
assert.equal(component.options.length, 1, 'list shows one option');
});
test('it behaves correctly if new items not allowed', async function(assert) {
const models = ['identity/entity'];
this.set('models', models);
this.set('onChange', sinon.spy());
await render(hbs`{{search-select label="foo" models=models onChange=onChange disallowNewItems=true}}`);
await clickTrigger();
assert.equal(component.options.length, 3, 'shows all options');
await typeInSearch('p');
assert.equal(component.options.length, 1, 'list shows one option');
assert.equal(component.options[0].text, 'No results found');
await clickTrigger();
assert.ok(this.onChange.notCalled, 'on change not called when empty state clicked');
});
test('it moves option from drop down to list when clicked', async function(assert) {
const models = ['identity/entity'];
this.set('models', models);

View File

@ -0,0 +1,12 @@
import { create, clickable, fillable, visitable } from 'ember-cli-page-object';
import ListView from 'vault/tests/pages/components/list-view';
export default create({
...ListView,
visit: visitable('/vault/secrets/:backend/list?tab=roles'),
visitCreate: visitable('/vault/secrets/:backend/create?itemType=role'),
createLink: clickable('[data-test-secret-create="true"]'),
name: fillable('[data-test-input="name"]'),
transformations: fillable('[data-test-input="transformations"'),
submit: clickable('[data-test-role-transform-create="true"]'),
});

View File

@ -0,0 +1,14 @@
import { create, clickable, fillable, visitable } from 'ember-cli-page-object';
import ListView from 'vault/tests/pages/components/list-view';
export default create({
...ListView,
visit: visitable('/vault/secrets/:backend/list'),
visitCreate: visitable('/vault/secrets/:backend/create'),
createLink: clickable('[data-test-secret-create="true"]'),
name: fillable('[data-test-input="name"]'),
submit: clickable('[data-test-transform-create]'),
type: fillable('[data-test-input="type"'),
tweakSource: fillable('[data-test-input="tweak_source"'),
maskingChar: fillable('[data-test-input="masking_character"'),
});