UI: add deletion_allowed to transform, add tokenization transform type (#25436)

* update adapter to accept :type in url

* update model attributes to include deletion_allowed and tokenization type

* update max_ttl text

* update adapter test

* add changelog;

* update comment
This commit is contained in:
claire bontempo
2024-02-16 09:07:52 -08:00
committed by GitHub
parent 22db889bce
commit cbe09c76a2
4 changed files with 155 additions and 17 deletions

3
changelog/25436.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:improvement
ui: Add `deletion_allowed` param to transformations and include `tokenization` as a type option
```

View File

@@ -11,11 +11,11 @@ import { encodePath } from 'vault/utils/path-encoding-helpers';
export default ApplicationAdapter.extend({
namespace: 'v1',
createOrUpdate(store, type, snapshot) {
const { backend, name } = snapshot.record;
const serializer = store.serializerFor(type.modelName);
createOrUpdate(store, { modelName }, snapshot) {
const { backend, name, type } = snapshot.record;
const serializer = store.serializerFor(modelName);
const data = serializer.serialize(snapshot);
const url = this.urlForTransformations(backend, name);
const url = this.urlForTransformations(backend, name, type);
return this.ajax(url, 'POST', { data }).then((resp) => {
const response = resp || {};
@@ -41,11 +41,11 @@ export default ApplicationAdapter.extend({
return 'transform';
},
urlForTransformations(backend, id) {
let url = `${this.buildURL()}/${encodePath(backend)}/transformation`;
if (id) {
url = url + '/' + encodePath(id);
}
urlForTransformations(backend, id, type) {
const base = `${this.buildURL()}/${encodePath(backend)}`;
// when type exists, transformations is plural
const url = type ? `${base}/transformations/${type}` : `${base}/transformation`;
if (id) return `${url}/${encodePath(id)}`;
return url;
},
@@ -62,7 +62,7 @@ export default ApplicationAdapter.extend({
const queryAjax = this.ajax(this.urlForTransformations(backend, id), 'GET', this.optionsForQuery(id));
return allSettled([queryAjax]).then((results) => {
// query result 404d, so throw the adapterError
// query result 404, so throw the adapterError
if (!results[0].value) {
throw results[0].reason;
}

View File

@@ -8,8 +8,7 @@ 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
// 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 = [
{
@@ -20,6 +19,10 @@ const TYPES = [
value: 'masking',
displayName: 'Masking',
},
{
value: 'tokenization',
displayName: 'Tokenization',
},
];
const TWEAK_SOURCE = [
@@ -83,12 +86,49 @@ export default Model.extend({
subText: 'Search for an existing role, type a new role to create it, or use a wildcard (*).',
wildcardLabel: 'role',
}),
transformAttrs: computed('type', function () {
if (this.type === 'masking') {
return ['name', 'type', 'masking_character', 'template', 'allowed_roles'];
}
return ['name', 'type', 'tweak_source', 'template', 'allowed_roles'];
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);
}),

View File

@@ -0,0 +1,95 @@
/**
* Copyright (c) HashiCorp, Inc.
* 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();
}
});
});