mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-30 18:17:55 +00:00
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:
3
changelog/25436.txt
Normal file
3
changelog/25436.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
```release-note:improvement
|
||||||
|
ui: Add `deletion_allowed` param to transformations and include `tokenization` as a type option
|
||||||
|
```
|
||||||
@@ -11,11 +11,11 @@ import { encodePath } from 'vault/utils/path-encoding-helpers';
|
|||||||
export default ApplicationAdapter.extend({
|
export default ApplicationAdapter.extend({
|
||||||
namespace: 'v1',
|
namespace: 'v1',
|
||||||
|
|
||||||
createOrUpdate(store, type, snapshot) {
|
createOrUpdate(store, { modelName }, snapshot) {
|
||||||
const { backend, name } = snapshot.record;
|
const { backend, name, type } = snapshot.record;
|
||||||
const serializer = store.serializerFor(type.modelName);
|
const serializer = store.serializerFor(modelName);
|
||||||
const data = serializer.serialize(snapshot);
|
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) => {
|
return this.ajax(url, 'POST', { data }).then((resp) => {
|
||||||
const response = resp || {};
|
const response = resp || {};
|
||||||
@@ -41,11 +41,11 @@ export default ApplicationAdapter.extend({
|
|||||||
return 'transform';
|
return 'transform';
|
||||||
},
|
},
|
||||||
|
|
||||||
urlForTransformations(backend, id) {
|
urlForTransformations(backend, id, type) {
|
||||||
let url = `${this.buildURL()}/${encodePath(backend)}/transformation`;
|
const base = `${this.buildURL()}/${encodePath(backend)}`;
|
||||||
if (id) {
|
// when type exists, transformations is plural
|
||||||
url = url + '/' + encodePath(id);
|
const url = type ? `${base}/transformations/${type}` : `${base}/transformation`;
|
||||||
}
|
if (id) return `${url}/${encodePath(id)}`;
|
||||||
return url;
|
return url;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ export default ApplicationAdapter.extend({
|
|||||||
const queryAjax = this.ajax(this.urlForTransformations(backend, id), 'GET', this.optionsForQuery(id));
|
const queryAjax = this.ajax(this.urlForTransformations(backend, id), 'GET', this.optionsForQuery(id));
|
||||||
|
|
||||||
return allSettled([queryAjax]).then((results) => {
|
return allSettled([queryAjax]).then((results) => {
|
||||||
// query result 404d, so throw the adapterError
|
// query result 404, so throw the adapterError
|
||||||
if (!results[0].value) {
|
if (!results[0].value) {
|
||||||
throw results[0].reason;
|
throw results[0].reason;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ import { computed } from '@ember/object';
|
|||||||
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
|
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
|
||||||
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
|
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
|
||||||
|
|
||||||
// these arrays define the order in which the fields will be displayed
|
// these arrays define the order in which the fields will be displayed, see:
|
||||||
// see
|
|
||||||
// https://developer.hashicorp.com/vault/api-docs/secret/transform#create-update-transformation-deprecated-1-6
|
// https://developer.hashicorp.com/vault/api-docs/secret/transform#create-update-transformation-deprecated-1-6
|
||||||
const TYPES = [
|
const TYPES = [
|
||||||
{
|
{
|
||||||
@@ -20,6 +19,10 @@ const TYPES = [
|
|||||||
value: 'masking',
|
value: 'masking',
|
||||||
displayName: 'Masking',
|
displayName: 'Masking',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: 'tokenization',
|
||||||
|
displayName: 'Tokenization',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const TWEAK_SOURCE = [
|
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 (*).',
|
subText: 'Search for an existing role, type a new role to create it, or use a wildcard (*).',
|
||||||
wildcardLabel: 'role',
|
wildcardLabel: 'role',
|
||||||
}),
|
}),
|
||||||
transformAttrs: computed('type', function () {
|
deletion_allowed: attr('boolean', {
|
||||||
if (this.type === 'masking') {
|
label: 'Allow deletion',
|
||||||
return ['name', 'type', 'masking_character', 'template', 'allowed_roles'];
|
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.',
|
||||||
return ['name', 'type', 'tweak_source', 'template', 'allowed_roles'];
|
|
||||||
}),
|
}),
|
||||||
|
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 () {
|
transformFieldAttrs: computed('transformAttrs', function () {
|
||||||
return expandAttributeMeta(this, this.transformAttrs);
|
return expandAttributeMeta(this, this.transformAttrs);
|
||||||
}),
|
}),
|
||||||
|
|||||||
95
ui/tests/unit/adapters/transform-test.js
Normal file
95
ui/tests/unit/adapters/transform-test.js
Normal 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user