From 1044886d84be250ddecd0c352410687b1d2f9cc2 Mon Sep 17 00:00:00 2001 From: "clairebontempo@gmail.com" Date: Wed, 14 Feb 2024 15:38:00 -0800 Subject: [PATCH 1/6] update adapter to accept :type in url --- ui/app/adapters/transform.js | 18 ++--- ui/tests/unit/adapters/transform-test.js | 95 ++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 9 deletions(-) create mode 100644 ui/tests/unit/adapters/transform-test.js diff --git a/ui/app/adapters/transform.js b/ui/app/adapters/transform.js index 1846341050f6..1346c5559430 100644 --- a/ui/app/adapters/transform.js +++ b/ui/app/adapters/transform.js @@ -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; }, diff --git a/ui/tests/unit/adapters/transform-test.js b/ui/tests/unit/adapters/transform-test.js new file mode 100644 index 000000000000..e642e3140509 --- /dev/null +++ b/ui/tests/unit/adapters/transform-test.js @@ -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.adapterFor('transform').query(); + }); + + 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(); + } + }); +}); From 946f050aef878578872e0e9a24dabb289658bc44 Mon Sep 17 00:00:00 2001 From: "clairebontempo@gmail.com" Date: Wed, 14 Feb 2024 15:38:47 -0800 Subject: [PATCH 2/6] update model attributes to include deletion_allowed and tokenization type --- ui/app/models/transform.js | 47 +++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/ui/app/models/transform.js b/ui/app/models/transform.js index 09656bd7bc62..94654fed8e44 100644 --- a/ui/app/models/transform.js +++ b/ui/app/models/transform.js @@ -20,6 +20,10 @@ const TYPES = [ value: 'masking', displayName: 'Masking', }, + { + value: 'tokenization', + displayName: 'Tokenization', + }, ]; const TWEAK_SOURCE = [ @@ -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', { + 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 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); }), From 2ee986008b137d2b1e50fad3a40c108680be5c55 Mon Sep 17 00:00:00 2001 From: "clairebontempo@gmail.com" Date: Wed, 14 Feb 2024 15:41:10 -0800 Subject: [PATCH 3/6] update max_ttl text --- ui/app/models/transform.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/models/transform.js b/ui/app/models/transform.js index 94654fed8e44..81768a69fb89 100644 --- a/ui/app/models/transform.js +++ b/ui/app/models/transform.js @@ -111,7 +111,7 @@ export default Model.extend({ max_ttl: attr({ editType: 'ttl', defaultValue: '0', - label: 'Maximum TTL of a token', + label: 'Maximum TTL (time-to-live) of a token', helperTextDisabled: 'If "0" or unspecified, tokens may have no expiration.', }), From 68acfab733b75f37510fcb179ab3f2b98eeb4151 Mon Sep 17 00:00:00 2001 From: "clairebontempo@gmail.com" Date: Wed, 14 Feb 2024 15:46:28 -0800 Subject: [PATCH 4/6] update adapter test --- ui/app/adapters/transform.js | 2 +- ui/tests/unit/adapters/transform-test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/app/adapters/transform.js b/ui/app/adapters/transform.js index 1346c5559430..619ecfd65db0 100644 --- a/ui/app/adapters/transform.js +++ b/ui/app/adapters/transform.js @@ -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; } diff --git a/ui/tests/unit/adapters/transform-test.js b/ui/tests/unit/adapters/transform-test.js index e642e3140509..eea263af13a7 100644 --- a/ui/tests/unit/adapters/transform-test.js +++ b/ui/tests/unit/adapters/transform-test.js @@ -29,7 +29,7 @@ module('Unit | Adapter | transform', function (hooks) { assert.propEqual(req.queryParams, { list: 'true' }, 'query params include list: true'); return { data: { key_info: {}, keys: [] } }; }); - await this.store.adapterFor('transform').query(); + await this.store.query('transform', { backend: this.backend }); }); test('it should make request to correct endpoint when querying a record', async function (assert) { From 145fd655c7baa9e1e3ecba9e3c8bc5af5f021abb Mon Sep 17 00:00:00 2001 From: "clairebontempo@gmail.com" Date: Wed, 14 Feb 2024 15:53:36 -0800 Subject: [PATCH 5/6] add changelog; --- changelog/25436.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog/25436.txt diff --git a/changelog/25436.txt b/changelog/25436.txt new file mode 100644 index 000000000000..132af39f1466 --- /dev/null +++ b/changelog/25436.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: Add `deletion_allowed` param to transformations and include `tokenization` as a type option +``` From 31a3fab8c6d9b20502fe4d8a315304ea0de8aa4b Mon Sep 17 00:00:00 2001 From: "clairebontempo@gmail.com" Date: Thu, 15 Feb 2024 08:35:30 -0800 Subject: [PATCH 6/6] update comment --- ui/app/models/transform.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ui/app/models/transform.js b/ui/app/models/transform.js index 81768a69fb89..e60d17f571dc 100644 --- a/ui/app/models/transform.js +++ b/ui/app/models/transform.js @@ -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 = [ {