mirror of
https://github.com/hashicorp/vault.git
synced 2026-02-03 20:40:45 -05:00
* updates kmip scope roles route to ts * updates kmip scope roles route to use api service and adds page component * converts kmip role route to ts * fixes a11y error in kmip header-credentials component * updates kmip role route to use api service and adds page component * removes kmip operation-field-display component that was moved into role page component * converts kmip role create route to ts * moves kmip role form component to component directory root * converts kmip role form component to ts * adds operation-groups helper and refactors kmip role page to use it * adds operation-label helper and updates kmip role page to use it * converts kmip edit role route to ts * updates kmip role test to use operation-groups helper * adds kmip role form * updates kmip role edit and create routes to use api service and form class * updates kmip role form component to work with form class * updates kmip acceptance tests * fixes flash message issue on kmip role form submit success Co-authored-by: Jordan Reimer <zofskeez@gmail.com>
This commit is contained in:
parent
812498cfc6
commit
8b300cf6eb
34 changed files with 1386 additions and 647 deletions
46
ui/app/forms/secrets/kmip/role.ts
Normal file
46
ui/app/forms/secrets/kmip/role.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import OpenApiForm from 'vault/forms/open-api';
|
||||
|
||||
import type { KmipWriteRoleRequest } from '@hashicorp/vault-client-typescript';
|
||||
import type Form from 'vault/forms/form';
|
||||
import type FormField from 'vault/utils/forms/field';
|
||||
|
||||
export default class KmipRoleForm extends OpenApiForm<KmipWriteRoleRequest> {
|
||||
constructor(...args: ConstructorParameters<typeof Form>) {
|
||||
super('KmipWriteRoleRequest', ...args);
|
||||
}
|
||||
|
||||
get tlsFields() {
|
||||
return this.formFields.filter((field) => field.name.startsWith('tls_'));
|
||||
}
|
||||
// there are currently no other fields but adding this for future-proofing
|
||||
get otherFields() {
|
||||
return this.formFields.filter(
|
||||
(field) => !field.name.startsWith('tls_') && !field.name.startsWith('operation_')
|
||||
);
|
||||
}
|
||||
// helper used in form template to look up field by name
|
||||
fieldFor = (key: keyof KmipWriteRoleRequest) => {
|
||||
return this.formFields.find((f) => f.name === key) as FormField;
|
||||
};
|
||||
|
||||
toJSON() {
|
||||
let data = this.data;
|
||||
const { tls_client_key_bits, tls_client_key_type, tls_client_ttl } = data;
|
||||
const tls = { tls_client_key_bits, tls_client_key_type, tls_client_ttl };
|
||||
if (data.operation_all) {
|
||||
data = { ...tls, operation_all: true };
|
||||
} else if (data.operation_none) {
|
||||
data = { ...tls, operation_none: true };
|
||||
} else {
|
||||
// ensure operation_all and operation_none are not present in payload
|
||||
const { operation_all, operation_none, ...rest } = data;
|
||||
data = rest;
|
||||
}
|
||||
return super.toJSON(data);
|
||||
}
|
||||
}
|
||||
|
|
@ -59,4 +59,5 @@ export const PATH_MAP = {
|
|||
kubernetesRole: apiPath`${'backend'}/role/${'name'}`,
|
||||
kubernetesCreds: apiPath`${'backend'}/creds/${'name'}`,
|
||||
kmipScope: apiPath`${'backend'}/scopes/${'name'}`,
|
||||
kmipRole: apiPath`${'backend'}/scopes/${'scope'}/roles/${'name'}`,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,12 +12,16 @@
|
|||
<div class="tabs-container box is-sideless is-fullwidth is-paddingless is-marginless">
|
||||
<nav class="tabs">
|
||||
<ul>
|
||||
<LinkTo @route="credentials.index" @models={{array @scope @role}} data-test-kmip-link-credentials="true">
|
||||
Credentials
|
||||
</LinkTo>
|
||||
<LinkTo @route="role" @models={{array @scope @role}} data-test-kmip-link-role-details="true">
|
||||
Details
|
||||
</LinkTo>
|
||||
<li>
|
||||
<LinkTo @route="credentials.index" @models={{array @scope @role}} data-test-kmip-link-credentials="true">
|
||||
Credentials
|
||||
</LinkTo>
|
||||
</li>
|
||||
<li>
|
||||
<LinkTo @route="role" @models={{array @scope @role}} data-test-kmip-link-role-details="true">
|
||||
Details
|
||||
</LinkTo>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
{{!
|
||||
Copyright IBM Corp. 2016, 2025
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<form {{on "submit" (perform this.save)}}>
|
||||
<MessageError @model={{@model}} data-test-edit-form-error />
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<NamespaceReminder @mode="save" />
|
||||
{{#if @model.isNew}}
|
||||
{{! Show role name only in create mode }}
|
||||
<FormField data-test-field @attr={{hash name="role" type="string" options=(hash label="Name")}} @model={{@model}} />
|
||||
{{/if}}
|
||||
<div class="control is-flex box is-shadowless is-fullwidth is-marginless">
|
||||
<input
|
||||
data-test-input="operationNone"
|
||||
id="operationNone"
|
||||
type="checkbox"
|
||||
class="toggle is-success is-small"
|
||||
checked={{not @model.operationNone}}
|
||||
{{on "change" this.toggleOperationSpecial}}
|
||||
/>
|
||||
<label for="operationNone" class="has-text-weight-bold is-size-8">
|
||||
Allow this role to perform KMIP operations
|
||||
</label>
|
||||
</div>
|
||||
{{#unless @model.operationNone}}
|
||||
<Toolbar>
|
||||
<h3 class="title is-6 has-left-padding-s" data-test-kmip-section="Allowed Operations">
|
||||
Allowed Operations
|
||||
</h3>
|
||||
</Toolbar>
|
||||
<div class="box">
|
||||
<FormField
|
||||
@attr={{hash name="operationAll" type="boolean" options=(hash label="Allow this role to perform all operations")}}
|
||||
@model={{@model}}
|
||||
/>
|
||||
<hr />
|
||||
<div class="kmip-role-operations">
|
||||
{{#each this.operationFormGroups as |group|}}
|
||||
<div class="kmip-role-allowed-operations">
|
||||
<h4 class="title is-7" data-test-kmip-operations={{group.name}}>{{group.name}}</h4>
|
||||
{{#each group.fields as |attr|}}
|
||||
<FormField
|
||||
data-test-field
|
||||
@disabled={{or @model.operationNone @model.operationAll}}
|
||||
@attr={{attr}}
|
||||
@model={{this.placeholderOrModel @model attr.name}}
|
||||
@showHelpText={{false}}
|
||||
/>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/unless}}
|
||||
<div class="box is-fullwidth is-shadowless">
|
||||
<h3 class="title is-3" data-test-kmip-section="TLS">
|
||||
TLS
|
||||
</h3>
|
||||
{{#each this.tlsFormFields as |attr|}}
|
||||
<FormField data-test-field @attr={{attr}} @model={{@model}} />
|
||||
{{/each}}
|
||||
</div>
|
||||
{{#each @model.fields as |attr|}}
|
||||
<FormField data-test-field @attr={{attr}} @model={{@model}} />
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
<div class="field is-grouped is-grouped-split is-fullwidth box is-bottomless">
|
||||
<Hds::ButtonSet>
|
||||
<Hds::Button
|
||||
@text="Save"
|
||||
@icon={{if this.save.isRunning "loading"}}
|
||||
type="submit"
|
||||
disabled={{this.save.isRunning}}
|
||||
data-test-submit
|
||||
/>
|
||||
<Hds::Button @text="Cancel" @color="secondary" {{on "click" @onCancel}} data-test-cancel />
|
||||
</Hds::ButtonSet>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import AdapterError from '@ember-data/adapter/error';
|
||||
import { action } from '@ember/object';
|
||||
import { service } from '@ember/service';
|
||||
import Component from '@glimmer/component';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { removeManyFromArray } from 'vault/helpers/remove-from-array';
|
||||
import { operationFieldsWithoutSpecial, tlsFields } from 'vault/utils/model-helpers/kmip-role-fields';
|
||||
|
||||
export default class KmipRoleFormComponent extends Component {
|
||||
@service flashMessages;
|
||||
@service store;
|
||||
|
||||
// Actual attribute fields
|
||||
get tlsFormFields() {
|
||||
return tlsFields().map((attr) => this.args.model.allByKey[attr]);
|
||||
}
|
||||
get operationFormGroups() {
|
||||
const objects = [
|
||||
'operationCreate',
|
||||
'operationActivate',
|
||||
'operationGet',
|
||||
'operationLocate',
|
||||
'operationRekey',
|
||||
'operationRevoke',
|
||||
'operationDestroy',
|
||||
];
|
||||
const attributes = ['operationAddAttribute', 'operationGetAttributes'];
|
||||
const server = ['operationDiscoverVersions'];
|
||||
const others = removeManyFromArray(operationFieldsWithoutSpecial(this.args.model.editableFields), [
|
||||
...objects,
|
||||
...attributes,
|
||||
...server,
|
||||
]);
|
||||
const groups = [
|
||||
{ name: 'Managed Cryptographic Objects', fields: objects },
|
||||
{ name: 'Object Attributes', fields: attributes },
|
||||
{ name: 'Server', fields: server },
|
||||
];
|
||||
if (others.length) {
|
||||
groups.push({
|
||||
name: 'Other',
|
||||
fields: others,
|
||||
});
|
||||
}
|
||||
// expand field names to attributes
|
||||
return groups.map((group) => ({
|
||||
...group,
|
||||
fields: group.fields.map((attr) => this.args.model.allByKey[attr]),
|
||||
}));
|
||||
}
|
||||
|
||||
placeholderOrModel = (model, attrName) => {
|
||||
return model.operationAll ? { [attrName]: true } : model;
|
||||
};
|
||||
|
||||
preSave() {
|
||||
const opFieldsWithoutSpecial = operationFieldsWithoutSpecial(this.args.model.editableFields);
|
||||
// if we have operationAll or operationNone, we want to clear
|
||||
// out the others so that display shows the right data
|
||||
if (this.args.model.operationAll || this.args.model.operationNone) {
|
||||
opFieldsWithoutSpecial.forEach((field) => (this.args.model[field] = null));
|
||||
}
|
||||
// set operationNone if user unchecks 'operationAll' instead of toggling the 'operationNone' input
|
||||
// doing here instead of on the 'operationNone' input because a user might deselect all, then reselect some options
|
||||
// and immediately setting operationNone will hide all of the checkboxes in the UI
|
||||
this.args.model.operationNone =
|
||||
opFieldsWithoutSpecial.every((attr) => this.args.model[attr] !== true) && !this.args.model.operationAll;
|
||||
return this.args.model;
|
||||
}
|
||||
|
||||
@action toggleOperationSpecial(evt) {
|
||||
const { checked } = evt.target;
|
||||
this.args.model.operationNone = !checked;
|
||||
this.args.model.operationAll = checked;
|
||||
}
|
||||
|
||||
save = task(async (evt) => {
|
||||
evt.preventDefault();
|
||||
const model = this.preSave();
|
||||
try {
|
||||
await model.save();
|
||||
this.flashMessages.success(`Saved role ${model.role}`);
|
||||
} catch (err) {
|
||||
// err will display via model state
|
||||
// AdapterErrors are handled by the error-message component
|
||||
if (err instanceof AdapterError === false) {
|
||||
throw err;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.args.onSave();
|
||||
});
|
||||
|
||||
willDestroy() {
|
||||
// components are torn down after store is unloaded and will cause an error if attempt to unload record
|
||||
const noTeardown = this.store && !this.store.isDestroying;
|
||||
if (noTeardown && this.args?.model?.isDirty) {
|
||||
this.args.model.rollbackAttributes();
|
||||
}
|
||||
super.willDestroy();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
{{!
|
||||
Copyright IBM Corp. 2016, 2025
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
{{#if this.model.operationAll}}
|
||||
<AlertInline @type="info" @message="This role allows all KMIP operations" class="is-marginless" />
|
||||
{{/if}}
|
||||
{{#each @model.operationFormFields as |group|}}
|
||||
{{#each-in group as |groupName fieldsInGroup|}}
|
||||
<InfoTableRow @alwaysRender={{true}} @label={{groupName}} @value={{true}}>
|
||||
<div>
|
||||
{{#each fieldsInGroup as |field|}}
|
||||
<Icon
|
||||
aria-label={{compute (action "operationEnabled") this.model field}}
|
||||
class={{compute (action "iconClass") this.model field}}
|
||||
@name={{compute (action "iconGlyph") this.model field}}
|
||||
/>
|
||||
{{field.options.label}}
|
||||
<br />
|
||||
{{/each}}
|
||||
</div>
|
||||
</InfoTableRow>
|
||||
{{/each-in}}
|
||||
{{/each}}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module OperationFieldDisplay
|
||||
* OperationFieldDisplay components are used on KMIP role show pages to display the allowed operations on that model
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <OperationFieldDisplay @model={{model}} />
|
||||
* ```
|
||||
*
|
||||
* @param model {DS.Model} - model is the KMIP role model that needs to display its allowed operations
|
||||
*
|
||||
*/
|
||||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
model: null,
|
||||
|
||||
trueOrFalseString(model, field, trueString, falseString) {
|
||||
if (model.operationAll) {
|
||||
return trueString;
|
||||
}
|
||||
if (model.operationNone) {
|
||||
return falseString;
|
||||
}
|
||||
return model[field.name] ? trueString : falseString;
|
||||
},
|
||||
|
||||
actions: {
|
||||
iconClass(model, field) {
|
||||
return this.trueOrFalseString(model, field, 'hds-foreground-success', 'hds-foreground-faint');
|
||||
},
|
||||
iconGlyph(model, field) {
|
||||
return this.trueOrFalseString(model, field, 'check-circle', 'x-square');
|
||||
},
|
||||
operationEnabled(model, field) {
|
||||
return this.trueOrFalseString(model, field, 'Enabled', 'Disabled');
|
||||
},
|
||||
},
|
||||
});
|
||||
61
ui/lib/kmip/addon/components/page/role.hbs
Normal file
61
ui/lib/kmip/addon/components/page/role.hbs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
{{!
|
||||
Copyright IBM Corp. 2016, 2025
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<HeaderCredentials @role={{@roleName}} @scope={{@scopeName}} />
|
||||
|
||||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
{{#if @capabilities.canDelete}}
|
||||
<ConfirmAction
|
||||
@buttonText="Delete role"
|
||||
class="toolbar-button"
|
||||
@buttonColor="secondary"
|
||||
@onConfirmAction={{this.deleteRole}}
|
||||
@confirmMessage="Are you sure you want to delete {{@roleName}}?"
|
||||
/>
|
||||
<div class="toolbar-separator"></div>
|
||||
{{/if}}
|
||||
|
||||
{{#if @capabilities.canUpdate}}
|
||||
<ToolbarLink @route="role.edit" @models={{array @scopeName @roleName}} data-test-kmip-link-edit-role>
|
||||
Edit role
|
||||
</ToolbarLink>
|
||||
{{/if}}
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
|
||||
<div class="box is-fullwidth is-sideless is-shadowless">
|
||||
<div class="box is-fullwidth is-shadowless is-sideless is-marginless">
|
||||
<h2 class="title is-5">TLS</h2>
|
||||
<InfoTableRow @label="TLS client key bits" @value={{@role.tls_client_key_bits}} />
|
||||
<InfoTableRow @label="TLS client key type" @value={{@role.tls_client_key_type}} />
|
||||
<InfoTableRow @label="TLS client TTL" @value={{@role.tls_client_ttl}} @formatTtl={{true}} />
|
||||
</div>
|
||||
|
||||
<div class="box is-fullwidth is-shadowless is-sideless is-marginless">
|
||||
<h2 class="title is-5">Allowed operations</h2>
|
||||
{{#if @role.operation_all}}
|
||||
<AlertInline @type="info" @message="This role allows all KMIP operations" class="is-marginless" />
|
||||
{{/if}}
|
||||
|
||||
{{! the operation-groups helper gets all the available operation keys and formats them into groups }}
|
||||
{{#each-in (operation-groups) as |groupName fields|}}
|
||||
<InfoTableRow @alwaysRender={{true}} @label={{groupName}} @value={{true}}>
|
||||
<div>
|
||||
{{#each fields as |field|}}
|
||||
{{#let (this.iconState field) as |icon|}}
|
||||
<div data-test-operation-field={{field}}>
|
||||
<Icon aria-label={{icon.label}} class={{icon.class}} @name={{icon.name}} />
|
||||
{{! operation-label helper formats the key into a human-readable label }}
|
||||
{{operation-label field}}
|
||||
<br />
|
||||
</div>
|
||||
{{/let}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</InfoTableRow>
|
||||
{{/each-in}}
|
||||
</div>
|
||||
</div>
|
||||
54
ui/lib/kmip/addon/components/page/role.ts
Normal file
54
ui/lib/kmip/addon/components/page/role.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type SecretMountPath from 'vault/services/secret-mount-path';
|
||||
import type ApiService from 'vault/services/api';
|
||||
import type { Capabilities } from 'vault/app-types';
|
||||
import type FlashMessageService from 'vault/services/flash-messages';
|
||||
import type { KmipWriteRoleRequest } from '@hashicorp/vault-client-typescript';
|
||||
|
||||
interface Args {
|
||||
role: KmipWriteRoleRequest;
|
||||
roleName: string;
|
||||
scopeName: string;
|
||||
capabilities: Capabilities;
|
||||
}
|
||||
|
||||
export default class KmipScopesPageComponent extends Component<Args> {
|
||||
@service('app-router') declare readonly router: RouterService;
|
||||
@service declare readonly secretMountPath: SecretMountPath;
|
||||
@service declare readonly api: ApiService;
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
|
||||
iconState = (field: keyof KmipWriteRoleRequest) => {
|
||||
const { operation_all, operation_none } = this.args.role;
|
||||
const isEnabled = operation_all || (!operation_none && this.args.role[field]);
|
||||
|
||||
return {
|
||||
class: isEnabled ? 'hds-foreground-success' : 'hds-foreground-faint',
|
||||
name: isEnabled ? 'check-circle' : 'x-square',
|
||||
label: isEnabled ? 'Enabled' : 'Disabled',
|
||||
};
|
||||
};
|
||||
|
||||
@action
|
||||
async deleteRole() {
|
||||
const { roleName, scopeName } = this.args;
|
||||
const { currentPath } = this.secretMountPath;
|
||||
try {
|
||||
await this.api.secrets.kmipDeleteRole(roleName, scopeName, currentPath);
|
||||
this.flashMessages.success(`Successfully deleted role ${roleName}`);
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.kmip.scope.roles', scopeName);
|
||||
} catch (error) {
|
||||
const { message } = await this.api.parseError(error);
|
||||
this.flashMessages.danger(`Error deleting role ${roleName}: ${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
117
ui/lib/kmip/addon/components/page/scope/roles.hbs
Normal file
117
ui/lib/kmip/addon/components/page/scope/roles.hbs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
{{!
|
||||
Copyright IBM Corp. 2016, 2025
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<Page::Header @title={{@scope}}>
|
||||
<:breadcrumbs>
|
||||
<KmipBreadcrumb @showPath={{true}} @currentRoute={{@scope}} />
|
||||
</:breadcrumbs>
|
||||
</Page::Header>
|
||||
|
||||
<Toolbar>
|
||||
{{#if @roles.meta.total}}
|
||||
<ToolbarFilters>
|
||||
<FilterInput
|
||||
aria-label="Filter roles by name"
|
||||
placeholder="Filter roles by name"
|
||||
@value={{@filterValue}}
|
||||
@wait={{200}}
|
||||
@onInput={{this.onFilterChange}}
|
||||
/>
|
||||
</ToolbarFilters>
|
||||
{{/if}}
|
||||
<ToolbarActions>
|
||||
<ToolbarLink @route="scope.roles.create" @type="add" data-test-role-create>
|
||||
Create role
|
||||
</ToolbarLink>
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
|
||||
{{#if @roles}}
|
||||
<div class="has-bottom-margin-s">
|
||||
{{#each @roles as |role|}}
|
||||
<div data-test-list-item={{role}}>
|
||||
<ListItem @linkPrefix={{this.mountPoint}} @linkParams={{array "credentials" @scope role}} as |Item|>
|
||||
<Item.content>
|
||||
<Icon @name="user" class="has-text-grey-light" />{{role}}
|
||||
</Item.content>
|
||||
<Item.menu>
|
||||
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
|
||||
<dd.ToggleIcon
|
||||
@icon="more-horizontal"
|
||||
@text="More options"
|
||||
@hasChevron={{false}}
|
||||
data-test-popup-menu-trigger
|
||||
/>
|
||||
<dd.Interactive @route="credentials" @models={{array @scope role}} data-test-popup-menu="View credentials">
|
||||
View credentials
|
||||
</dd.Interactive>
|
||||
<dd.Interactive @route="role" @models={{array @scope role}} data-test-popup-menu="View role">
|
||||
View role
|
||||
</dd.Interactive>
|
||||
{{#if
|
||||
(has-capability
|
||||
@capabilities
|
||||
"update"
|
||||
pathKey="kmipRole"
|
||||
params=(hash backend=this.secretMountPath.currentPath scope=@scope name=role)
|
||||
)
|
||||
}}
|
||||
<dd.Interactive @route="role.edit" @models={{array @scope role}} data-test-popup-menu="Edit role">
|
||||
Edit role
|
||||
</dd.Interactive>
|
||||
{{/if}}
|
||||
{{#if
|
||||
(has-capability
|
||||
@capabilities
|
||||
"delete"
|
||||
pathKey="kmipRole"
|
||||
params=(hash backend=this.secretMountPath.currentPath scope=@scope name=role)
|
||||
)
|
||||
}}
|
||||
<dd.Interactive
|
||||
@color="critical"
|
||||
{{on "click" (fn (mut this.roleToDelete) role)}}
|
||||
data-test-confirm-action-trigger
|
||||
>
|
||||
Delete role
|
||||
</dd.Interactive>
|
||||
{{/if}}
|
||||
</Hds::Dropdown>
|
||||
|
||||
{{#if (eq this.roleToDelete role)}}
|
||||
<ConfirmModal
|
||||
@color="critical"
|
||||
@confirmMessage="Are you sure you want to delete {{this.roleToDelete}}?"
|
||||
@onConfirm={{this.deleteRole}}
|
||||
@onClose={{fn (mut this.roleToDelete) null}}
|
||||
/>
|
||||
{{/if}}
|
||||
</Item.menu>
|
||||
</ListItem>
|
||||
</div>
|
||||
{{/each}}
|
||||
|
||||
<Hds::Pagination::Numbered
|
||||
@currentPage={{@roles.meta.currentPage}}
|
||||
@currentPageSize={{@roles.meta.pageSize}}
|
||||
@route="scope.roles"
|
||||
@showSizeSelector={{false}}
|
||||
@totalItems={{@roles.meta.filteredTotal}}
|
||||
@queryFunction={{this.paginationQueryParams}}
|
||||
data-test-pagination
|
||||
/>
|
||||
</div>
|
||||
{{else}}
|
||||
{{#if @filterValue}}
|
||||
<EmptyState @title="There are no roles matching "{{@filterValue}}"" />
|
||||
{{else}}
|
||||
<EmptyState
|
||||
@title="No roles in this scope yet"
|
||||
@message="Roles let you generate credentials with a specified set of KMIP operations permissions that clients are allowed to perform."
|
||||
>
|
||||
<Hds::Link::Standalone @icon="plus" @text="Create a role" @route="scope.roles.create" />
|
||||
</EmptyState>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
62
ui/lib/kmip/addon/components/page/scope/roles.ts
Normal file
62
ui/lib/kmip/addon/components/page/scope/roles.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { getOwner } from '@ember/owner';
|
||||
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type SecretMountPath from 'vault/services/secret-mount-path';
|
||||
import type ApiService from 'vault/services/api';
|
||||
import type { CapabilitiesMap, EngineOwner } from 'vault/app-types';
|
||||
import FlashMessageService from 'vault/services/flash-messages';
|
||||
|
||||
interface Args {
|
||||
scope: string;
|
||||
roles: string[];
|
||||
capabilities: CapabilitiesMap;
|
||||
filterValue: string | undefined;
|
||||
}
|
||||
|
||||
export default class KmipScopeRolesPageComponent extends Component<Args> {
|
||||
@service('app-router') declare readonly router: RouterService;
|
||||
@service declare readonly secretMountPath: SecretMountPath;
|
||||
@service declare readonly api: ApiService;
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
|
||||
@tracked roleToDelete: string | null = null;
|
||||
|
||||
get mountPoint() {
|
||||
return (getOwner(this) as EngineOwner).mountPoint;
|
||||
}
|
||||
|
||||
get paginationQueryParams() {
|
||||
return (page: number) => ({ page });
|
||||
}
|
||||
|
||||
@action
|
||||
onFilterChange(pageFilter: string) {
|
||||
this.router.transitionTo({ queryParams: { pageFilter } });
|
||||
}
|
||||
|
||||
@action
|
||||
async deleteRole() {
|
||||
try {
|
||||
await this.api.secrets.kmipDeleteRole(
|
||||
this.roleToDelete as string,
|
||||
this.args.scope,
|
||||
this.secretMountPath.currentPath
|
||||
);
|
||||
this.flashMessages.success(`Successfully deleted role ${this.roleToDelete}`);
|
||||
this.roleToDelete = null;
|
||||
this.router.refresh();
|
||||
} catch (error) {
|
||||
const { message } = await this.api.parseError(error);
|
||||
this.flashMessages.danger(`Error deleting role ${this.roleToDelete}: ${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
111
ui/lib/kmip/addon/components/role-form.hbs
Normal file
111
ui/lib/kmip/addon/components/role-form.hbs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
{{!
|
||||
Copyright IBM Corp. 2016, 2025
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<form {{on "submit" (perform this.save)}}>
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<MessageError @errorMessage={{this.errorMessage}} />
|
||||
<NamespaceReminder @mode="save" />
|
||||
|
||||
{{#if @form.isNew}}
|
||||
{{! only show name field for new roles }}
|
||||
<div class="field" data-test-field="name">
|
||||
<Hds::Form::TextInput::Field
|
||||
name="name"
|
||||
@id="kmip-role-name"
|
||||
@value={{this.name}}
|
||||
@isInvalid={{this.validationError}}
|
||||
disabled={{this.save.isRunning}}
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
data-test-input="name"
|
||||
{{on "input" (pipe (pick "target.value") (fn (mut this.name)))}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label data-test-form-field-label>Name</F.Label>
|
||||
{{#if this.validationError}}
|
||||
<F.Error data-test-validation-error="name">Name is required</F.Error>
|
||||
{{/if}}
|
||||
</Hds::Form::TextInput::Field>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="control is-flex box is-shadowless is-fullwidth is-marginless">
|
||||
<Hds::Form::Toggle::Field
|
||||
data-test-input="operation_none"
|
||||
checked={{not @form.data.operation_none}}
|
||||
{{on "change" this.toggleOperationNone}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>Allow this role to perform KMIP operations</F.Label>
|
||||
</Hds::Form::Toggle::Field>
|
||||
</div>
|
||||
|
||||
{{#unless @form.data.operation_none}}
|
||||
<Toolbar>
|
||||
<h3 class="title is-6 has-left-padding-s" data-test-kmip-section="Allowed Operations">
|
||||
Allowed Operations
|
||||
</h3>
|
||||
</Toolbar>
|
||||
|
||||
<div class="box">
|
||||
<FormField
|
||||
@attr={{hash name="operation_all" type="boolean" options=(hash label="Allow this role to perform all operations")}}
|
||||
@model={{@form}}
|
||||
/>
|
||||
<hr />
|
||||
|
||||
<div class="kmip-role-operations">
|
||||
{{! the operation-groups helper gets all the available operation keys and formats them into groups }}
|
||||
{{#each-in (operation-groups) as |groupName fields|}}
|
||||
<div class="kmip-role-allowed-operations">
|
||||
<h4 class="title is-7" data-test-kmip-operations={{groupName}}>
|
||||
{{groupName}}
|
||||
</h4>
|
||||
|
||||
{{#each fields as |fieldKey|}}
|
||||
{{#let (@form.fieldFor fieldKey) as |field|}}
|
||||
<div class="field" data-test-field={{field.name}}>
|
||||
{{#let (get @form.data fieldKey) as |value|}}
|
||||
<Hds::Form::Checkbox::Field
|
||||
name={{field.name}}
|
||||
checked={{or @form.data.operation_all value}}
|
||||
disabled={{@form.data.operation_all}}
|
||||
{{on "change" (fn (mut value) (not value))}}
|
||||
data-test-input={{field.name}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label data-test-form-field-label>{{field.options.label}}</F.Label>
|
||||
</Hds::Form::Checkbox::Field>
|
||||
{{/let}}
|
||||
</div>
|
||||
{{/let}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/each-in}}
|
||||
</div>
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
<div class="box is-fullwidth is-shadowless">
|
||||
<h3 class="title is-3" data-test-kmip-section="TLS">
|
||||
TLS
|
||||
</h3>
|
||||
{{#each @form.tlsFields as |field|}}
|
||||
<FormField @attr={{field}} @model={{@form}} />
|
||||
{{/each}}
|
||||
</div>
|
||||
{{#each @form.otherFields as |field|}}
|
||||
<FormField @attr={{field}} @model={{@form}} />
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
<FormSaveButtons @isSaving={{this.save.isRunning}} @onCancel={{@onCancel}}>
|
||||
<:error>
|
||||
{{#if this.invalidFormAlert}}
|
||||
<AlertInline @type="danger" class="has-top-padding-s" @message={{this.invalidFormAlert}} />
|
||||
{{/if}}
|
||||
</:error>
|
||||
</FormSaveButtons>
|
||||
</form>
|
||||
71
ui/lib/kmip/addon/components/role-form.ts
Normal file
71
ui/lib/kmip/addon/components/role-form.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
import { service } from '@ember/service';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
|
||||
import type KmipRoleForm from 'vault/forms/secrets/kmip/role';
|
||||
import type FlashMessageService from 'vault/services/flash-messages';
|
||||
import type ApiService from 'vault/services/api';
|
||||
import type SecretMountPath from 'vault/services/secret-mount-path';
|
||||
import type { HTMLElementEvent } from 'vault/forms';
|
||||
|
||||
interface Args {
|
||||
roleName: string;
|
||||
scopeName: string;
|
||||
form: KmipRoleForm;
|
||||
onSave: CallableFunction;
|
||||
onCancel: CallableFunction;
|
||||
}
|
||||
|
||||
export default class KmipRoleFormComponent extends Component<Args> {
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
@service declare readonly api: ApiService;
|
||||
@service declare readonly secretMountPath: SecretMountPath;
|
||||
|
||||
@tracked declare name: string;
|
||||
@tracked validationError = false;
|
||||
@tracked invalidFormAlert: string | null = null;
|
||||
@tracked errorMessage: string | null = null;
|
||||
|
||||
@action
|
||||
toggleOperationNone(event: HTMLElementEvent<HTMLInputElement>) {
|
||||
const { checked } = event.target;
|
||||
const { data } = this.args.form;
|
||||
data.operation_none = !checked;
|
||||
data.operation_all = checked;
|
||||
}
|
||||
|
||||
save = task(
|
||||
waitFor(async (event: HTMLElementEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const { form, roleName, scopeName } = this.args;
|
||||
|
||||
if (!form.isNew || this.name) {
|
||||
this.validationError = false;
|
||||
try {
|
||||
const { data } = form.toJSON();
|
||||
const name = form.isNew ? this.name : roleName;
|
||||
|
||||
await this.api.secrets.kmipWriteRole(name, scopeName, this.secretMountPath.currentPath, data);
|
||||
|
||||
this.flashMessages.success(`Successfully saved role ${name}`);
|
||||
this.args.onSave();
|
||||
} catch (error) {
|
||||
const { message } = await this.api.parseError(error);
|
||||
this.errorMessage = message;
|
||||
this.invalidFormAlert = 'There was an error submitting this form.';
|
||||
}
|
||||
} else {
|
||||
this.validationError = true;
|
||||
this.invalidFormAlert = 'There is an error with this form.';
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Controller from '@ember/controller';
|
||||
import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class RoleController extends Controller {
|
||||
@service flashMessages;
|
||||
@service('app-router') router;
|
||||
|
||||
@action
|
||||
async deleteRole() {
|
||||
const { id } = this.model;
|
||||
try {
|
||||
await this.model.destroyRecord();
|
||||
this.flashMessages.success(`Successfully deleted role ${id}`);
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.kmip.scope.roles', this.scope);
|
||||
} catch (e) {
|
||||
this.flashMessages.danger(`There was an error deleting the role ${id}: ${e.errors.join(' ')}`);
|
||||
this.model.rollbackAttributes();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import ListController from 'core/mixins/list-controller';
|
||||
import Controller from '@ember/controller';
|
||||
import { computed } from '@ember/object';
|
||||
import { getOwner } from '@ember/owner';
|
||||
|
||||
export default Controller.extend(ListController, {
|
||||
mountPoint: computed(function () {
|
||||
return getOwner(this).mountPoint;
|
||||
}),
|
||||
});
|
||||
32
ui/lib/kmip/addon/helpers/operation-groups.ts
Normal file
32
ui/lib/kmip/addon/helpers/operation-groups.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { KmipWriteRoleRequest, KmipWriteRoleRequestToJSONTyped } from '@hashicorp/vault-client-typescript';
|
||||
|
||||
export default function operationGroups() {
|
||||
// the client helper returns an object with all available properties from the schema
|
||||
// we could pass in a role to set the values but in this case we are only interested in the keys
|
||||
const role = KmipWriteRoleRequestToJSONTyped({} as KmipWriteRoleRequest);
|
||||
const objects = [
|
||||
'operation_create',
|
||||
'operation_activate',
|
||||
'operation_get',
|
||||
'operation_locate',
|
||||
'operation_rekey',
|
||||
'operation_revoke',
|
||||
'operation_destroy',
|
||||
];
|
||||
const attributes = ['operation_add_attribute', 'operation_get_attributes'];
|
||||
const server = ['operation_discover_versions'];
|
||||
const notOther = [...objects, ...attributes, ...server, 'operation_all', 'operation_none'];
|
||||
const other = Object.keys(role).filter((key) => key.startsWith('operation_') && !notOther.includes(key));
|
||||
|
||||
return {
|
||||
'Managed Cryptographic Objects': objects,
|
||||
'Object Attributes': attributes,
|
||||
Server: server,
|
||||
Other: other,
|
||||
};
|
||||
}
|
||||
11
ui/lib/kmip/addon/helpers/operation-label.ts
Normal file
11
ui/lib/kmip/addon/helpers/operation-label.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { capitalize } from '@ember/string';
|
||||
import type { KmipWriteRoleRequest } from '@hashicorp/vault-client-typescript';
|
||||
|
||||
export default function label(field: keyof KmipWriteRoleRequest) {
|
||||
return field.replace('operation_', '').split('_').map(capitalize).join(' ');
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
export default class KmipRoleRoute extends Route {
|
||||
@service store;
|
||||
@service secretMountPath;
|
||||
@service pathHelp;
|
||||
|
||||
beforeModel() {
|
||||
return this.pathHelp.hydrateModel('kmip/role', this.secretMountPath.currentPath);
|
||||
}
|
||||
|
||||
model(params) {
|
||||
return this.store.queryRecord('kmip/role', {
|
||||
backend: this.secretMountPath.currentPath,
|
||||
scope: params.scope_name,
|
||||
id: params.role_name,
|
||||
});
|
||||
}
|
||||
|
||||
setupController(controller) {
|
||||
super.setupController(...arguments);
|
||||
const { scope_name: scope, role_name: role } = this.paramsFor('role');
|
||||
controller.setProperties({ role, scope });
|
||||
}
|
||||
}
|
||||
40
ui/lib/kmip/addon/routes/role.ts
Normal file
40
ui/lib/kmip/addon/routes/role.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
import type ApiService from 'vault/services/api';
|
||||
import type SecretMountPath from 'vault/services/secret-mount-path';
|
||||
import type CapabilitiesService from 'vault/services/capabilities';
|
||||
|
||||
export default class KmipRoleRoute extends Route {
|
||||
@service declare readonly api: ApiService;
|
||||
@service declare readonly secretMountPath: SecretMountPath;
|
||||
@service declare readonly capabilities: CapabilitiesService;
|
||||
|
||||
async model(params: { scope_name: string; role_name: string }) {
|
||||
const { currentPath } = this.secretMountPath;
|
||||
const { scope_name, role_name } = params;
|
||||
|
||||
const { data: role } = await this.api.secrets.kmipReadRole(
|
||||
params.role_name,
|
||||
params.scope_name,
|
||||
currentPath
|
||||
);
|
||||
const capabilities = await this.capabilities.for('kmipRole', {
|
||||
backend: currentPath,
|
||||
scope: scope_name,
|
||||
name: role_name,
|
||||
});
|
||||
|
||||
return {
|
||||
role,
|
||||
roleName: role_name,
|
||||
scopeName: scope_name,
|
||||
capabilities,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
export default Route.extend({
|
||||
store: service(),
|
||||
secretMountPath: service(),
|
||||
pathHelp: service(),
|
||||
beforeModel() {
|
||||
return this.pathHelp.hydrateModel('kmip/role', this.secretMountPath.currentPath);
|
||||
},
|
||||
model() {
|
||||
const params = this.paramsFor(this.routeName);
|
||||
return this.store.queryRecord('kmip/role', {
|
||||
backend: this.secretMountPath.currentPath,
|
||||
scope: params.scope_name,
|
||||
id: params.role_name,
|
||||
});
|
||||
},
|
||||
|
||||
setupController(controller) {
|
||||
this._super(...arguments);
|
||||
const { scope_name: scope, role_name: role } = this.paramsFor(this.routeName);
|
||||
controller.setProperties({ role, scope });
|
||||
},
|
||||
});
|
||||
30
ui/lib/kmip/addon/routes/role/edit.ts
Normal file
30
ui/lib/kmip/addon/routes/role/edit.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import KmipRoleForm from 'vault/forms/secrets/kmip/role';
|
||||
|
||||
import type ApiService from 'vault/services/api';
|
||||
import type SecretMountPath from 'vault/services/secret-mount-path';
|
||||
import type { KmipWriteRoleRequest } from '@hashicorp/vault-client-typescript';
|
||||
|
||||
export default class KmipRoleEditRoute extends Route {
|
||||
@service declare readonly api: ApiService;
|
||||
@service declare readonly secretMountPath: SecretMountPath;
|
||||
|
||||
async model(params: { scope_name: string; role_name: string }) {
|
||||
const { currentPath } = this.secretMountPath;
|
||||
const { scope_name: scopeName, role_name: roleName } = params;
|
||||
|
||||
const { data: role } = await this.api.secrets.kmipReadRole(roleName, scopeName, currentPath);
|
||||
|
||||
return {
|
||||
form: new KmipRoleForm(role as KmipWriteRoleRequest),
|
||||
roleName,
|
||||
scopeName,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import ListRoute from 'core/mixins/list-route';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
export default Route.extend(ListRoute, {
|
||||
pagination: service(),
|
||||
secretMountPath: service(),
|
||||
pathHelp: service(),
|
||||
scope() {
|
||||
return this.paramsFor('scope').scope_name;
|
||||
},
|
||||
beforeModel() {
|
||||
return this.pathHelp.hydrateModel('kmip/role', this.secretMountPath.currentPath);
|
||||
},
|
||||
model(params) {
|
||||
return this.pagination
|
||||
.lazyPaginatedQuery('kmip/role', {
|
||||
backend: this.secretMountPath.currentPath,
|
||||
scope: this.scope(),
|
||||
responsePath: 'data.keys',
|
||||
page: params.page,
|
||||
pageFilter: params.pageFilter,
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.httpStatus === 404) {
|
||||
return [];
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setupController(controller) {
|
||||
this._super(...arguments);
|
||||
controller.set('scope', this.scope());
|
||||
},
|
||||
});
|
||||
69
ui/lib/kmip/addon/routes/scope/roles.ts
Normal file
69
ui/lib/kmip/addon/routes/scope/roles.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { paginate } from 'core/utils/paginate-list';
|
||||
import { KmipListRolesListEnum } from '@hashicorp/vault-client-typescript';
|
||||
|
||||
import type ApiService from 'vault/services/api';
|
||||
import type SecretMountPath from 'vault/services/secret-mount-path';
|
||||
import type Controller from '@ember/controller';
|
||||
import type CapabilitiesService from 'vault/services/capabilities';
|
||||
|
||||
interface KmipScopeRolesController extends Controller {
|
||||
pageFilter: string | undefined;
|
||||
page: number | undefined;
|
||||
}
|
||||
|
||||
export default class KmipScopeRolesRoute extends Route {
|
||||
@service declare readonly api: ApiService;
|
||||
@service declare readonly secretMountPath: SecretMountPath;
|
||||
@service declare readonly capabilities: CapabilitiesService;
|
||||
|
||||
queryParams = {
|
||||
page: {
|
||||
refreshModel: true,
|
||||
},
|
||||
pageFilter: {
|
||||
refreshModel: true,
|
||||
},
|
||||
};
|
||||
|
||||
async model(params: { page: number; pageFilter: string }) {
|
||||
const { page, pageFilter } = params;
|
||||
const { currentPath } = this.secretMountPath;
|
||||
const { scope_name: scope } = this.paramsFor('scope');
|
||||
|
||||
try {
|
||||
const { keys } = await this.api.secrets.kmipListRoles(
|
||||
scope as string,
|
||||
currentPath,
|
||||
KmipListRolesListEnum.TRUE
|
||||
);
|
||||
const roles = keys ? paginate(keys, { page: Number(page) || 1, filter: pageFilter }) : [];
|
||||
// fetch capabilities for filtered scopes
|
||||
const paths = roles.map((role) =>
|
||||
this.capabilities.pathFor('kmipRole', { backend: currentPath, scope, name: role })
|
||||
);
|
||||
const capabilities = paths ? await this.capabilities.fetch(paths) : {};
|
||||
|
||||
return { roles, capabilities, scope };
|
||||
} catch (error) {
|
||||
const { status } = await this.api.parseError(error);
|
||||
if (status === 404) {
|
||||
return { roles: [], capabilities: {}, scope };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
resetController(controller: KmipScopeRolesController, isExiting: boolean) {
|
||||
if (isExiting) {
|
||||
controller.set('pageFilter', undefined);
|
||||
controller.set('page', undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
export default Route.extend({
|
||||
store: service(),
|
||||
secretMountPath: service(),
|
||||
pathHelp: service(),
|
||||
scope() {
|
||||
return this.paramsFor('scope').scope_name;
|
||||
},
|
||||
beforeModel() {
|
||||
this.store.unloadAll('kmip/role');
|
||||
return this.pathHelp.hydrateModel('kmip/role', this.secretMountPath.currentPath);
|
||||
},
|
||||
model() {
|
||||
const model = this.store.createRecord('kmip/role', {
|
||||
backend: this.secretMountPath.currentPath,
|
||||
scope: this.scope(),
|
||||
operationAll: true,
|
||||
});
|
||||
return model;
|
||||
},
|
||||
setupController(controller) {
|
||||
this._super(...arguments);
|
||||
controller.set('scope', this.scope());
|
||||
},
|
||||
});
|
||||
17
ui/lib/kmip/addon/routes/scope/roles/create.ts
Normal file
17
ui/lib/kmip/addon/routes/scope/roles/create.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import KmipRoleForm from 'vault/forms/secrets/kmip/role';
|
||||
|
||||
export default class KmipRoleCreateRoute extends Route {
|
||||
model() {
|
||||
const { scope_name } = this.paramsFor('scope');
|
||||
return {
|
||||
scopeName: scope_name,
|
||||
form: new KmipRoleForm({ operation_all: true }, { isNew: true }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -3,32 +3,9 @@
|
|||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<HeaderCredentials @role={{this.role}} @scope={{this.scope}} />
|
||||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
{{#if this.model.updatePath.canUpdate}}
|
||||
<ConfirmAction
|
||||
@buttonText="Delete role"
|
||||
class="toolbar-button"
|
||||
@buttonColor="secondary"
|
||||
@onConfirmAction={{this.deleteRole}}
|
||||
@confirmMessage="Are you sure you want to delete {{this.model.id}}?"
|
||||
/>
|
||||
<div class="toolbar-separator"></div>
|
||||
{{/if}}
|
||||
{{#if this.model.updatePath.canUpdate}}
|
||||
<ToolbarLink @route="role.edit" @models={{array this.scope this.role}} data-test-kmip-link-edit-role>
|
||||
Edit role
|
||||
</ToolbarLink>
|
||||
{{/if}}
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
<div class="box is-fullwidth is-sideless is-shadowless">
|
||||
<FieldGroupShow @model={{this.model}} @showAllFields={{false}} />
|
||||
<div class="box is-fullwidth is-shadowless">
|
||||
<h2 class="title is-5">
|
||||
Allowed operations
|
||||
</h2>
|
||||
<OperationFieldDisplay @model={{this.model}} />
|
||||
</div>
|
||||
</div>
|
||||
<Page::Role
|
||||
@role={{this.model.role}}
|
||||
@roleName={{this.model.roleName}}
|
||||
@scopeName={{this.model.scopeName}}
|
||||
@capabilities={{this.model.capabilities}}
|
||||
/>
|
||||
|
|
@ -5,12 +5,14 @@
|
|||
|
||||
<Page::Header @title="Edit Role">
|
||||
<:breadcrumbs>
|
||||
<KmipBreadcrumb @scope={{this.scope}} @role={{this.role}} @currentRoute="Edit Role" />
|
||||
<KmipBreadcrumb @scope={{this.model.scopeName}} @role={{this.model.roleName}} @currentRoute="Edit Role" />
|
||||
</:breadcrumbs>
|
||||
</Page::Header>
|
||||
|
||||
<Kmip::RoleForm
|
||||
@model={{this.model}}
|
||||
@onSave={{transition-to "vault.cluster.secrets.backend.kmip.role" this.scope this.role}}
|
||||
@onCancel={{transition-to "vault.cluster.secrets.backend.kmip.role" this.scope this.role}}
|
||||
<RoleForm
|
||||
@form={{this.model.form}}
|
||||
@roleName={{this.model.roleName}}
|
||||
@scopeName={{this.model.scopeName}}
|
||||
@onSave={{transition-to "vault.cluster.secrets.backend.kmip.role" this.model.scopeName this.model.roleName}}
|
||||
@onCancel={{transition-to "vault.cluster.secrets.backend.kmip.role" this.model.scopeName this.model.roleName}}
|
||||
/>
|
||||
|
|
@ -3,116 +3,9 @@
|
|||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<Page::Header @title={{this.scope}}>
|
||||
<:breadcrumbs>
|
||||
<KmipBreadcrumb @showPath={{true}} @currentRoute={{this.scope}} />
|
||||
</:breadcrumbs>
|
||||
</Page::Header>
|
||||
|
||||
<Toolbar>
|
||||
{{#if this.model.meta.total}}
|
||||
<ToolbarFilters>
|
||||
<NavigateInput
|
||||
@filterFocusDidChange={{action "setFilterFocus"}}
|
||||
@filterDidChange={{action "setFilter"}}
|
||||
@filter={{this.filter}}
|
||||
@filterMatchesKey={{this.filterMatchesKey}}
|
||||
@firstPartialMatch={{this.firstPartialMatch}}
|
||||
@placeholder="Filter roles by name"
|
||||
@extraNavParams={{this.scope}}
|
||||
@urls={{hash
|
||||
create="vault.cluster.secrets.backend.kmip.scope.roles.create"
|
||||
list="vault.cluster.secrets.backend.kmip.scope.roles"
|
||||
show="vault.cluster.secrets.backend.kmip.credentials"
|
||||
}}
|
||||
/>
|
||||
{{#if this.filterFocused}}
|
||||
{{#if this.filterMatchesKey}}
|
||||
<p class="input-hint">
|
||||
<kbd>ENTER</kbd>
|
||||
to go to
|
||||
<code>{{this.filter}}</code>
|
||||
roles
|
||||
</p>
|
||||
{{/if}}
|
||||
{{#if this.firstPartialMatch}}
|
||||
<p class="input-hint">
|
||||
<kbd>TAB</kbd>
|
||||
to complete
|
||||
<code>{{this.firstPartialMatch.id}}</code>
|
||||
</p>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</ToolbarFilters>
|
||||
{{/if}}
|
||||
<ToolbarActions>
|
||||
<ToolbarLink @route="scope.roles.create" @type="add" data-test-role-create>
|
||||
Create role
|
||||
</ToolbarLink>
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
<ListView @items={{this.model}} @itemNoun="role" @paginationRouteName="scope.roles" as |list|>
|
||||
{{#if list.empty}}
|
||||
<list.empty
|
||||
@title="No roles in this scope yet"
|
||||
@message="Roles let you generate credentials with a specified set of KMIP operations permissions that clients are allowed to perform."
|
||||
>
|
||||
<Hds::Link::Standalone @icon="plus" @text="Create a role" @route="scope.roles.create" />
|
||||
</list.empty>
|
||||
{{else if list.item}}
|
||||
<ListItem @linkPrefix={{this.mountPoint}} @linkParams={{array "credentials" this.scope list.item.id}} as |Item|>
|
||||
<Item.content>
|
||||
<Icon @name="user" class="has-text-grey-light" />{{list.item.id}}
|
||||
</Item.content>
|
||||
<Item.menu>
|
||||
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
|
||||
<dd.ToggleIcon @icon="more-horizontal" @text="More options" @hasChevron={{false}} data-test-popup-menu-trigger />
|
||||
<dd.Interactive @route="credentials" @models={{array this.scope list.item.id}}>View credentials</dd.Interactive>
|
||||
<dd.Interactive @route="role" @models={{array this.scope list.item.id}}>View role</dd.Interactive>
|
||||
{{#if list.item.updatePath.isPending}}
|
||||
<dd.Generic>
|
||||
<LoadingDropdownOption />
|
||||
</dd.Generic>
|
||||
{{else}}
|
||||
{{#if list.item.updatePath.canUpdate}}
|
||||
<dd.Interactive @route="role.edit" @models={{array this.scope list.item.id}}>Edit role</dd.Interactive>
|
||||
{{/if}}
|
||||
{{#if list.item.updatePath.canDelete}}
|
||||
<dd.Interactive
|
||||
@color="critical"
|
||||
{{on "click" (fn (mut this.roleToDelete) list.item)}}
|
||||
data-test-confirm-action-trigger
|
||||
>Delete role</dd.Interactive>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</Hds::Dropdown>
|
||||
{{#if (eq this.roleToDelete list.item)}}
|
||||
<ConfirmModal
|
||||
@color="critical"
|
||||
@confirmMessage="Are you sure you want to delete {{this.roleToDelete.id}}?"
|
||||
@onConfirm={{action
|
||||
(perform
|
||||
Item.callMethod
|
||||
"destroyRecord"
|
||||
this.roleToDelete
|
||||
(concat "Successfully deleted role " this.roleToDelete.id)
|
||||
(concat "There was an error deleting the role " this.roleToDelete.id)
|
||||
(action "refresh")
|
||||
)
|
||||
}}
|
||||
@onClose={{fn (mut this.roleToDelete) null}}
|
||||
/>
|
||||
{{/if}}
|
||||
</Item.menu>
|
||||
</ListItem>
|
||||
{{else}}
|
||||
<ListItem as |Item|>
|
||||
<Item.content>
|
||||
There are no roles that match
|
||||
{{this.filter}}, press
|
||||
<kbd>ENTER</kbd>
|
||||
to add one.
|
||||
</Item.content>
|
||||
</ListItem>
|
||||
{{/if}}
|
||||
</ListView>
|
||||
<Page::Scope::Roles
|
||||
@roles={{this.model.roles}}
|
||||
@scope={{this.model.scope}}
|
||||
@capabilities={{this.model.capabilities}}
|
||||
@filterValue={{this.pageFilter}}
|
||||
/>
|
||||
|
|
@ -5,12 +5,13 @@
|
|||
|
||||
<Page::Header @title="Create a Role">
|
||||
<:breadcrumbs>
|
||||
<KmipBreadcrumb @scope={{this.scope}} @currentRoute="Create Role" />
|
||||
<KmipBreadcrumb @scope={{this.model.scopeName}} @currentRoute="Create Role" />
|
||||
</:breadcrumbs>
|
||||
</Page::Header>
|
||||
|
||||
<Kmip::RoleForm
|
||||
@model={{this.model}}
|
||||
@onSave={{transition-to "vault.cluster.secrets.backend.kmip.scope.roles" this.scope}}
|
||||
@onCancel={{transition-to "vault.cluster.secrets.backend.kmip.scope.roles" this.scope}}
|
||||
<RoleForm
|
||||
@form={{this.model.form}}
|
||||
@scopeName={{this.model.scopeName}}
|
||||
@onSave={{transition-to "vault.cluster.secrets.backend.kmip.scope.roles" this.model.scopeName}}
|
||||
@onCancel={{transition-to "vault.cluster.secrets.backend.kmip.scope.roles" this.model.scopeName}}
|
||||
/>
|
||||
|
|
@ -11,6 +11,7 @@ import {
|
|||
visit,
|
||||
waitUntil,
|
||||
find,
|
||||
findAll,
|
||||
click,
|
||||
} from '@ember/test-helpers';
|
||||
import { module, test } from 'qunit';
|
||||
|
|
@ -232,8 +233,8 @@ module('Acceptance | Enterprise | KMIP secrets', function (hooks) {
|
|||
'links to the role create form'
|
||||
);
|
||||
// check that the role form looks right
|
||||
assert.dom(GENERAL.inputByAttr('operationNone')).isChecked('allows role to perform roles by default');
|
||||
assert.dom(GENERAL.inputByAttr('operationAll')).isChecked('operationAll is checked by default');
|
||||
assert.dom(GENERAL.inputByAttr('operation_none')).isChecked('allows role to perform roles by default');
|
||||
assert.dom(GENERAL.inputByAttr('operation_all')).isChecked('operation_all is checked by default');
|
||||
assert.dom('[data-test-kmip-section]').exists({ count: 2 });
|
||||
assert.dom('[data-test-kmip-operations]').exists({ count: 4 });
|
||||
|
||||
|
|
@ -388,75 +389,87 @@ module('Acceptance | Enterprise | KMIP secrets', function (hooks) {
|
|||
this.store = this.owner.lookup('service:store');
|
||||
this.scope = 'my-scope';
|
||||
this.name = 'my-role';
|
||||
|
||||
await login();
|
||||
await runCmd(mountEngineCmd('kmip', this.backend), false);
|
||||
await runCmd([`write ${this.backend}/scope/${this.scope} -force`]);
|
||||
await rolesPage.visit({ backend: this.backend, scope: this.scope });
|
||||
this.setModel = async () => {
|
||||
|
||||
this.saveRole = async () => {
|
||||
await click(GENERAL.submitButton);
|
||||
await visit(`/vault/secrets-engines/${this.backend}/kmip/scopes/${this.scope}/roles/${this.name}`);
|
||||
this.model = this.store.peekRecord('kmip/role', this.name);
|
||||
};
|
||||
|
||||
this.iconSelector = (operation) => `[data-test-operation-field="${operation}"] svg`;
|
||||
});
|
||||
|
||||
// "operationNone" is the attr name for the 'Allow this role to perform KMIP operations' toggle
|
||||
// operationNone = false => the toggle is ON and KMIP operations are allowed
|
||||
// operationNone = true => the toggle is OFF and KMIP operations are not allowed
|
||||
test('it submits when operationNone is toggled on', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
await click('[data-test-role-create]');
|
||||
await fillIn(GENERAL.inputByAttr('role'), this.name);
|
||||
assert.dom(GENERAL.inputByAttr('operationAll')).isChecked('operationAll is checked by default');
|
||||
await this.setModel();
|
||||
assert.true(this.model.operationAll, 'operationAll is true');
|
||||
assert.strictEqual(this.model.operationNone, undefined, 'operationNone is unset');
|
||||
});
|
||||
|
||||
test('it submits when operationNone is toggled off', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
await click('[data-test-role-create]');
|
||||
await fillIn(GENERAL.inputByAttr('role'), this.name);
|
||||
await click(GENERAL.inputByAttr('operationNone'));
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('operationNone'))
|
||||
.isNotChecked('Allow this role to perform KMIP operations is toggled off');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('operationAll'))
|
||||
.doesNotExist('clicking the toggle hides KMIP operation checkboxes');
|
||||
await this.setModel();
|
||||
assert.strictEqual(this.model.operationAll, undefined, 'operationAll is unset');
|
||||
assert.true(this.model.operationNone, 'operationNone is true');
|
||||
});
|
||||
|
||||
test('it submits when operationAll is unchecked', async function (assert) {
|
||||
// "operation_none" is the field name for the 'Allow this role to perform KMIP operations' toggle
|
||||
// operation_none = false => the toggle is ON and KMIP operations are allowed
|
||||
// operation_none = true => the toggle is OFF and KMIP operations are not allowed
|
||||
test('it submits when operation_none is toggled on', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
await click('[data-test-role-create]');
|
||||
await fillIn(GENERAL.inputByAttr('role'), this.name);
|
||||
await click(GENERAL.inputByAttr('operationAll'));
|
||||
await this.setModel();
|
||||
await fillIn(GENERAL.inputByAttr('name'), this.name);
|
||||
assert.dom(GENERAL.inputByAttr('operation_all')).isChecked('operation_all is checked by default');
|
||||
await this.saveRole();
|
||||
assert
|
||||
.dom(GENERAL.inlineError)
|
||||
.hasText('This role allows all KMIP operations', 'operation_all was saved');
|
||||
});
|
||||
|
||||
assert.strictEqual(this.model.operationAll, undefined, 'operationAll is unset');
|
||||
assert.true(this.model.operationNone, 'operationNone is true');
|
||||
test('it submits when operation_none is toggled off', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
await click('[data-test-role-create]');
|
||||
await fillIn(GENERAL.inputByAttr('name'), this.name);
|
||||
await click(GENERAL.inputByAttr('operation_none'));
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('operation_none'))
|
||||
.isNotChecked('Allow this role to perform KMIP operations is toggled off');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('operation_all'))
|
||||
.doesNotExist('clicking the toggle hides KMIP operation checkboxes');
|
||||
|
||||
await this.saveRole();
|
||||
const operations = findAll('[data-test-operation-field]');
|
||||
const notAllowed = findAll('[data-test-operation-field] svg[data-test-icon="x-square"]');
|
||||
assert.strictEqual(notAllowed.length, operations.length, 'no operations are allowed');
|
||||
});
|
||||
|
||||
test('it submits when operation_all is unchecked', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
await click('[data-test-role-create]');
|
||||
await fillIn(GENERAL.inputByAttr('name'), this.name);
|
||||
await click(GENERAL.inputByAttr('operation_all'));
|
||||
await click(GENERAL.inputByAttr('operation_create'));
|
||||
await this.saveRole();
|
||||
|
||||
assert.dom(GENERAL.inlineError).doesNotExist('operation_all was not saved');
|
||||
assert
|
||||
.dom(this.iconSelector('operation_create'))
|
||||
.hasAttribute('data-test-icon', 'check-circle', 'operation_create was saved');
|
||||
});
|
||||
|
||||
test('it submits individually selected operations', async function (assert) {
|
||||
assert.expect(6);
|
||||
assert.expect(4);
|
||||
|
||||
await click('[data-test-role-create]');
|
||||
await fillIn(GENERAL.inputByAttr('role'), this.name);
|
||||
await click(GENERAL.inputByAttr('operationAll'));
|
||||
await click(GENERAL.inputByAttr('operationGet'));
|
||||
await click(GENERAL.inputByAttr('operationGetAttributes'));
|
||||
assert.dom(GENERAL.inputByAttr('operationAll')).isNotChecked();
|
||||
assert.dom(GENERAL.inputByAttr('operationCreate')).isNotChecked(); // unchecking operationAll deselects the other checkboxes
|
||||
await this.setModel();
|
||||
assert.strictEqual(this.model.operationAll, undefined, 'operationAll is unset');
|
||||
assert.strictEqual(this.model.operationNone, undefined, 'operationNone is unset');
|
||||
assert.true(this.model.operationGet, 'operationGet is true');
|
||||
assert.true(this.model.operationGetAttributes, 'operationGetAttributes is true');
|
||||
await fillIn(GENERAL.inputByAttr('name'), this.name);
|
||||
await click(GENERAL.inputByAttr('operation_all'));
|
||||
await click(GENERAL.inputByAttr('operation_get'));
|
||||
await click(GENERAL.inputByAttr('operation_get_attributes'));
|
||||
assert.dom(GENERAL.inputByAttr('operation_all')).isNotChecked();
|
||||
assert.dom(GENERAL.inputByAttr('operation_create')).isNotChecked(); // unchecking operation_all deselects the other checkboxes
|
||||
|
||||
await this.saveRole();
|
||||
assert
|
||||
.dom(this.iconSelector('operation_get'))
|
||||
.hasAttribute('data-test-icon', 'check-circle', 'operation_get was saved');
|
||||
assert
|
||||
.dom(this.iconSelector('operation_get_attributes'))
|
||||
.hasAttribute('data-test-icon', 'check-circle', 'operation_get_attributes was saved');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
180
ui/tests/integration/components/kmip/page/role-test.js
Normal file
180
ui/tests/integration/components/kmip/page/role-test.js
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { setupEngine } from 'ember-engines/test-support';
|
||||
import { click, render } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import sinon from 'sinon';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { capitalize } from '@ember/string';
|
||||
import operationGroups from 'kmip/helpers/operation-groups';
|
||||
|
||||
module('Integration | Component | kmip | Page::Role', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'kmip');
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.backend = 'kmip-test';
|
||||
this.owner.lookup('service:secret-mount-path').update(this.backend);
|
||||
|
||||
this.roleName = 'role-1';
|
||||
this.scopeName = 'scope-1';
|
||||
this.capabilities = { canDelete: true, canUpdate: true };
|
||||
|
||||
this.getRole = (allOrNone) => {
|
||||
const tlsOptions = {
|
||||
tls_client_key_bits: 521,
|
||||
tls_client_key_type: 'ec',
|
||||
tls_client_ttl: 86400,
|
||||
};
|
||||
let operations = {
|
||||
operation_activate: true,
|
||||
operation_add_attribute: true,
|
||||
operation_decrypt: true,
|
||||
operation_discover_versions: true,
|
||||
operation_import: true,
|
||||
operation_locate: true,
|
||||
operation_register: true,
|
||||
operation_revoke: true,
|
||||
};
|
||||
if (allOrNone === 'all') {
|
||||
operations = { operation_all: true };
|
||||
} else if (allOrNone === 'none') {
|
||||
operations = { operation_none: true };
|
||||
}
|
||||
return { ...tlsOptions, ...operations };
|
||||
};
|
||||
this.role = this.getRole();
|
||||
|
||||
// get all keys that are rendered in the operation groups
|
||||
this.operationKeys = Object.values(operationGroups()).flat();
|
||||
|
||||
this.apiStub = sinon.stub(this.owner.lookup('service:api').secrets, 'kmipDeleteRole').resolves();
|
||||
this.flashStub = sinon.stub(this.owner.lookup('service:flashMessages'), 'success');
|
||||
this.routerStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo');
|
||||
|
||||
this.operationState = (assert, operation, isEnabled) => {
|
||||
const iconClass = isEnabled ? 'hds-foreground-success' : 'hds-foreground-faint';
|
||||
const iconName = isEnabled ? 'check-circle' : 'x-square';
|
||||
const label = operation.replace('operation_', '').split('_').map(capitalize).join(' ');
|
||||
const state = isEnabled ? 'enabled' : 'disabled';
|
||||
|
||||
const selector = `[data-test-operation-field="${operation}"]`;
|
||||
assert
|
||||
.dom(`${selector} svg`)
|
||||
.hasClass(iconClass, `${operation} has correct icon class for ${state} state`);
|
||||
assert
|
||||
.dom(`${selector} svg`)
|
||||
.hasAttribute('data-test-icon', iconName, `${operation} has correct icon for ${state} state`);
|
||||
assert.dom(selector).containsText(label, `${operation} has correct label`);
|
||||
};
|
||||
|
||||
this.renderComponent = () =>
|
||||
render(
|
||||
hbs`<Page::Role @roleName={{this.roleName}} @scopeName={{this.scopeName}} @role={{this.role}} @capabilities={{this.capabilities}} />`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
});
|
||||
|
||||
test('it should render/hide toolbar actions based on capabilities', async function (assert) {
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom(GENERAL.confirmTrigger).hasText('Delete role', 'Delete role action renders in toolbar');
|
||||
assert.dom('[data-test-kmip-link-edit-role]').hasText('Edit role', 'Edit role action renders in toolbar');
|
||||
|
||||
this.capabilities = { canDelete: false, canUpdate: false };
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom(GENERAL.confirmTrigger).doesNotExist('Delete role action is hidden without capability');
|
||||
assert
|
||||
.dom('[data-test-kmip-link-edit-role]')
|
||||
.doesNotExist('Edit role action is hidden without capability');
|
||||
});
|
||||
|
||||
test('it should delete role', async function (assert) {
|
||||
await this.renderComponent();
|
||||
|
||||
await click(GENERAL.confirmTrigger);
|
||||
await click(GENERAL.confirmButton);
|
||||
|
||||
assert.true(
|
||||
this.apiStub.calledWith(this.roleName, this.scopeName, this.backend),
|
||||
'API called to delete role'
|
||||
);
|
||||
assert.true(
|
||||
this.flashStub.calledWith(`Successfully deleted role ${this.roleName}`),
|
||||
'Success flash message shown'
|
||||
);
|
||||
assert.true(
|
||||
this.routerStub.calledWith('vault.cluster.secrets.backend.kmip.scope.roles', this.scopeName),
|
||||
'Transitions to roles list on delete success'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should render tls fields', async function (assert) {
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom(GENERAL.infoRowValue('TLS client key bits')).hasText('521', 'TLS client key bits renders');
|
||||
assert.dom(GENERAL.infoRowValue('TLS client key type')).hasText('ec', 'TLS client key type renders');
|
||||
assert.dom(GENERAL.infoRowValue('TLS client TTL')).hasText('1 day', 'TLS client TTL renders');
|
||||
});
|
||||
|
||||
test('it should render operation groups', async function (assert) {
|
||||
await this.renderComponent();
|
||||
|
||||
assert
|
||||
.dom(GENERAL.infoRowLabel('Managed Cryptographic Objects'))
|
||||
.exists('Cypto operations group renders');
|
||||
assert.dom(GENERAL.infoRowLabel('Object Attributes')).exists('Attributes operations group renders');
|
||||
assert.dom(GENERAL.infoRowLabel('Server')).exists('Server operations group renders');
|
||||
assert.dom(GENERAL.infoRowLabel('Other')).exists('Other operations group renders');
|
||||
});
|
||||
|
||||
test('it should mark all operations as enabled when operations_all was selected', async function (assert) {
|
||||
assert.expect(this.operationKeys.length * 3 + 1);
|
||||
|
||||
this.role = this.getRole('all');
|
||||
await this.renderComponent();
|
||||
|
||||
assert
|
||||
.dom(GENERAL.inlineError)
|
||||
.hasText('This role allows all KMIP operations', 'All operations enabled message renders');
|
||||
|
||||
this.operationKeys.forEach((operation) => {
|
||||
this.operationState(assert, operation, true);
|
||||
});
|
||||
});
|
||||
|
||||
test('it should mark operations as disabled when operations_none was selected', async function (assert) {
|
||||
assert.expect(this.operationKeys.length * 3 + 1);
|
||||
|
||||
this.role = this.getRole('none');
|
||||
await this.renderComponent();
|
||||
|
||||
assert
|
||||
.dom(GENERAL.inlineError)
|
||||
.doesNotExist('All operations enabled message does not render when operations are disabled');
|
||||
|
||||
this.operationKeys.forEach((operation) => {
|
||||
this.operationState(assert, operation, false);
|
||||
});
|
||||
});
|
||||
|
||||
test('it should correctly mark operations as enabled or disabled based on selections', async function (assert) {
|
||||
assert.expect(this.operationKeys.length * 3 + 1);
|
||||
|
||||
await this.renderComponent();
|
||||
|
||||
assert
|
||||
.dom(GENERAL.inlineError)
|
||||
.doesNotExist('All operations enabled message does not render when some operations are disabled');
|
||||
|
||||
this.operationKeys.forEach((operation) => {
|
||||
this.operationState(assert, operation, this.role[operation] === true);
|
||||
});
|
||||
});
|
||||
});
|
||||
144
ui/tests/integration/components/kmip/page/scope/roles-test.js
Normal file
144
ui/tests/integration/components/kmip/page/scope/roles-test.js
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { setupEngine } from 'ember-engines/test-support';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { click, fillIn, render } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import sinon from 'sinon';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { getErrorResponse } from 'vault/tests/helpers/api/error-response';
|
||||
|
||||
module('Integration | Component | kmip | Page::Scope::Roles', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'kmip');
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.backend = 'kmip-test';
|
||||
this.owner.lookup('service:secret-mount-path').update(this.backend);
|
||||
|
||||
const { secrets } = this.owner.lookup('service:api');
|
||||
this.apiStub = sinon.stub(secrets, 'kmipDeleteRole').resolves();
|
||||
|
||||
const flash = this.owner.lookup('service:flashMessages');
|
||||
this.flashSuccessStub = sinon.stub(flash, 'success');
|
||||
this.flashDangerStub = sinon.stub(flash, 'danger');
|
||||
|
||||
const router = this.owner.lookup('service:router');
|
||||
this.transitionStub = sinon.stub(router, 'transitionTo');
|
||||
this.refreshStub = sinon.stub(router, 'refresh');
|
||||
|
||||
this.scope = 'scope-1';
|
||||
this.roles = ['role-1', 'role-2'];
|
||||
this.roles.meta = {
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
filteredTotal: this.roles.length,
|
||||
total: this.roles.length,
|
||||
};
|
||||
this.filterValue = '';
|
||||
|
||||
const { pathFor } = this.owner.lookup('service:capabilities');
|
||||
this.capabilities = this.roles.reduce((capabilities, name) => {
|
||||
const path = pathFor('kmipRole', { backend: this.backend, scope: this.scope, name });
|
||||
const hasPermission = name === 'role-1'; // only role-1 has permissions to test conditional rendering
|
||||
capabilities[path] = { canDelete: hasPermission, canUpdate: hasPermission };
|
||||
return capabilities;
|
||||
}, {});
|
||||
|
||||
this.renderComponent = () =>
|
||||
render(
|
||||
hbs`<Page::Scope::Roles @roles={{this.roles}} @scope={{this.scope}} @capabilities={{this.capabilities}} @filterValue={{this.filterValue}} />`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
});
|
||||
|
||||
test('it should render filter and create action in toolbar', async function (assert) {
|
||||
await this.renderComponent();
|
||||
assert.dom(GENERAL.filterInput).exists('Renders filter input in toolbar');
|
||||
assert.dom('[data-test-role-create]').exists('Renders create role action in toolbar');
|
||||
});
|
||||
|
||||
test('it should populate filter with arg value', async function (assert) {
|
||||
this.filterValue = 'role-1';
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom(GENERAL.filterInput).hasValue('role-1', 'Renders filter input with correct value');
|
||||
});
|
||||
|
||||
test('it should filter list items', async function (assert) {
|
||||
await this.renderComponent();
|
||||
|
||||
await fillIn(GENERAL.filterInput, 'role-1');
|
||||
assert.true(
|
||||
this.transitionStub.calledWith({ queryParams: { pageFilter: 'role-1' } }),
|
||||
'Transitions with correct query param on page filter change'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should render list items', async function (assert) {
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom(GENERAL.listItem()).exists({ count: this.roles.length }, 'Renders correct number of roles');
|
||||
assert.dom(GENERAL.listItem('role-1')).containsText('role-1', 'Renders role name in list item');
|
||||
|
||||
await click(`${GENERAL.listItem('role-1')} ${GENERAL.menuTrigger}`);
|
||||
assert.dom(GENERAL.menuItem('View credentials')).exists('Renders View credentials action in more menu');
|
||||
assert.dom(GENERAL.menuItem('View role')).exists('Renders View role action in more menu');
|
||||
assert.dom(GENERAL.menuItem('Edit role')).exists('Renders Edit role action in more menu');
|
||||
assert
|
||||
.dom(`${GENERAL.listItem('role-1')} ${GENERAL.confirmTrigger}`)
|
||||
.exists('Renders Delete action in more menu');
|
||||
|
||||
await click(`${GENERAL.listItem('role-2')} ${GENERAL.menuTrigger}`);
|
||||
assert
|
||||
.dom(GENERAL.menuItem('Edit role'))
|
||||
.doesNotExist('Edit role action does not render with no update capability');
|
||||
assert
|
||||
.dom(`${GENERAL.listItem('role-2')} ${GENERAL.confirmTrigger}`)
|
||||
.doesNotExist('Delete action does not render with no delete capability');
|
||||
});
|
||||
|
||||
test('it should delete role', async function (assert) {
|
||||
await this.renderComponent();
|
||||
|
||||
await click(`${GENERAL.listItem('role-1')} ${GENERAL.menuTrigger}`);
|
||||
await click(`${GENERAL.listItem('role-1')} ${GENERAL.confirmTrigger}`);
|
||||
await click(GENERAL.confirmButton);
|
||||
|
||||
assert.true(this.apiStub.calledWith('role-1', this.scope, this.backend), 'Calls API to delete role');
|
||||
assert.true(
|
||||
this.flashSuccessStub.calledWith('Successfully deleted role role-1'),
|
||||
'Shows success flash message'
|
||||
);
|
||||
assert.true(this.refreshStub.called, 'Refreshes the route on delete success');
|
||||
});
|
||||
|
||||
test('it should handle delete error', async function (assert) {
|
||||
const error = 'An error occurred deleting the role';
|
||||
this.apiStub.rejects(getErrorResponse({ errors: [error] }, 500));
|
||||
|
||||
await this.renderComponent();
|
||||
await click(`${GENERAL.listItem('role-1')} ${GENERAL.menuTrigger}`);
|
||||
await click(`${GENERAL.listItem('role-1')} ${GENERAL.confirmTrigger}`);
|
||||
await click(GENERAL.confirmButton);
|
||||
|
||||
assert.true(
|
||||
this.flashDangerStub.calledWith(`Error deleting role role-1: ${error}`),
|
||||
'Shows flash message on delete error'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should render pagination controls', async function (assert) {
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom(GENERAL.pagination).exists('Renders pagination controls');
|
||||
assert.dom(GENERAL.paginationInfo).hasText('1–2 of 2', 'Renders correct pagination info');
|
||||
assert.dom(GENERAL.paginationSizeSelector).doesNotExist('Pagination size selector does not render');
|
||||
});
|
||||
});
|
||||
238
ui/tests/integration/components/kmip/role-form-test.js
Normal file
238
ui/tests/integration/components/kmip/role-form-test.js
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { setupEngine } from 'ember-engines/test-support';
|
||||
import { click, fillIn, render } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import sinon from 'sinon';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import KmipRoleForm from 'vault/forms/secrets/kmip/role';
|
||||
import operationGroups from 'kmip/helpers/operation-groups';
|
||||
|
||||
module('Integration | Component | kmip | RoleForm', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'kmip');
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.backend = 'kmip-test';
|
||||
this.owner.lookup('service:secret-mount-path').update(this.backend);
|
||||
|
||||
this.roleName = '';
|
||||
this.scopeName = 'scope-1';
|
||||
|
||||
this.tlsOptions = {
|
||||
tls_client_key_bits: 521,
|
||||
tls_client_key_type: 'ec',
|
||||
tls_client_ttl: '86400',
|
||||
};
|
||||
|
||||
this.setForm = (isNew = true, allOrNone) => {
|
||||
let operations = {
|
||||
operation_activate: true,
|
||||
operation_add_attribute: true,
|
||||
operation_decrypt: true,
|
||||
operation_discover_versions: true,
|
||||
operation_import: true,
|
||||
operation_locate: true,
|
||||
operation_register: true,
|
||||
operation_revoke: true,
|
||||
};
|
||||
if (allOrNone === 'all') {
|
||||
operations = { operation_all: true };
|
||||
} else if (allOrNone === 'none') {
|
||||
operations = { operation_none: true };
|
||||
}
|
||||
const role = isNew ? { operation_all: true } : { ...this.tlsOptions, ...operations };
|
||||
this.form = new KmipRoleForm(role, { isNew });
|
||||
};
|
||||
|
||||
this.onSave = sinon.spy();
|
||||
this.onCancel = sinon.spy();
|
||||
|
||||
// get all keys that are rendered in the operation groups
|
||||
this.operationKeys = Object.values(operationGroups()).flat();
|
||||
|
||||
this.apiStub = sinon.stub(this.owner.lookup('service:api').secrets, 'kmipWriteRole').resolves();
|
||||
this.flashStub = sinon.stub(this.owner.lookup('service:flashMessages'), 'success');
|
||||
this.routerStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo');
|
||||
|
||||
this.renderComponent = (isNew, allOrNone) => {
|
||||
this.setForm(isNew, allOrNone);
|
||||
// set the roleName for existing forms
|
||||
this.roleName = isNew ? '' : 'role-1';
|
||||
return render(
|
||||
hbs`<RoleForm @roleName={{this.roleName}} @scopeName={{this.scopeName}} @form={{this.form}} @onSave={{this.onSave}} @onCancel={{this.onCancel}} />`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
test('it should render name field and default to allow all for new roles', async function (assert) {
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom(GENERAL.inputByAttr('name')).exists('Name field is rendered for new role');
|
||||
assert.dom(GENERAL.inputByAttr('operation_none')).isChecked('Operations are allowed by default');
|
||||
assert.dom(GENERAL.inputByAttr('operation_all')).isChecked('Allow all operations is checked by default');
|
||||
});
|
||||
|
||||
test('it should hide operations when none is toggled', async function (assert) {
|
||||
await this.renderComponent();
|
||||
|
||||
assert
|
||||
.dom('[data-test-kmip-section="Allowed Operations"]')
|
||||
.exists('Allowed Operations section is rendered');
|
||||
await click(GENERAL.inputByAttr('operation_none'));
|
||||
assert
|
||||
.dom('[data-test-kmip-section="Allowed Operations"]')
|
||||
.doesNotExist('Allowed Operations section is hidden');
|
||||
});
|
||||
|
||||
test('it should check all operations when allow all is selected', async function (assert) {
|
||||
await this.renderComponent();
|
||||
|
||||
this.operationKeys.forEach((key) => {
|
||||
assert.dom(GENERAL.inputByAttr(key)).isChecked(`${key} is checked when allow all is selected`);
|
||||
});
|
||||
});
|
||||
|
||||
test('it should trigger onCancel callback', async function (assert) {
|
||||
await this.renderComponent();
|
||||
await click(GENERAL.cancelButton);
|
||||
assert.true(this.onCancel.calledOnce, 'onCancel callback is triggered on cancel');
|
||||
});
|
||||
|
||||
module('Editing existing role', function () {
|
||||
test('it should hide name field when editing', async function (assert) {
|
||||
await this.renderComponent(false);
|
||||
|
||||
assert.dom(GENERAL.inputByAttr('name')).doesNotExist('Name field is hidden when editing');
|
||||
});
|
||||
|
||||
test('it should populate fields when editing: operation_none', async function (assert) {
|
||||
await this.renderComponent(false, 'none');
|
||||
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('operation_none'))
|
||||
.isNotChecked('Renders correct toggle state for operation_none');
|
||||
assert
|
||||
.dom('[data-test-kmip-section="Allowed Operations"]')
|
||||
.doesNotExist('Allowed Operations section is hidden');
|
||||
});
|
||||
|
||||
test('it should populate fields when editing: operation_all', async function (assert) {
|
||||
await this.renderComponent(false, 'all');
|
||||
|
||||
assert.dom(GENERAL.inputByAttr('operation_none')).isChecked('Allow operations toggle is checked');
|
||||
assert.dom(GENERAL.inputByAttr('operation_all')).isChecked('Allow all operations is checked');
|
||||
this.operationKeys.forEach((key) => {
|
||||
assert.dom(GENERAL.inputByAttr(key)).isChecked(`${key} is checked when allow all is selected`);
|
||||
});
|
||||
});
|
||||
|
||||
test('it should populate fields when editing: selected operations', async function (assert) {
|
||||
await this.renderComponent(false);
|
||||
|
||||
this.operationKeys.forEach((key) => {
|
||||
const domMethod = this.form.data[key] === true ? 'isChecked' : 'isNotChecked';
|
||||
assert.dom(GENERAL.inputByAttr(key))[domMethod](`${key} ${domMethod} correctly`);
|
||||
});
|
||||
});
|
||||
|
||||
test('it should populate tls fields', async function (assert) {
|
||||
// this is a bit wonky but when the value is set by the component it's a string
|
||||
// but in order to populate it , it needs to be a number
|
||||
this.tlsOptions.tls_client_ttl = 86400;
|
||||
await this.renderComponent(false);
|
||||
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('tls_client_key_bits'))
|
||||
.hasValue('521', 'TLS field is populated correctly');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('tls_client_key_type'))
|
||||
.hasValue('ec', 'TLS field is populated correctly');
|
||||
assert
|
||||
.dom(GENERAL.ttl.input('TLS Client TTL'))
|
||||
.hasValue('1', 'TLS TTL value field is populated correctly');
|
||||
assert.dom(GENERAL.selectByAttr('ttl-unit')).hasValue('d', 'TLS TTL unit field is populated correctly');
|
||||
});
|
||||
|
||||
test('it should save edited role', async function (assert) {
|
||||
await this.renderComponent(false);
|
||||
await click(GENERAL.inputByAttr('operation_none'));
|
||||
await click(GENERAL.submitButton);
|
||||
|
||||
const payload = { ...this.tlsOptions, operation_none: true };
|
||||
assert.true(this.apiStub.calledWith(this.roleName, this.scopeName, this.backend, payload));
|
||||
assert.true(this.flashStub.calledWith(`Successfully saved role ${this.roleName}`));
|
||||
assert.true(this.onSave.calledOnce, 'onSave callback is triggered on save');
|
||||
});
|
||||
});
|
||||
|
||||
module('Create new role', function () {
|
||||
test('it should show validation error when name is not provided', async function (assert) {
|
||||
await this.renderComponent();
|
||||
await click(GENERAL.submitButton);
|
||||
|
||||
assert
|
||||
.dom(GENERAL.validationErrorByAttr('name'))
|
||||
.hasText('Name is required', 'Shows validation error for name field');
|
||||
assert.dom(GENERAL.inlineError).hasText('There is an error with this form.');
|
||||
});
|
||||
|
||||
test('it should create new role without operations', async function (assert) {
|
||||
await this.renderComponent();
|
||||
await fillIn(GENERAL.inputByAttr('name'), 'new-role');
|
||||
await click(GENERAL.inputByAttr('operation_none'));
|
||||
await click(GENERAL.submitButton);
|
||||
|
||||
const payload = { ...this.tlsOptions, operation_none: true };
|
||||
assert.true(
|
||||
this.apiStub.calledWith('new-role', this.scopeName, this.backend, payload),
|
||||
'Role created with no operations'
|
||||
);
|
||||
assert.true(this.flashStub.calledWith('Successfully saved role new-role'));
|
||||
assert.true(this.onSave.calledOnce, 'onSave callback is triggered on save');
|
||||
});
|
||||
|
||||
test('it should create new role with all operations', async function (assert) {
|
||||
await this.renderComponent();
|
||||
await fillIn(GENERAL.inputByAttr('name'), 'new-role');
|
||||
await click(GENERAL.submitButton);
|
||||
|
||||
const payload = { ...this.tlsOptions, operation_all: true };
|
||||
assert.true(
|
||||
this.apiStub.calledWith('new-role', this.scopeName, this.backend, payload),
|
||||
'Role created with all operations'
|
||||
);
|
||||
assert.true(this.flashStub.calledWith('Successfully saved role new-role'));
|
||||
assert.true(this.onSave.calledOnce, 'onSave callback is triggered on save');
|
||||
});
|
||||
|
||||
test('it should create new role with selected operations', async function (assert) {
|
||||
await this.renderComponent();
|
||||
await fillIn(GENERAL.inputByAttr('name'), 'new-role');
|
||||
await click(GENERAL.inputByAttr('operation_all'));
|
||||
await click(GENERAL.inputByAttr('operation_decrypt'));
|
||||
await click(GENERAL.inputByAttr('operation_create'));
|
||||
await click(GENERAL.inputByAttr('operation_get_attributes'));
|
||||
await click(GENERAL.submitButton);
|
||||
|
||||
const payload = {
|
||||
...this.tlsOptions,
|
||||
operation_decrypt: true,
|
||||
operation_create: true,
|
||||
operation_get_attributes: true,
|
||||
};
|
||||
assert.true(
|
||||
this.apiStub.calledWith('new-role', this.scopeName, this.backend, payload),
|
||||
'Role created with all operations'
|
||||
);
|
||||
assert.true(this.flashStub.calledWith('Successfully saved role new-role'));
|
||||
assert.true(this.onSave.calledOnce, 'onSave callback is triggered on save');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -11,7 +11,7 @@ export default create({
|
|||
visit: visitable('/vault/secrets-engines/:backend/kmip/scopes/:scope/roles'),
|
||||
visitDetail: visitable('/vault/secrets-engines/:backend/kmip/scopes/:scope/roles/:role'),
|
||||
create: clickable('[data-test-role-create]'),
|
||||
roleName: fillable('[data-test-input="role"]'),
|
||||
roleName: fillable('[data-test-input="name"]'),
|
||||
detailEditLink: clickable('[data-test-kmip-link-edit-role]'),
|
||||
cancelLink: clickable('[data-test-edit-form-cancel]'),
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue