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:
parent
1d066276d0
commit
4f1de3f76c
|
@ -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',
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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}}
|
|
@ -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;
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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")}}
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
|
|
|
@ -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"]'),
|
||||
});
|
|
@ -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"'),
|
||||
});
|
Loading…
Reference in New Issue