Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 6 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/25436.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
ui: Add `deletion_allowed` param to transformations and include `tokenization` as a type option
```
20 changes: 10 additions & 10 deletions ui/app/adapters/transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 || {};
Expand All @@ -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)}`;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find url easier to read this way, but I can revert to url = url + '/' + encodePath(id); if folks prefer that! I don't have strong feelings 😄

return url;
},

Expand All @@ -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;
}
Expand Down
47 changes: 44 additions & 3 deletions ui/app/models/transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ const TYPES = [
value: 'masking',
displayName: 'Masking',
},
{
value: 'tokenization',
displayName: 'Tokenization',
},
];

const TWEAK_SOURCE = [
Expand Down Expand Up @@ -83,12 +87,49 @@ export default Model.extend({
subText: 'Search for an existing role, type a new role to create it, or use a wildcard (*).',
wildcardLabel: 'role',
}),
deletion_allowed: attr('boolean', {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrote these params based on the docs - so a second set of eyes checking them for typos/accuracy would be helpful!

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 () {
if (this.type === 'masking') {
return ['name', 'type', 'masking_character', 'template', 'allowed_roles'];
// 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];
}
return ['name', 'type', 'tweak_source', 'template', 'allowed_roles'];
}),

transformFieldAttrs: computed('transformAttrs', function () {
return expandAttributeMeta(this, this.transformAttrs);
}),
Expand Down
95 changes: 95 additions & 0 deletions ui/tests/unit/adapters/transform-test.js
Original file line number Diff line number Diff line change
@@ -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();
}
});
});
Loading