mirror of
https://github.com/hashicorp/vault.git
synced 2026-06-14 20:04:13 -04:00
Backport [UI] Ember Data Migration - Transform secrets engine code cleanup | VAULT-45710 into ce/main (#15307)
* no-op commit * clean up transform-related components and add tests for alphabet-edit and transformation-edit * fixed failing tests * removed redundant test file --------- Co-authored-by: Mohit Ojha <mohit.ojha@hashicorp.com>
This commit is contained in:
parent
6e6396a65b
commit
b14c6ea83f
16 changed files with 331 additions and 621 deletions
|
|
@ -2,7 +2,7 @@
|
|||
Copyright IBM Corp. 2016, 2025
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
<Page::Header @title={{this.title}}>
|
||||
<Page::Header @title={{this.title}} @subtitle={{this.subtitle}}>
|
||||
<:breadcrumbs>
|
||||
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
|
||||
</:breadcrumbs>
|
||||
|
|
@ -12,7 +12,13 @@
|
|||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
{{#if @capabilities.canDelete}}
|
||||
<Hds::Button @text="Delete alphabet" @color="secondary" class="toolbar-button" {{on "click" this.onDelete}} />
|
||||
<Hds::Button
|
||||
@text="Delete alphabet"
|
||||
@color="secondary"
|
||||
class="toolbar-button"
|
||||
data-test-delete
|
||||
{{on "click" this.onDelete}}
|
||||
/>
|
||||
<div class="toolbar-separator"></div>
|
||||
{{/if}}
|
||||
{{#if @capabilities.canUpdate}}
|
||||
|
|
|
|||
|
|
@ -29,29 +29,39 @@ export default class AlphabetEditComponent extends Component {
|
|||
@tracked errorMessage = '';
|
||||
|
||||
get breadcrumbs() {
|
||||
// ideally this is created on the controller in the parent route but this is a generic route and adding breadcrumbs to the controller requires a larger refactor.
|
||||
const backend = this.args.form?.data?.backend;
|
||||
const name = this.args.form?.data?.name;
|
||||
return [
|
||||
{ label: 'Vault', route: 'vault.cluster.dashboard', icon: 'vault' },
|
||||
{ label: 'Secrets engines', route: 'vault.cluster.secrets.backends' },
|
||||
{
|
||||
label: backend,
|
||||
route: 'vault.cluster.secrets.backend.list-root',
|
||||
model: backend,
|
||||
query: { tab: 'alphabet' },
|
||||
},
|
||||
{ label: 'Alphabet' },
|
||||
{ label: this.title },
|
||||
{ label: this.args?.mode === 'create' ? 'alphabet' : name },
|
||||
];
|
||||
}
|
||||
|
||||
get title() {
|
||||
if (this.args?.mode === 'create') {
|
||||
return 'Create Alphabet';
|
||||
return 'Create alphabet';
|
||||
} else if (this.args?.mode === 'edit') {
|
||||
return 'Edit Alphabet';
|
||||
return 'Edit alphabet';
|
||||
} else {
|
||||
return this.args?.form?.data?.name;
|
||||
return 'Alphabet';
|
||||
}
|
||||
}
|
||||
|
||||
get subtitle() {
|
||||
if (this.args?.mode === 'show') {
|
||||
return this.args.form?.data?.name;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
transition(route = 'show') {
|
||||
this.errorMessage = '';
|
||||
const { backend, name } = this.args.form.data;
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
{{!
|
||||
Copyright IBM Corp. 2016, 2025
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<form onsubmit={{action "createOrUpdate" "create"}}>
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<MessageError @model={{this.model}} />
|
||||
<NamespaceReminder @mode={{this.mode}} @noun="transformation" />
|
||||
{{#each this.model.transformFieldAttrs as |attr|}}
|
||||
<FormField data-test-field @attr={{attr}} @model={{this.model}} />
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="field is-grouped-split box is-fullwidth is-bottomless">
|
||||
<Hds::ButtonSet>
|
||||
<Hds::Button @text="Create transformation" type="submit" data-test-submit />
|
||||
<Hds::Button
|
||||
@text="Cancel"
|
||||
@color="secondary"
|
||||
@route="vault.cluster.secrets.backend.list-root"
|
||||
@model={{this.model.backend}}
|
||||
/>
|
||||
</Hds::ButtonSet>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import TransformationEdit from './transformation-edit';
|
||||
|
||||
export default TransformationEdit.extend({});
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { service } from '@ember/service';
|
||||
import { or } from '@ember/object/computed';
|
||||
import { isBlank } from '@ember/utils';
|
||||
import Component from '@ember/component';
|
||||
import { set } from '@ember/object';
|
||||
import FocusOnInsertMixin from 'vault/mixins/focus-on-insert';
|
||||
|
||||
const LIST_ROOT_ROUTE = 'vault.cluster.secrets.backend.list-root';
|
||||
const SHOW_ROUTE = 'vault.cluster.secrets.backend.show';
|
||||
|
||||
export const addToList = (list, itemToAdd) => {
|
||||
if (!list || !Array.isArray(list)) return list;
|
||||
list.push(itemToAdd);
|
||||
return list.uniq();
|
||||
};
|
||||
|
||||
export const removeFromList = (list, itemToRemove) => {
|
||||
if (!list) return list;
|
||||
const index = list.indexOf(itemToRemove);
|
||||
if (index < 0) return list;
|
||||
const newList = list.removeAt(index, 1);
|
||||
return newList.uniq();
|
||||
};
|
||||
|
||||
/**
|
||||
* @type Class
|
||||
*/
|
||||
export default Component.extend(FocusOnInsertMixin, {
|
||||
store: service(),
|
||||
flashMessages: service(),
|
||||
router: service(),
|
||||
|
||||
mode: null,
|
||||
onDataChange() {},
|
||||
onRefresh() {},
|
||||
model: null,
|
||||
requestInFlight: or('model.isLoading', 'model.isReloading', 'model.isSaving'),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.set('backendType', 'transform');
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
if (this.model && this.model.isError && !this.model.isDestroyed && !this.model.isDestroying) {
|
||||
this.model.rollbackAttributes();
|
||||
}
|
||||
this._super(...arguments);
|
||||
},
|
||||
|
||||
transitionToRoute() {
|
||||
this.router.transitionTo(...arguments);
|
||||
},
|
||||
|
||||
modelPrefixFromType(modelType) {
|
||||
let modelPrefix = '';
|
||||
if (modelType && modelType.startsWith('transform/')) {
|
||||
modelPrefix = `${modelType.replace('transform/', '')}/`;
|
||||
}
|
||||
return modelPrefix;
|
||||
},
|
||||
|
||||
listTabFromType(modelType) {
|
||||
let tab;
|
||||
if (modelType && modelType.startsWith('transform/')) {
|
||||
tab = `${modelType.replace('transform/', '')}`;
|
||||
}
|
||||
return tab;
|
||||
},
|
||||
|
||||
persist(method, successCallback) {
|
||||
const model = this.model;
|
||||
return model[method]()
|
||||
.then(() => {
|
||||
successCallback(model);
|
||||
})
|
||||
.catch((e) => {
|
||||
model.set('displayErrors', e.errors);
|
||||
throw e;
|
||||
});
|
||||
},
|
||||
|
||||
applyDelete(callback = () => {}) {
|
||||
const tab = this.listTabFromType(this.model.constructor.modelName);
|
||||
this.persist('destroyRecord', () => {
|
||||
this.hasDataChanges();
|
||||
callback();
|
||||
this.transitionToRoute(LIST_ROOT_ROUTE, { queryParams: { tab } });
|
||||
});
|
||||
},
|
||||
|
||||
applyChanges(type, callback = () => {}) {
|
||||
const modelId = this.model.id || this.model.name; // transform comes in as model.name
|
||||
const modelPrefix = this.modelPrefixFromType(this.model.constructor.modelName);
|
||||
// prevent from submitting if there's no key
|
||||
// maybe do something fancier later
|
||||
if (type === 'create' && isBlank(modelId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.persist('save', () => {
|
||||
this.hasDataChanges();
|
||||
callback();
|
||||
this.transitionToRoute(SHOW_ROUTE, `${modelPrefix}${modelId}`);
|
||||
});
|
||||
},
|
||||
|
||||
hasDataChanges() {
|
||||
this.onDataChange(this.model?.hasDirtyAttributes);
|
||||
},
|
||||
|
||||
actions: {
|
||||
createOrUpdate(type, event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.applyChanges(type);
|
||||
},
|
||||
|
||||
setValue(key, event) {
|
||||
set(this.model, key, event.target.checked);
|
||||
},
|
||||
|
||||
refresh() {
|
||||
this.onRefresh();
|
||||
},
|
||||
|
||||
delete() {
|
||||
this.applyDelete();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
{{!
|
||||
Copyright IBM Corp. 2016, 2025
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<form onsubmit={{action "createOrUpdate" "create"}}>
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<MessageError @model={{this.model}} />
|
||||
<NamespaceReminder @mode={{this.mode}} @noun="transformation" />
|
||||
{{#each this.model.transformFieldAttrs as |attr|}}
|
||||
{{#if (or (eq attr.name "name") (eq attr.name "type"))}}
|
||||
<label for={{attr.name}} class="is-label">
|
||||
{{attr.options.label}}
|
||||
</label>
|
||||
{{#if attr.options.subText}}
|
||||
<p class="sub-text">{{attr.options.subText}}</p>
|
||||
{{/if}}
|
||||
{{#if attr.options.possibleValues}}
|
||||
<div class="control is-expanded field is-readOnly">
|
||||
<div class="select is-fullwidth">
|
||||
<select name={{attr.name}} id={{attr.name}} disabled data-test-input={{attr.name}}>
|
||||
<option selected={{get this.model attr.name}} value={{get this.model attr.name}}>
|
||||
{{get this.model attr.name}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<input
|
||||
data-test-input={{attr.name}}
|
||||
id={{attr.name}}
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
value={{or (get this.model attr.name) attr.options.defaultValue}}
|
||||
readonly
|
||||
class="field input is-readOnly"
|
||||
type={{attr.type}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<FormField data-test-field @attr={{attr}} @model={{this.model}} />
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="field is-grouped-split box is-fullwidth is-bottomless">
|
||||
<Hds::ButtonSet>
|
||||
<Hds::Button @text="Save" type="submit" data-test-submit />
|
||||
<Hds::Button
|
||||
@text="Cancel"
|
||||
@color="secondary"
|
||||
@route="vault.cluster.secrets.backend.show"
|
||||
@models={{array this.model.backend this.model.id}}
|
||||
/>
|
||||
</Hds::ButtonSet>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import TransformationEdit from './transformation-edit';
|
||||
|
||||
export default TransformationEdit.extend();
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
{{!
|
||||
Copyright IBM Corp. 2016, 2025
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
|
||||
{{#each this.model.transformFieldAttrs as |attr|}}
|
||||
{{#if (eq attr.type "object")}}
|
||||
<InfoTableRow
|
||||
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
|
||||
@value={{stringify (get this.model attr.name)}}
|
||||
/>
|
||||
{{else if (eq attr.type "array")}}
|
||||
<InfoTableRow
|
||||
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
|
||||
@value={{get this.model attr.name}}
|
||||
@type={{attr.type}}
|
||||
@isLink={{eq attr.name "allowed_roles"}}
|
||||
@queryParam="role"
|
||||
@arrayOptions={{if (eq attr.name "allowed_roles") @transformRoles null}}
|
||||
@wildcardLabel={{if (eq attr.name "allowed_roles") attr.options.wildcardLabel null}}
|
||||
/>
|
||||
{{else}}
|
||||
<InfoTableRow
|
||||
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
|
||||
@value={{get this.model attr.name}}
|
||||
@type={{attr.type}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
<div class="has-top-margin-xl has-bottom-margin-s">
|
||||
<label class="title has-border-bottom-light page-header">CLI Commands</label>
|
||||
<div class="has-bottom-margin-s">
|
||||
<h2 class="title is-6">Encode</h2>
|
||||
<div class="has-bottom-margin-s">
|
||||
<span class="helper-text has-text-grey">
|
||||
To test the encoding capability of your transformation, use the following command. It will output an encoded_value.
|
||||
</span>
|
||||
</div>
|
||||
<div class="copy-text level">
|
||||
{{#let (concat "vault write " this.model.backend "/encode/" this.cliCommand) as |copyEncodeCommand|}}
|
||||
<Hds::CodeBlock
|
||||
@language="bash"
|
||||
@hasLineNumbers={{false}}
|
||||
@hasLineWrapping={{true}}
|
||||
@hasCopyButton={{true}}
|
||||
@value={{copyEncodeCommand}}
|
||||
/>
|
||||
{{/let}}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="title is-6">Decode</h2>
|
||||
<div class="has-bottom-margin-s">
|
||||
<span class="helper-text has-text-grey">
|
||||
To test decoding capability of your transformation, use the encoded_value in the following command. It should return
|
||||
your original input.
|
||||
</span>
|
||||
</div>
|
||||
<div class="copy-text level">
|
||||
{{#let (concat "vault write " this.model.backend "/decode/" this.cliCommand) as |copyDecodeCommand|}}
|
||||
<Hds::CodeBlock
|
||||
@language="bash"
|
||||
@hasLineNumbers={{false}}
|
||||
@hasLineWrapping={{true}}
|
||||
@hasCopyButton={{true}}
|
||||
@value={{copyDecodeCommand}}
|
||||
/>
|
||||
{{/let}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import TransformBase from './transform-edit-base';
|
||||
import { computed } from '@ember/object';
|
||||
|
||||
export default TransformBase.extend({
|
||||
cliCommand: computed('model.{allowed_roles,type,tweak_source}', function () {
|
||||
if (!this.model) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { type, allowed_roles, tweak_source, name } = this.model;
|
||||
const wildCardRole = allowed_roles.find((role) => role.includes('*'));
|
||||
|
||||
// values to be returned
|
||||
let role = '<choose a role>';
|
||||
const value = 'value=<enter your value here>';
|
||||
let tweak = '';
|
||||
|
||||
// determine the role
|
||||
if (allowed_roles.length === 1 && !wildCardRole) {
|
||||
role = allowed_roles[0];
|
||||
}
|
||||
// determine the tweak_source
|
||||
if (type === 'fpe' && tweak_source === 'supplied') {
|
||||
tweak = 'tweak=<enter your tweak>';
|
||||
}
|
||||
|
||||
return `${role} ${value} ${tweak} transformation=${name}`;
|
||||
}),
|
||||
});
|
||||
|
|
@ -49,24 +49,27 @@ export default class TransformTemplateEditComponent extends Component {
|
|||
}
|
||||
|
||||
get breadcrumbs() {
|
||||
// ideally this is created on the controller in the parent route but this is a generic route and adding breadcrumbs to the controller requires a larger refactor.
|
||||
const backend = this.args.form?.data?.backend;
|
||||
const name = this.args.form?.data?.name;
|
||||
return [
|
||||
{ label: 'Vault', route: 'vault.cluster.dashboard', icon: 'vault' },
|
||||
{ label: 'Secrets engines', route: 'vault.cluster.secrets.backends' },
|
||||
{
|
||||
label: backend,
|
||||
route: 'vault.cluster.secrets.backend.list-root',
|
||||
model: backend,
|
||||
query: { tab: 'template' },
|
||||
},
|
||||
{ label: 'Template' },
|
||||
{ label: this.title },
|
||||
{ label: this.args.mode === 'create' ? 'template' : name },
|
||||
];
|
||||
}
|
||||
|
||||
get title() {
|
||||
if (this.args.mode === 'create') {
|
||||
return 'Create Template';
|
||||
return 'Create template';
|
||||
} else if (this.args.mode === 'edit') {
|
||||
return 'Edit Template';
|
||||
return 'Edit template';
|
||||
} else {
|
||||
return 'Template';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -275,13 +275,19 @@ export default class TransformationEditComponent extends Component {
|
|||
max_ttl,
|
||||
stores,
|
||||
});
|
||||
this.flashMessages.success('Transformation saved.');
|
||||
await this.handleRoleSync(name, backend);
|
||||
this.transition();
|
||||
} catch (e) {
|
||||
const { message } = await this.api.parseError(e);
|
||||
this.errorMessage = message;
|
||||
return;
|
||||
}
|
||||
|
||||
this.flashMessages.success('Transformation saved.');
|
||||
|
||||
// handleRoleSync handles its own errors internally with flash messages;
|
||||
// guard against any unexpected throws to ensure navigation always happens after a successful save.
|
||||
await this.handleRoleSync(name, backend).catch(() => {});
|
||||
|
||||
this.transition();
|
||||
}
|
||||
|
||||
@action async onDelete() {
|
||||
|
|
|
|||
|
|
@ -1,140 +0,0 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Model, { attr } from '@ember-data/model';
|
||||
import { computed } from '@ember/object';
|
||||
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
|
||||
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
|
||||
|
||||
// these arrays define the order in which the fields will be displayed, see:
|
||||
// https://developer.hashicorp.com/vault/api-docs/secret/transform#create-update-transformation-deprecated-1-6
|
||||
const TYPES = [
|
||||
{
|
||||
value: 'fpe',
|
||||
displayName: 'Format Preserving Encryption (FPE)',
|
||||
},
|
||||
{
|
||||
value: 'masking',
|
||||
displayName: 'Masking',
|
||||
},
|
||||
{
|
||||
value: 'tokenization',
|
||||
displayName: 'Tokenization',
|
||||
},
|
||||
];
|
||||
|
||||
const TWEAK_SOURCE = [
|
||||
{
|
||||
value: 'supplied',
|
||||
displayName: 'supplied',
|
||||
},
|
||||
{
|
||||
value: 'generated',
|
||||
displayName: 'generated',
|
||||
},
|
||||
{
|
||||
value: 'internal',
|
||||
displayName: 'internal',
|
||||
},
|
||||
];
|
||||
|
||||
export default Model.extend({
|
||||
name: attr('string', {
|
||||
// CBS TODO: make this required for making a transformation
|
||||
label: 'Name',
|
||||
readOnly: true,
|
||||
subText: 'The name for your transformation. This cannot be edited later.',
|
||||
}),
|
||||
type: attr('string', {
|
||||
defaultValue: 'fpe',
|
||||
label: 'Type',
|
||||
possibleValues: TYPES,
|
||||
subText:
|
||||
'Vault provides two types of transformations: Format Preserving Encryption (FPE) is reversible, while Masking is not. This cannot be edited later.',
|
||||
}),
|
||||
tweak_source: attr('string', {
|
||||
defaultValue: 'supplied',
|
||||
label: 'Tweak source',
|
||||
possibleValues: TWEAK_SOURCE,
|
||||
subText: `A tweak value is used when performing FPE transformations. This can be supplied, generated, or internal.`, // CBS TODO: I do not include the link here. Need to figure out the best way to approach this.
|
||||
}),
|
||||
masking_character: attr('string', {
|
||||
characterLimit: 1,
|
||||
defaultValue: '*',
|
||||
label: 'Masking character',
|
||||
subText: 'Specify which character you’d like to mask your data.',
|
||||
}),
|
||||
template: attr('array', {
|
||||
editType: 'searchSelect',
|
||||
isSectionHeader: true,
|
||||
fallbackComponent: 'string-list',
|
||||
label: 'Template', // CBS TODO: make this required for making a transformation
|
||||
models: ['transform/template'],
|
||||
selectLimit: 1,
|
||||
onlyAllowExisting: true,
|
||||
subText:
|
||||
'Templates allow Vault to determine what and how to capture the value to be transformed. Type to use an existing template or create a new one.',
|
||||
}),
|
||||
allowed_roles: attr('array', {
|
||||
editType: 'searchSelect',
|
||||
isSectionHeader: true,
|
||||
label: 'Allowed roles',
|
||||
fallbackComponent: 'string-list',
|
||||
models: ['transform/role'],
|
||||
subText: 'Search for an existing role, type a new role to create it, or use a wildcard (*).',
|
||||
wildcardLabel: 'role',
|
||||
}),
|
||||
deletion_allowed: attr('boolean', {
|
||||
label: 'Allow deletion',
|
||||
subText:
|
||||
'If checked, this transform can be deleted otherwise deletion is blocked. Note that deleting the transform deletes the underlying key which makes decoding of tokenized values impossible without restoring from a backup.',
|
||||
}),
|
||||
convergent: attr('boolean', {
|
||||
label: 'Use convergent tokenization',
|
||||
subText:
|
||||
"This cannot be edited later. If checked, tokenization of the same plaintext more than once results in the same token. Defaults to false as unique tokens are more desirable from a security standpoint if there isn't a use-case need for convergence.",
|
||||
}),
|
||||
stores: attr('array', {
|
||||
label: 'Stores',
|
||||
editType: 'stringArray',
|
||||
subText:
|
||||
"The list of tokenization stores to use for tokenization state. Vault's internal storage is used by default.",
|
||||
}),
|
||||
mapping_mode: attr('string', {
|
||||
defaultValue: 'default',
|
||||
subText:
|
||||
'Specifies the mapping mode for stored tokenization values. "default" is strongly recommended for highest security, "exportable" allows for all plaintexts to be decoded via the export-decoded endpoint in an emergency.',
|
||||
}),
|
||||
max_ttl: attr({
|
||||
editType: 'ttl',
|
||||
defaultValue: '0',
|
||||
label: 'Maximum TTL (time-to-live) of a token',
|
||||
helperTextDisabled: 'If "0" or unspecified, tokens may have no expiration.',
|
||||
}),
|
||||
|
||||
transformAttrs: computed('type', function () {
|
||||
// allowed_roles not included so it displays at the bottom of the form
|
||||
const baseAttrs = ['name', 'type', 'deletion_allowed'];
|
||||
switch (this.type) {
|
||||
case 'fpe':
|
||||
return [...baseAttrs, 'tweak_source', 'template', 'allowed_roles'];
|
||||
case 'masking':
|
||||
return [...baseAttrs, 'masking_character', 'template', 'allowed_roles'];
|
||||
case 'tokenization':
|
||||
return [...baseAttrs, 'mapping_mode', 'convergent', 'max_ttl', 'stores', 'allowed_roles'];
|
||||
default:
|
||||
return [...baseAttrs];
|
||||
}
|
||||
}),
|
||||
|
||||
transformFieldAttrs: computed('transformAttrs', function () {
|
||||
return expandAttributeMeta(this, this.transformAttrs);
|
||||
}),
|
||||
|
||||
backend: attr('string', {
|
||||
readOnly: true,
|
||||
}),
|
||||
updatePath: lazyCapabilities(apiPath`${'backend'}/transformation/${'id'}`, 'backend', 'id'),
|
||||
});
|
||||
106
ui/tests/integration/components/alphabet-edit-test.ts
Normal file
106
ui/tests/integration/components/alphabet-edit-test.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2026
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||
import { render, click, findAll } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import AlphabetForm from 'vault/forms/transform/alphabet';
|
||||
import sinon from 'sinon';
|
||||
import type ApiService from 'vault/services/api';
|
||||
|
||||
module('Integration | Component | alphabet-edit', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
const router = this.owner.lookup('service:router') as unknown as Record<string, unknown>;
|
||||
router['transitionTo'] = sinon.stub();
|
||||
|
||||
this.set('capabilities', {
|
||||
canDelete: true,
|
||||
canUpdate: true,
|
||||
canRead: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('it renders in show mode', async function (assert) {
|
||||
this.set(
|
||||
'form',
|
||||
new AlphabetForm(
|
||||
{
|
||||
name: 'my-alphabet',
|
||||
alphabet: 'abcdefghijklmnopqrstuvwxyz',
|
||||
backend: 'transform',
|
||||
},
|
||||
{ isNew: false }
|
||||
)
|
||||
);
|
||||
this.set('mode', 'show');
|
||||
|
||||
await render(
|
||||
hbs`<AlphabetEdit @form={{this.form}} @capabilities={{this.capabilities}} @mode={{this.mode}} />`
|
||||
);
|
||||
|
||||
assert.dom('[data-test-edit-link]').exists('renders toolbar edit link');
|
||||
assert.dom('[data-test-field]').doesNotExist('does not render form fields in show mode');
|
||||
});
|
||||
|
||||
test('it renders in create mode', async function (assert) {
|
||||
this.set('form', new AlphabetForm({ backend: 'transform' }, { isNew: true }));
|
||||
this.set('mode', 'create');
|
||||
|
||||
await render(
|
||||
hbs`<AlphabetEdit @form={{this.form}} @capabilities={{this.capabilities}} @mode={{this.mode}} />`
|
||||
);
|
||||
|
||||
assert.dom('[data-test-submit]').exists('renders submit button');
|
||||
assert.dom('[data-test-submit]').hasText('Create alphabet');
|
||||
const fields = findAll('[data-test-field]');
|
||||
assert.strictEqual(fields.length, 2, 'renders name and alphabet fields');
|
||||
});
|
||||
|
||||
test('it renders in edit mode', async function (assert) {
|
||||
this.set(
|
||||
'form',
|
||||
new AlphabetForm(
|
||||
{
|
||||
name: 'my-alphabet',
|
||||
alphabet: 'abcdefghijklmnopqrstuvwxyz',
|
||||
backend: 'transform',
|
||||
},
|
||||
{ isNew: false }
|
||||
)
|
||||
);
|
||||
this.set('mode', 'edit');
|
||||
|
||||
await render(
|
||||
hbs`<AlphabetEdit @form={{this.form}} @capabilities={{this.capabilities}} @mode={{this.mode}} />`
|
||||
);
|
||||
|
||||
assert.dom('[data-test-submit]').exists('renders submit button');
|
||||
assert.dom('[data-test-submit]').hasText('Save');
|
||||
assert.dom('[data-test-input="name"]').hasAttribute('readonly', '', 'name is readonly in edit mode');
|
||||
});
|
||||
|
||||
test('it calls onDelete and transitions to list', async function (assert) {
|
||||
const api = this.owner.lookup('service:api') as unknown as ApiService;
|
||||
const deleteStub = sinon.stub(api.secrets, 'transformDeleteAlphabet').resolves();
|
||||
|
||||
this.set('form', new AlphabetForm({ name: 'my-alphabet', backend: 'transform' }, { isNew: false }));
|
||||
this.set('mode', 'show');
|
||||
|
||||
await render(
|
||||
hbs`<AlphabetEdit @form={{this.form}} @capabilities={{this.capabilities}} @mode={{this.mode}} />`
|
||||
);
|
||||
|
||||
await click('[data-test-delete]');
|
||||
|
||||
assert.ok(
|
||||
deleteStub.calledWith('my-alphabet', 'transform'),
|
||||
'calls transformDeleteAlphabet with correct args'
|
||||
);
|
||||
deleteStub.restore();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | transform-edit-base', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
// Set any properties with this.set('myProperty', 'value');
|
||||
// Handle any actions with this.set('myAction', function(val) { ... });
|
||||
|
||||
await render(hbs`<TransformEditBase />`);
|
||||
|
||||
assert.dom(this.element).hasText('');
|
||||
|
||||
// Template block usage:
|
||||
await render(hbs`
|
||||
<TransformEditBase>
|
||||
template block text
|
||||
</TransformEditBase>
|
||||
`);
|
||||
|
||||
assert.dom(this.element).hasText('template block text');
|
||||
});
|
||||
});
|
||||
186
ui/tests/integration/components/transformation-edit-test.ts
Normal file
186
ui/tests/integration/components/transformation-edit-test.ts
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2026
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||
import { render, click, fillIn, findAll } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import TransformationForm from 'vault/forms/transform/transformation';
|
||||
import sinon from 'sinon';
|
||||
import type ApiService from 'vault/services/api';
|
||||
|
||||
module('Integration | Component | transformation-edit', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
const router = this.owner.lookup('service:router') as unknown as Record<string, unknown>;
|
||||
router['transitionTo'] = sinon.stub();
|
||||
|
||||
this.set('capabilities', {
|
||||
canDelete: true,
|
||||
canUpdate: true,
|
||||
canRead: true,
|
||||
});
|
||||
|
||||
// Stub list fetches called in constructor to avoid real API calls
|
||||
const api = this.owner.lookup('service:api') as unknown as ApiService;
|
||||
sinon.stub(api.secrets, 'transformListRoles').resolves({ keys: [] });
|
||||
sinon.stub(api.secrets, 'transformListTemplates').resolves({ keys: [] });
|
||||
});
|
||||
|
||||
hooks.afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
test('it renders in show mode', async function (assert) {
|
||||
this.set(
|
||||
'form',
|
||||
new TransformationForm(
|
||||
{
|
||||
name: 'my-transformation',
|
||||
type: 'fpe',
|
||||
allowed_roles: [],
|
||||
backend: 'transform',
|
||||
},
|
||||
{ isNew: false }
|
||||
)
|
||||
);
|
||||
this.set('mode', 'show');
|
||||
|
||||
await render(
|
||||
hbs`<TransformationEdit @form={{this.form}} @capabilities={{this.capabilities}} @mode={{this.mode}} />`
|
||||
);
|
||||
|
||||
assert.dom('[data-test-edit-link]').exists('renders toolbar edit link');
|
||||
assert.dom('[data-test-field]').doesNotExist('does not render form fields in show mode');
|
||||
});
|
||||
|
||||
test('it renders in create mode with fpe type', async function (assert) {
|
||||
this.set('form', new TransformationForm({ backend: 'transform', type: 'fpe' }, { isNew: true }));
|
||||
this.set('mode', 'create');
|
||||
|
||||
await render(
|
||||
hbs`<TransformationEdit @form={{this.form}} @capabilities={{this.capabilities}} @mode={{this.mode}} />`
|
||||
);
|
||||
|
||||
assert.dom('[data-test-submit]').exists('renders submit button');
|
||||
assert.dom('[data-test-submit]').hasText('Create transformation');
|
||||
// fpe: name, type, deletion_allowed, tweak_source (FormField) + template, allowed_roles (SearchSelect with data-test-field)
|
||||
const fields = findAll('[data-test-field]');
|
||||
assert.strictEqual(fields.length, 6, 'renders 6 fields for fpe type (4 FormField + 2 SearchSelect)');
|
||||
});
|
||||
|
||||
test('it renders masking-specific field for masking type', async function (assert) {
|
||||
this.set('form', new TransformationForm({ backend: 'transform', type: 'masking' }, { isNew: true }));
|
||||
this.set('mode', 'create');
|
||||
|
||||
await render(
|
||||
hbs`<TransformationEdit @form={{this.form}} @capabilities={{this.capabilities}} @mode={{this.mode}} />`
|
||||
);
|
||||
|
||||
// masking: name, type, deletion_allowed, masking_character (FormField) + template, allowed_roles (SearchSelect with data-test-field)
|
||||
const fields = findAll('[data-test-field]');
|
||||
assert.strictEqual(fields.length, 6, 'renders 6 fields for masking type (4 FormField + 2 SearchSelect)');
|
||||
assert.dom('[data-test-input="masking_character"]').exists('renders masking_character field');
|
||||
assert
|
||||
.dom('[data-test-input="tweak_source"]')
|
||||
.doesNotExist('does not render tweak_source for masking type');
|
||||
});
|
||||
|
||||
test('it renders tokenization-specific fields for tokenization type', async function (assert) {
|
||||
this.set('form', new TransformationForm({ backend: 'transform', type: 'tokenization' }, { isNew: true }));
|
||||
this.set('mode', 'create');
|
||||
|
||||
await render(
|
||||
hbs`<TransformationEdit @form={{this.form}} @capabilities={{this.capabilities}} @mode={{this.mode}} />`
|
||||
);
|
||||
|
||||
// tokenization shows: name, type, deletion_allowed, mapping_mode, convergent, max_ttl, stores, allowed_roles (SearchSelect)
|
||||
assert.dom('[data-test-input="mapping_mode"]').exists('renders mapping_mode field');
|
||||
assert.dom('[data-test-input="convergent"]').exists('renders convergent field');
|
||||
assert.dom('[data-test-input="max_ttl"]').exists('renders max_ttl field');
|
||||
assert
|
||||
.dom('[data-test-input="tweak_source"]')
|
||||
.doesNotExist('does not render tweak_source for tokenization type');
|
||||
assert
|
||||
.dom('[data-test-input="masking_character"]')
|
||||
.doesNotExist('does not render masking_character for tokenization type');
|
||||
});
|
||||
|
||||
test('it renders in edit mode with name readonly', async function (assert) {
|
||||
this.set(
|
||||
'form',
|
||||
new TransformationForm(
|
||||
{
|
||||
name: 'my-transformation',
|
||||
type: 'fpe',
|
||||
allowed_roles: [],
|
||||
backend: 'transform',
|
||||
},
|
||||
{ isNew: false }
|
||||
)
|
||||
);
|
||||
this.set('mode', 'edit');
|
||||
|
||||
await render(
|
||||
hbs`<TransformationEdit @form={{this.form}} @capabilities={{this.capabilities}} @mode={{this.mode}} />`
|
||||
);
|
||||
|
||||
assert.dom('[data-test-submit]').hasText('Save');
|
||||
assert.dom('[data-test-input="name"]').hasAttribute('readonly', '', 'name is readonly in edit mode');
|
||||
});
|
||||
|
||||
test('it calls onDelete and transitions to list', async function (assert) {
|
||||
const api = this.owner.lookup('service:api') as unknown as ApiService;
|
||||
const deleteStub = sinon.stub(api.secrets, 'transformDeleteTransformation').resolves();
|
||||
|
||||
this.set(
|
||||
'form',
|
||||
new TransformationForm(
|
||||
{ name: 'my-transformation', type: 'fpe', allowed_roles: [], backend: 'transform' },
|
||||
{ isNew: false }
|
||||
)
|
||||
);
|
||||
this.set('mode', 'show');
|
||||
|
||||
await render(
|
||||
hbs`<TransformationEdit @form={{this.form}} @capabilities={{this.capabilities}} @mode={{this.mode}} />`
|
||||
);
|
||||
|
||||
await click('[data-test-delete]');
|
||||
await fillIn('[data-test-confirmation-modal-input="Delete transformation"]', 'my-transformation');
|
||||
await click('[data-test-confirm-button="Delete transformation"]');
|
||||
|
||||
assert.ok(
|
||||
deleteStub.calledWith('my-transformation', 'transform'),
|
||||
'calls transformDeleteTransformation with correct args'
|
||||
);
|
||||
});
|
||||
|
||||
test('it shows edit warning modal when transformation has allowed roles', async function (assert) {
|
||||
this.set(
|
||||
'form',
|
||||
new TransformationForm(
|
||||
{
|
||||
name: 'my-transformation',
|
||||
type: 'fpe',
|
||||
allowed_roles: ['my-role'],
|
||||
backend: 'transform',
|
||||
},
|
||||
{ isNew: false }
|
||||
)
|
||||
);
|
||||
this.set('mode', 'show');
|
||||
|
||||
await render(
|
||||
hbs`<TransformationEdit @form={{this.form}} @capabilities={{this.capabilities}} @mode={{this.mode}} />`
|
||||
);
|
||||
|
||||
await click('[data-test-edit-link]');
|
||||
|
||||
assert.dom('#transformation-edit-modal').exists('shows edit warning modal when transformation has roles');
|
||||
assert.dom('[data-test-edit-confirm-button]').exists('renders confirm button in modal');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
|
||||
const TRANSFORM_TYPES = ['fpe', 'masking', 'tokenization'];
|
||||
module('Unit | Adapter | transform', function (hooks) {
|
||||
setupTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.backend = 'my-transform-engine';
|
||||
this.name = 'my-transform';
|
||||
});
|
||||
|
||||
hooks.afterEach(function () {
|
||||
this.store.unloadAll('transform');
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoint when querying all records', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.server.get(`${this.backend}/transformation`, (schema, req) => {
|
||||
assert.ok(true, 'GET request made to correct endpoint when querying record');
|
||||
assert.propEqual(req.queryParams, { list: 'true' }, 'query params include list: true');
|
||||
return { data: { key_info: {}, keys: [] } };
|
||||
});
|
||||
await this.store.query('transform', { backend: this.backend });
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoint when querying a record', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.get(`${this.backend}/transformation/${this.name}`, () => {
|
||||
assert.ok(true, 'GET request made to correct endpoint when querying record');
|
||||
return { data: { backend: this.backend, name: this.name } };
|
||||
});
|
||||
await this.store.queryRecord('transform', { backend: this.backend, id: this.name });
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoint when creating new record', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
for (const type of TRANSFORM_TYPES) {
|
||||
const name = `transform-${type}-test`;
|
||||
this.server.post(`${this.backend}/transformations/${type}/${name}`, () => {
|
||||
assert.ok(true, `POST request made to transformations/${type}/:name creating a record`);
|
||||
return { data: { backend: this.backend, name, type } };
|
||||
});
|
||||
const record = this.store.createRecord('transform', { backend: this.backend, name, type });
|
||||
await record.save();
|
||||
}
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoint when updating record', async function (assert) {
|
||||
assert.expect(3);
|
||||
for (const type of TRANSFORM_TYPES) {
|
||||
const name = `transform-${type}-test`;
|
||||
this.server.post(`${this.backend}/transformations/${type}/${name}`, () => {
|
||||
assert.ok(true, `POST request made to transformations/${type}/:name endpoint`);
|
||||
});
|
||||
this.store.pushPayload('transform', {
|
||||
modelName: 'transform',
|
||||
backend: this.backend,
|
||||
id: name,
|
||||
type,
|
||||
name,
|
||||
});
|
||||
const record = this.store.peekRecord('transform', name);
|
||||
await record.save();
|
||||
}
|
||||
});
|
||||
|
||||
test('it should make request to correct endpoint when deleting record', async function (assert) {
|
||||
assert.expect(3);
|
||||
for (const type of TRANSFORM_TYPES) {
|
||||
const name = `transform-${type}-test`;
|
||||
this.server.delete(`${this.backend}/transformation/${name}`, () => {
|
||||
assert.ok(true, `type: ${type} - DELETE request to transformation/:name endpoint`);
|
||||
});
|
||||
this.store.pushPayload('transform', {
|
||||
modelName: 'transform',
|
||||
backend: this.backend,
|
||||
id: name,
|
||||
type,
|
||||
name,
|
||||
});
|
||||
const record = this.store.peekRecord('transform', name);
|
||||
await record.destroyRecord();
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue