Skip to content

Commit

Permalink
Sidebranch: Transform Secret Engine Initial setup (#9625)
Browse files Browse the repository at this point in the history
* WIP // list transforms, console.logs and all

* setup LIST transformations ajax request and draft out options-for-backend options

* change from plural to singluar and add transform to secret-edit

* create two transform edit components

* modify transform model with new attrs

* add adapterFor to connect transform adapter to transform-edit-form component

* setup Allowed roles searchSelect component to search over new transform/role adapter and model.

* clean up for PR

* clean up linting errors

* restructure adapter call, now it works.

* remove console

* setup template model for SearchSelect component

* add props to form field and search select for styling

Co-authored-by: Chelsea Shaw <chelshaw.dev@gmail.com>
  • Loading branch information
Monkeychip and chelshaw authored Jul 31, 2020
1 parent ecc83ef commit 60e551f
Show file tree
Hide file tree
Showing 22 changed files with 442 additions and 7 deletions.
108 changes: 108 additions & 0 deletions ui/app/adapters/transform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { assign } from '@ember/polyfills';
import { resolve, allSettled } from 'rsvp';
import ApplicationAdapter from './application';
import { encodePath } from 'vault/utils/path-encoding-helpers';

export default ApplicationAdapter.extend({
// TODO this adapter was copied over, much of this stuff may or may not need to be here.
namespace: 'v1',

// defaultSerializer: 'role',

createOrUpdate(store, type, snapshot) {
const serializer = store.serializerFor('transform'); // TODO replace transform with type.modelName
const data = serializer.serialize(snapshot);
const { id } = snapshot;
let url = this.urlForTransformations(snapshot.record.get('backend'), id);

return this.ajax(url, 'POST', { data });
},

createRecord() {
return this.createOrUpdate(...arguments);
},

updateRecord() {
return this.createOrUpdate(...arguments, 'update');
},

deleteRecord(store, type, snapshot) {
const { id } = snapshot;
return this.ajax(this.urlForRole(snapshot.record.get('backend'), id), 'DELETE');
},

pathForType() {
return 'transform';
},

urlForAlphabet(backend, id) {
let url = `${this.buildURL()}/${encodePath(backend)}/alphabet`;
if (id) {
url = url + '/' + encodePath(id);
}
return url;
},

urlForTransformations(backend, id) {
let url = `${this.buildURL()}/${encodePath(backend)}/transformation`;
if (id) {
url = url + '/' + encodePath(id);
}
return url;
},

optionsForQuery(id) {
let data = {};
if (!id) {
data['list'] = true;
}
return { data };
},

fetchByQuery(store, query) {
const { id, backend } = query;
let zeroAddressAjax = resolve();
const queryAjax = this.ajax(this.urlForTransformations(backend, id), 'GET', this.optionsForQuery(id));
if (!id) {
zeroAddressAjax = this.findAllZeroAddress(store, query);
}

return allSettled([queryAjax, zeroAddressAjax]).then(results => {
// query result 404d, so throw the adapterError
if (!results[0].value) {
throw results[0].reason;
}
let resp = {
id,
name: id,
backend,
data: {},
};

results.forEach(result => {
if (result.value) {
if (result.value.data.roles) {
resp.data = assign({}, resp.data, { zero_address_roles: result.value.data.roles });
} else {
resp.data = assign({}, resp.data, result.value.data);
}
}
});
return resp;
});
},

findAllZeroAddress(store, query) {
const { backend } = query;
const url = `/v1/${encodePath(backend)}/config/zeroaddress`;
return this.ajax(url, 'GET');
},

query(store, type, query) {
return this.fetchByQuery(store, query);
},

queryRecord(store, type, query) {
return this.fetchByQuery(store, query);
},
});
20 changes: 20 additions & 0 deletions ui/app/adapters/transform/role.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import ApplicationAdapater from '../application';

export default ApplicationAdapater.extend({
namespace: 'v1',
pathForType(type) {
return type;
},

urlForQuery() {
return this._super(...arguments) + '?list=true';
},

query(store, type) {
return this.ajax(this.buildURL(type.modelName, null, null, 'query'), 'GET');
},

buildURL(modelName, id, snapshot, requestType, query) {
return this._super(`${modelName}/`, id, snapshot, requestType, query);
},
});
20 changes: 20 additions & 0 deletions ui/app/adapters/transform/template.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import ApplicationAdapater from '../application';

export default ApplicationAdapater.extend({
namespace: 'v1',
pathForType(type) {
return type;
},

urlForQuery() {
return this._super(...arguments) + '?list=true';
},

query(store, type) {
return this.ajax(this.buildURL(type.modelName, null, null, 'query'), 'GET');
},

buildURL(modelName, id, snapshot, requestType, query) {
return this._super(`${modelName}/`, id, snapshot, requestType, query);
},
});
2 changes: 1 addition & 1 deletion ui/app/components/role-edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export default Component.extend(FocusOnInsertMixin, {
createOrUpdate(type, event) {
event.preventDefault();

const modelId = this.get('model.id');
const modelId = this.get('model.id') || this.get('model.name'); //ARG TODO this is not okay
// prevent from submitting if there's no key
// maybe do something fancier later
if (type === 'create' && isBlank(modelId)) {
Expand Down
8 changes: 8 additions & 0 deletions ui/app/components/transform-edit-form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import RoleEdit from './role-edit';

export default RoleEdit.extend({
init() {
this._super(...arguments);
this.set('backendType', 'transform');
},
});
8 changes: 8 additions & 0 deletions ui/app/components/transform-edit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import RoleEdit from './role-edit';

export default RoleEdit.extend({
init() {
this._super(...arguments);
this.set('backendType', 'transform');
},
});
9 changes: 9 additions & 0 deletions ui/app/helpers/options-for-backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ const SECRET_BACKENDS = {
editComponent: 'role-ssh-edit',
listItemPartial: 'partials/secret-list/ssh-role-item',
},
// TODO: edit or remove listItemPartial and better understand what's happening here
transform: {
displayName: 'Transform',
searchPlaceholder: 'Filter Transform',
item: 'transform',
create: 'Create Transformation',
editComponent: 'transform-edit',
listItemPartial: 'partials/secret-list/ssh-role-item',
},
transit: {
searchPlaceholder: 'Filter keys',
item: 'key',
Expand Down
12 changes: 11 additions & 1 deletion ui/app/helpers/supported-secret-backends.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { helper as buildHelper } from '@ember/component/helper';

const SUPPORTED_SECRET_BACKENDS = ['aws', 'cubbyhole', 'generic', 'kv', 'pki', 'ssh', 'transit', 'kmip'];
const SUPPORTED_SECRET_BACKENDS = [
'aws',
'cubbyhole',
'generic',
'kv',
'pki',
'ssh',
'transit',
'kmip',
'transform',
];

export function supportedSecretBackends() {
return SUPPORTED_SECRET_BACKENDS;
Expand Down
105 changes: 105 additions & 0 deletions ui/app/models/transform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { alias } from '@ember/object/computed';
import { computed } from '@ember/object';
import DS from 'ember-data';
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';

const { attr } = DS;

// these arrays define the order in which the fields will be displayed
// see
//https://www.vaultproject.io/api-docs/secret/transform#create-update-transformation
const TYPES = [
{
value: 'fpe',
displayName: 'Format Preserving Encryption (FPE)',
},
{
value: 'masking',
displayName: 'Masking',
},
];

const TWEAK_SOURCE = [
{
value: 'supplied',
displayName: 'supplied',
},
{
value: 'generated',
displayName: 'generated',
},
{
value: 'internal',
displayName: 'internal',
},
];

export default DS.Model.extend({
// TODO: for now, commenting out openApi info, but keeping here just in case we end up using it.
// useOpenAPI: true,
// getHelpUrl: function(backend) {
// console.log(backend, 'Backend');
// return `/v1/${backend}?help=1`;
// },
name: attr('string', {
// TODO: make this required for making a transformation
label: 'Name',
fieldValue: 'id',
readOnly: true,
}),
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.',
}),
template: attr('stringArray', {
label: 'Template', // TODO: make this required for making a transformation
subLabel: 'Template Name',
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.',
editType: 'searchSelect',
fallbackComponent: 'string-list',
models: ['transform/template'],
}),
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.`, // TODO: I do not include the link here. Need to figure out the best way to approach this.
}),
masking_character: attr('string', {
label: 'Masking character',
subText: 'Specify which character you’d like to mask your data.',
}),
allowed_roles: attr('stringArray', {
label: 'Allowed roles',
editType: 'searchSelect',
fallbackComponent: 'string-list',
models: ['transform/role'],
subText: 'Search for an existing role, type a new role to create it, or use a wildcard (*).',
}),
transformAttrs: computed(function() {
// TODO: group them into sections/groups. Right now, we don't different between required and not required as we do by hiding options.
// will default to design mocks on how to handle as it will likely be a different pattern using client-side validation, which we have not done before
return ['name', 'type', 'template', 'tweak_source', 'masking_characters', 'allowed_roles'];
}),
transformFieldAttrs: computed('transformAttrs', function() {
return expandAttributeMeta(this, this.get('transformAttrs'));
}),
updatePath: lazyCapabilities(apiPath`${'backend'}/transforms/${'id'}`, 'backend', 'id'),
canDelete: alias('updatePath.canDelete'),
canEdit: alias('updatePath.canUpdate'),
canRead: alias('updatePath.canRead'),

generatePath: lazyCapabilities(apiPath`${'backend'}/creds/${'id'}`, 'backend', 'id'),
canGenerate: alias('generatePath.canUpdate'),

signPath: lazyCapabilities(apiPath`${'backend'}/sign/${'id'}`, 'backend', 'id'),
canSign: alias('signPath.canUpdate'),

zeroAddressPath: lazyCapabilities(apiPath`${'backend'}/config/zeroaddress`, 'backend'),
canEditZeroAddress: alias('zeroAddressPath.canUpdate'),
});
3 changes: 3 additions & 0 deletions ui/app/models/transform/role.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import DS from 'ember-data';

export default DS.Model.extend({});
3 changes: 3 additions & 0 deletions ui/app/models/transform/template.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import DS from 'ember-data';

export default DS.Model.extend({});
2 changes: 2 additions & 0 deletions ui/app/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ Router.map(function() {
// transit-specific routes
this.route('actions-root', { path: '/actions/' });
this.route('actions', { path: '/actions/*secret' });
// transform-specific routes
// TODO: add these
});
});
this.route('policies', { path: '/policies/:type' }, function() {
Expand Down
2 changes: 2 additions & 0 deletions ui/app/routes/vault/cluster/secrets/backend/list.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export default Route.extend({
let types = {
transit: 'transit-key',
ssh: 'role-ssh',
transform: 'transform',
aws: 'role-aws',
pki: tab === 'certs' ? 'pki-certificate' : 'role-pki',
// secret or secret-v2
Expand All @@ -70,6 +71,7 @@ export default Route.extend({
const secret = this.secretParam() || '';
const backend = this.enginePathParam();
const backendModel = this.modelFor('vault.cluster.secrets.backend');

return hash({
secret,
secrets: this.store
Expand Down
1 change: 1 addition & 0 deletions ui/app/routes/vault/cluster/secrets/backend/secret-edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export default Route.extend(UnloadModelRoute, {
let types = {
transit: 'transit-key',
ssh: 'role-ssh',
transform: 'transform',
aws: 'role-aws',
pki: secret && secret.startsWith('cert/') ? 'pki-certificate' : 'role-pki',
cubbyhole: 'secret',
Expand Down
7 changes: 5 additions & 2 deletions ui/app/services/path-help.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,13 @@ export default Service.extend({
// Returns relevant information from OpenAPI
// as determined by the expandOpenApiProps util
getProps(helpUrl, backend) {
// add name of thing you want
debug(`Fetching schema properties for ${backend} from ${helpUrl}`);

return this.ajax(helpUrl, backend).then(help => {
// paths is an array but it will have a single entry
// for the scope we're in
const path = Object.keys(help.openapi.paths)[0];
const path = Object.keys(help.openapi.paths)[0]; // do this or look at name
const pathInfo = help.openapi.paths[path];
const params = pathInfo.parameters;
let paramProp = {};
Expand All @@ -202,7 +203,9 @@ export default Service.extend({
}

// TODO: handle post endpoints without requestBody
const props = pathInfo.post.requestBody.content['application/json'].schema.properties;
const props = pathInfo.post
? pathInfo.post.requestBody.content['application/json'].schema.properties
: {};
// put url params (e.g. {name}, {role})
// at the front of the props list
const newProps = assign({}, paramProp, props);
Expand Down
4 changes: 4 additions & 0 deletions ui/app/styles/core/forms.scss
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ label {
}
}

.sub-text {
color: $grey;
margin-bottom: 0.25rem;
}
.input,
.textarea,
.select select {
Expand Down
Loading

0 comments on commit 60e551f

Please sign in to comment.