1
0

remove transform (ENT)

This commit is contained in:
Konstantin Demin 2024-07-03 11:41:12 +03:00
parent ffc930c08a
commit a0f77417d5
51 changed files with 1 additions and 2401 deletions

View File

@ -671,21 +671,6 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) map[string]cli.Co
BaseCommand: getBaseCommand(),
}, nil
},
"transform": func() (cli.Command, error) {
return &TransformCommand{
BaseCommand: getBaseCommand(),
}, nil
},
"transform import": func() (cli.Command, error) {
return &TransformImportCommand{
BaseCommand: getBaseCommand(),
}, nil
},
"transform import-version": func() (cli.Command, error) {
return &TransformImportVersionCommand{
BaseCommand: getBaseCommand(),
}, nil
},
"transit": func() (cli.Command, error) {
return &TransitCommand{
BaseCommand: getBaseCommand(),

View File

@ -1,44 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"strings"
"github.com/mitchellh/cli"
)
var _ cli.Command = (*TransformCommand)(nil)
type TransformCommand struct {
*BaseCommand
}
func (c *TransformCommand) Synopsis() string {
return "Interact with Vault's Transform Secrets Engine"
}
func (c *TransformCommand) Help() string {
helpText := `
Usage: vault transform <subcommand> [options] [args]
This command has subcommands for interacting with Vault's Transform Secrets
Engine. Here are some simple examples, and more detailed examples are
available in the subcommands or the documentation.
To import a key into a new FPE transformation:
$ vault transform import transform/transformations/fpe/new-transformation @path/to/key \
template=identifier \
allowed_roles=physical-access
Please see the individual subcommand help for detailed usage information.
`
return strings.TrimSpace(helpText)
}
func (c *TransformCommand) Run(args []string) int {
return cli.RunResultHelp
}

View File

@ -1,79 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"errors"
"regexp"
"strings"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var (
_ cli.Command = (*TransformImportCommand)(nil)
_ cli.CommandAutocomplete = (*TransformImportCommand)(nil)
transformKeyPath = regexp.MustCompile("^(.*)/transformations/(fpe|tokenization)/([^/]*)$")
)
type TransformImportCommand struct {
*BaseCommand
}
func (c *TransformImportCommand) Synopsis() string {
return "Import a key into the Transform secrets engines."
}
func (c *TransformImportCommand) Help() string {
helpText := `
Usage: vault transform import PATH KEY [options...]
Using the Transform key wrapping system, imports key material from
the base64 encoded KEY (either directly on the CLI or via @path notation),
into a new FPE or tokenization transformation whose API path is PATH.
To import a new key version into an existing tokenization transformation,
use import_version.
The remaining options after KEY (key=value style) are passed on to
Create/Update FPE Transformation or Create/Update Tokenization Transformation
API endpoints.
For example:
$ vault transform import transform/transformations/tokenization/application-form @path/to/key \
allowed_roles=legacy-system
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *TransformImportCommand) Flags() *FlagSets {
return c.flagSet(FlagSetHTTP)
}
func (c *TransformImportCommand) AutocompleteArgs() complete.Predictor {
return nil
}
func (c *TransformImportCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *TransformImportCommand) Run(args []string) int {
return ImportKey(c.BaseCommand, "import", transformImportKeyPath, c.Flags(), args)
}
func transformImportKeyPath(s string, operation string) (path string, apiPath string, err error) {
parts := transformKeyPath.FindStringSubmatch(s)
if len(parts) != 4 {
return "", "", errors.New("expected transform path and key name in the form :path:/transformations/fpe|tokenization/:name:")
}
path = parts[1]
transformation := parts[2]
keyName := parts[3]
apiPath = path + "/transformations/" + transformation + "/" + keyName + "/" + operation
return path, apiPath, nil
}

View File

@ -1,59 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"strings"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var (
_ cli.Command = (*TransformImportVersionCommand)(nil)
_ cli.CommandAutocomplete = (*TransformImportVersionCommand)(nil)
)
type TransformImportVersionCommand struct {
*BaseCommand
}
func (c *TransformImportVersionCommand) Synopsis() string {
return "Import key material into a new key version in the Transform secrets engines."
}
func (c *TransformImportVersionCommand) Help() string {
helpText := `
Usage: vault transform import-version PATH KEY [...]
Using the Transform key wrapping system, imports new key material from
the base64 encoded KEY (either directly on the CLI or via @path notation),
into an existing tokenization transformation whose API path is PATH.
The remaining options after KEY (key=value style) are passed on to
Create/Update Tokenization Transformation API endpoint.
For example:
$ vault transform import-version transform/transformations/tokenization/application-form @path/to/new_version \
allowed_roles=legacy-system
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *TransformImportVersionCommand) Flags() *FlagSets {
return c.flagSet(FlagSetHTTP)
}
func (c *TransformImportVersionCommand) AutocompleteArgs() complete.Predictor {
return nil
}
func (c *TransformImportVersionCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *TransformImportVersionCommand) Run(args []string) int {
return ImportKey(c.BaseCommand, "import_version", transformImportKeyPath, c.Flags(), args)
}

View File

@ -76,7 +76,6 @@ vault secrets enable "transit"
# Enable enterprise features
if [[ -n "${VAULT_LICENSE:-}" ]]; then
vault secrets enable "keymgmt"
vault secrets enable "transform"
fi
# Output OpenAPI, optionally formatted

View File

@ -1,102 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { assign } from '@ember/polyfills';
import { allSettled } from 'rsvp';
import ApplicationAdapter from './application';
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);
const data = serializer.serialize(snapshot);
const url = this.urlForTransformations(backend, name);
return this.ajax(url, 'POST', { data }).then((resp) => {
const response = resp || {};
response.id = name;
return response;
});
},
createRecord() {
return this.createOrUpdate(...arguments);
},
updateRecord() {
return this.createOrUpdate(...arguments, 'update');
},
deleteRecord(store, type, snapshot) {
const { id } = snapshot;
return this.ajax(this.urlForTransformations(snapshot.record.get('backend'), id), 'DELETE');
},
pathForType() {
return 'transform';
},
urlForTransformations(backend, id) {
let url = `${this.buildURL()}/${encodePath(backend)}/transformation`;
if (id) {
url = url + '/' + encodePath(id);
}
return url;
},
optionsForQuery(id) {
const data = {};
if (!id) {
data['list'] = true;
}
return { data };
},
fetchByQuery(store, query) {
const { id, backend } = query;
const queryAjax = this.ajax(this.urlForTransformations(backend, id), 'GET', this.optionsForQuery(id));
return allSettled([queryAjax]).then((results) => {
// query result 404d, so throw the adapterError
if (!results[0].value) {
throw results[0].reason;
}
const resp = {
id,
name: id,
backend,
data: {},
};
results.forEach((result) => {
if (result.value) {
let d = result.value.data;
if (d.templates) {
// In Transformations data goes up as "template", but comes down as "templates"
// To keep the keys consistent we're translating here
d = {
...d,
template: d.templates,
};
delete d.templates;
}
resp.data = assign({}, resp.data, d);
}
});
return resp;
});
},
query(store, type, query) {
return this.fetchByQuery(store, query);
},
queryRecord(store, type, query) {
return this.fetchByQuery(store, query);
},
});

View File

@ -1,12 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import BaseAdapter from './base';
export default BaseAdapter.extend({
pathForType() {
return 'alphabet';
},
});

View File

@ -1,77 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationAdapter from '../application';
import { encodePath } from 'vault/utils/path-encoding-helpers';
export default ApplicationAdapter.extend({
namespace: 'v1',
pathForType(type) {
return type.replace('transform/', '');
},
createOrUpdate(store, type, snapshot) {
const { backend, name } = snapshot.record;
const serializer = store.serializerFor(type.modelName);
const data = serializer.serialize(snapshot);
const url = this.url(backend, type.modelName, name);
return this.ajax(url, 'POST', { data }).then((resp) => {
// Ember data doesn't like 204 responses except for DELETE method
const response = resp || { data: {} };
response.data.name = name;
return response;
});
},
createRecord() {
return this.createOrUpdate(...arguments);
},
updateRecord() {
return this.createOrUpdate(...arguments, 'update');
},
deleteRecord(store, type, snapshot) {
const { id } = snapshot;
return this.ajax(this.url(snapshot.record.get('backend'), type.modelName, id), 'DELETE');
},
url(backend, modelType, id) {
const type = this.pathForType(modelType);
const url = `/${this.namespace}/${encodePath(backend)}/${encodePath(type)}`;
if (id) {
return `${url}/${encodePath(id)}`;
}
return url + '?list=true';
},
fetchByQuery(query) {
const { backend, modelName, id } = query;
return this.ajax(this.url(backend, modelName, id), 'GET').then((resp) => {
// The API response doesn't explicitly include the name/id, so add it here
return {
...resp,
backend,
id,
name: id,
};
});
},
query(store, type, query) {
return this.fetchByQuery(query);
},
queryRecord(store, type, query) {
return this.ajax(this.url(query.backend, type.modelName, query.id), 'GET').then((result) => {
return {
id: query.id,
name: query.id,
...result,
};
});
},
});

View File

@ -1,12 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import BaseAdapter from './base';
export default BaseAdapter.extend({
pathForType() {
return 'role';
},
});

View File

@ -1,12 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import BaseAdapter from './base';
export default BaseAdapter.extend({
pathForType() {
return 'template';
},
});

View File

@ -1,10 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import BaseAdapter from './base';
export default BaseAdapter.extend({
// custom stuff for transformation
});

View File

@ -1,8 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import TransformBase from './transform-edit-base';
export default TransformBase.extend({});

View File

@ -18,7 +18,7 @@
@onRadioChange={{mut this.selection}}
@disabled={{if type.requiredFeature (not (has-feature type.requiredFeature)) false}}
@tooltipMessage={{if
(or (eq type.type "transform") (eq type.type "keymgmt"))
(eq type.type "keymgmt")
(concat
type.displayName
" is part of the Advanced Data Protection module, which is not included in your enterprise license."

View File

@ -1,37 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
/**
* @module TransformListItem
* TransformListItem components are used for the list items for the Transform Secret Engines for all but Transformations.
* This component automatically handles read-only list items if capabilities are not granted or the item is internal only.
*
* @example
* ```js
* <TransformListItem @item={item} @itemPath="role/my-item" @itemType="role" />
* ```
* @param {object} item - item refers to the model item used on the list item partial
* @param {string} itemPath - usually the id of the item, but can be prefixed with the model type (see transform/role)
* @param {string} [itemType] - itemType is used to calculate whether an item is readable or
*/
import { computed } from '@ember/object';
import Component from '@ember/component';
export default Component.extend({
item: null,
itemPath: '',
itemType: '',
isBuiltin: computed('item', 'itemType', function () {
const item = this.item;
if (this.itemType === 'alphabet' || this.itemType === 'template') {
return item.get('id').startsWith('builtin/');
}
return false;
}),
backendType: 'transform',
});

View File

@ -1,42 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action, set } from '@ember/object';
/**
* @module TransformAdvancedTemplating
* TransformAdvancedTemplating components are used to modify encode/decode formats of transform templates
*
* @example
* ```js
* <TransformAdvancedTemplating @model={{this.model}} />
* ```
* @param {Object} model - transform template model
*/
export default class TransformAdvancedTemplating extends Component {
@tracked inputOptions = [];
@action
setInputOptions(testValue, captureGroups) {
if (captureGroups && captureGroups.length) {
this.inputOptions = captureGroups.map(({ position, value }) => {
return {
label: `${position}: ${value}`,
value: position,
};
});
} else {
this.inputOptions = [];
}
}
@action
decodeFormatValueChange(kvObject, kvData, value) {
set(kvObject, 'value', value);
this.args.model.decodeFormats = kvData.toJSON();
}
}

View File

@ -1,8 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import TransformationEdit from './transformation-edit';
export default TransformationEdit.extend({});

View File

@ -1,133 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { inject as service } from '@ember/service';
import { or } from '@ember/object/computed';
import { isBlank } from '@ember/utils';
import Component from '@ember/component';
import { set } from '@ember/object';
import FocusOnInsertMixin from 'vault/mixins/focus-on-insert';
const LIST_ROOT_ROUTE = 'vault.cluster.secrets.backend.list-root';
const SHOW_ROUTE = 'vault.cluster.secrets.backend.show';
export const addToList = (list, itemToAdd) => {
if (!list || !Array.isArray(list)) return list;
list.push(itemToAdd);
return list.uniq();
};
export const removeFromList = (list, itemToRemove) => {
if (!list) return list;
const index = list.indexOf(itemToRemove);
if (index < 0) return list;
const newList = list.removeAt(index, 1);
return newList.uniq();
};
export default Component.extend(FocusOnInsertMixin, {
store: service(),
flashMessages: service(),
router: service(),
mode: null,
onDataChange() {},
onRefresh() {},
model: null,
requestInFlight: or('model.isLoading', 'model.isReloading', 'model.isSaving'),
init() {
this._super(...arguments);
this.set('backendType', 'transform');
},
willDestroyElement() {
this._super(...arguments);
if (this.model && this.model.isError) {
this.model.rollbackAttributes();
}
},
transitionToRoute() {
this.router.transitionTo(...arguments);
},
modelPrefixFromType(modelType) {
let modelPrefix = '';
if (modelType && modelType.startsWith('transform/')) {
modelPrefix = `${modelType.replace('transform/', '')}/`;
}
return modelPrefix;
},
listTabFromType(modelType) {
let tab;
if (modelType && modelType.startsWith('transform/')) {
tab = `${modelType.replace('transform/', '')}`;
}
return tab;
},
persist(method, successCallback) {
const model = this.model;
return model[method]()
.then(() => {
successCallback(model);
})
.catch((e) => {
model.set('displayErrors', e.errors);
throw e;
});
},
applyDelete(callback = () => {}) {
const tab = this.listTabFromType(this.model.constructor.modelName);
this.persist('destroyRecord', () => {
this.hasDataChanges();
callback();
this.transitionToRoute(LIST_ROOT_ROUTE, { queryParams: { tab } });
});
},
applyChanges(type, callback = () => {}) {
const modelId = this.model.id || this.model.name; // transform comes in as model.name
const modelPrefix = this.modelPrefixFromType(this.model.constructor.modelName);
// prevent from submitting if there's no key
// maybe do something fancier later
if (type === 'create' && isBlank(modelId)) {
return;
}
this.persist('save', () => {
this.hasDataChanges();
callback();
this.transitionToRoute(SHOW_ROUTE, `${modelPrefix}${modelId}`);
});
},
hasDataChanges() {
this.onDataChange(this.model?.hasDirtyAttributes);
},
actions: {
createOrUpdate(type, event) {
event.preventDefault();
this.applyChanges(type);
},
setValue(key, event) {
set(this.model, key, event.target.checked);
},
refresh() {
this.onRefresh();
},
delete() {
this.applyDelete();
},
},
});

View File

@ -1,8 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import TransformationEdit from './transformation-edit';
export default TransformationEdit.extend({});

View File

@ -1,128 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import TransformBase, { addToList, removeFromList } from './transform-edit-base';
import { inject as service } from '@ember/service';
export default TransformBase.extend({
flashMessages: service(),
store: service(),
initialTransformations: null,
init() {
this._super(...arguments);
this.set('initialTransformations', this.model.transformations);
},
handleUpdateTransformations(updateTransformations, roleId, type = 'update') {
if (!updateTransformations) return;
const backend = this.model.backend;
const promises = updateTransformations.map((transform) => {
return this.store
.queryRecord('transform', {
backend,
id: transform.id,
})
.then(function (transformation) {
let roles = transformation.allowed_roles;
if (transform.action === 'ADD') {
roles = addToList(roles, roleId);
} else if (transform.action === 'REMOVE') {
roles = removeFromList(roles, roleId);
}
transformation.setProperties({
backend,
allowed_roles: roles,
});
return transformation.save().catch((e) => {
return { errorStatus: e.httpStatus, ...transform };
});
});
});
Promise.all(promises).then((res) => {
const hasError = res.find((r) => !!r.errorStatus);
if (hasError) {
const errorAdding = res.find((r) => r.errorStatus === 403 && r.action === 'ADD');
const errorRemoving = res.find((r) => r.errorStatus === 403 && r.action === 'REMOVE');
let message =
'The edits to this role were successful, but allowed_roles for its transformations was not edited due to a lack of permissions.';
if (type === 'create') {
message =
'Transformations have been attached to this role, but the role was not added to those transformations allowed_roles due to a lack of permissions.';
} else if (errorAdding && errorRemoving) {
message =
'This role was edited to both add and remove transformations; however, this role was not added or removed from those transformations allowed_roles due to a lack of permissions.';
} else if (errorAdding) {
message =
'This role was edited to include new transformations, but this role was not added to those transformations allowed_roles due to a lack of permissions.';
} else if (errorRemoving) {
message =
'This role was edited to remove transformations, but this role was not removed from those transformations allowed_roles due to a lack of permissions.';
}
this.flashMessages.info(message, {
sticky: true,
priority: 300,
});
}
});
},
actions: {
createOrUpdate(type, event) {
event.preventDefault();
this.applyChanges('save', () => {
const roleId = this.model.id;
const newModelTransformations = this.model.transformations;
if (!this.initialTransformations) {
this.handleUpdateTransformations(
newModelTransformations.map((t) => ({
id: t,
action: 'ADD',
})),
roleId,
type
);
return;
}
const updateTransformations = [...newModelTransformations, ...this.initialTransformations]
.map((t) => {
if (this.initialTransformations.indexOf(t) < 0) {
return {
id: t,
action: 'ADD',
};
}
if (newModelTransformations.indexOf(t) < 0) {
return {
id: t,
action: 'REMOVE',
};
}
return null;
})
.filter((t) => !!t);
this.handleUpdateTransformations(updateTransformations, roleId);
});
},
delete() {
const roleId = this.model?.id;
const roleTransformations = this.model?.transformations || [];
const updateTransformations = roleTransformations.map((t) => ({
id: t,
action: 'REMOVE',
}));
this.handleUpdateTransformations(updateTransformations, roleId);
this.applyDelete();
},
},
});

View File

@ -1,34 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import TransformBase from './transform-edit-base';
import { computed } from '@ember/object';
export default TransformBase.extend({
cliCommand: computed('model.{allowed_roles,type,tweak_source}', function () {
if (!this.model) {
return;
}
const { type, allowed_roles, tweak_source, name } = this.model;
const wildCardRole = allowed_roles.find((role) => role.includes('*'));
// values to be returned
let role = '<choose a role>';
const value = 'value=<enter your value here>';
let tweak = '';
// determine the role
if (allowed_roles.length === 1 && !wildCardRole) {
role = allowed_roles[0];
}
// determine the tweak_source
if (type === 'fpe' && tweak_source === 'supplied') {
tweak = 'tweak=<enter your tweak>';
}
return `${role} ${value} ${tweak} transformation=${name}`;
}),
});

View File

@ -1,8 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import TransformBase from './transform-edit-base';
export default TransformBase.extend({});

View File

@ -1,134 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import TransformBase, { addToList, removeFromList } from './transform-edit-base';
import { inject as service } from '@ember/service';
export default TransformBase.extend({
flashMessages: service(),
store: service(),
initialRoles: null,
init() {
this._super(...arguments);
if (!this.model) return;
this.set('initialRoles', this.model.allowed_roles);
},
updateOrCreateRole(role, transformationId, backend) {
return this.store
.queryRecord('transform/role', {
backend,
id: role.id,
})
.then((roleStore) => {
let transformations = roleStore.transformations;
if (role.action === 'ADD') {
transformations = addToList(transformations, transformationId);
} else if (role.action === 'REMOVE') {
transformations = removeFromList(transformations, transformationId);
}
roleStore.setProperties({
backend,
transformations,
});
return roleStore.save().catch((e) => {
return {
errorStatus: e.httpStatus,
...role,
};
});
})
.catch((e) => {
if (e.httpStatus !== 403 && role.action === 'ADD') {
// If role doesn't yet exist, create it with this transformation attached
var newRole = this.store.createRecord('transform/role', {
id: role.id,
name: role.id,
transformations: [transformationId],
backend,
});
return newRole.save().catch((e) => {
return {
errorStatus: e.httpStatus,
...role,
action: 'CREATE',
};
});
}
return {
...role,
errorStatus: e.httpStatus,
};
});
},
handleUpdateRoles(updateRoles, transformationId) {
if (!updateRoles) return;
const backend = this.model.backend;
const promises = updateRoles.map((r) => this.updateOrCreateRole(r, transformationId, backend));
Promise.all(promises).then((results) => {
const hasError = results.find((role) => !!role.errorStatus);
if (hasError) {
let message =
'The edits to this transformation were successful, but transformations for its roles was not edited due to a lack of permissions.';
if (results.find((e) => !!e.errorStatus && e.errorStatus !== 403)) {
// if the errors weren't all due to permissions show generic message
// eg. trying to update a role with empty array as transformations
message = `You've edited the allowed_roles for this transformation. However, the corresponding edits to some roles' transformations were not made`;
}
this.flashMessages.info(message, {
sticky: true,
priority: 300,
});
}
});
},
isWildcard(role) {
if (typeof role === 'string') {
return role.indexOf('*') >= 0;
}
if (role && role.id) {
return role.id.indexOf('*') >= 0;
}
return false;
},
actions: {
createOrUpdate(type, event) {
event.preventDefault();
this.applyChanges('save', () => {
const transformationId = this.model.id || this.model.name;
const newModelRoles = this.model.allowed_roles || [];
const initialRoles = this.initialRoles || [];
const updateRoles = [...newModelRoles, ...initialRoles]
.filter((r) => !this.isWildcard(r)) // CBS TODO: expand wildcards into included roles instead
.map((role) => {
if (initialRoles.indexOf(role) < 0) {
return {
id: role,
action: 'ADD',
};
}
if (newModelRoles.indexOf(role) < 0) {
return {
id: role,
action: 'REMOVE',
};
}
return null;
})
.filter((r) => !!r);
this.handleUpdateRoles(updateRoles, transformationId);
});
},
},
});

View File

@ -16,7 +16,6 @@ const ALL_FEATURES = [
'Performance Standby',
'Namespaces',
'Entropy Augmentation',
'Transform Secrets Engine',
];
export function allFeatures() {

View File

@ -6,12 +6,6 @@
import { helper as buildHelper } from '@ember/component/helper';
const ENTERPRISE_SECRET_ENGINES = [
{
displayName: 'Transform',
type: 'transform',
category: 'generic',
requiredFeature: 'Transform Secrets Engine',
},
{
displayName: 'Key Management',
type: 'keymgmt',

View File

@ -13,7 +13,6 @@ const SUPPORTED_SECRET_BACKENDS = [
'pki',
'ssh',
'transit',
'transform',
'keymgmt',
'kubernetes',
];

View File

@ -1,104 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Model, { attr } from '@ember-data/model';
import { computed } from '@ember/object';
import { apiPath } from 'vault/macros/lazy-capabilities';
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
import attachCapabilities from 'vault/lib/attach-capabilities';
// 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',
},
];
const ModelExport = Model.extend({
name: attr('string', {
// CBS TODO: make this required for making a transformation
label: 'Name',
readOnly: true,
subText: 'The name for your transformation. This cannot be edited later.',
}),
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. This cannot be edited later.',
}),
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.`, // CBS TODO: I do not include the link here. Need to figure out the best way to approach this.
}),
masking_character: attr('string', {
characterLimit: 1,
defaultValue: '*',
label: 'Masking character',
subText: 'Specify which character youd like to mask your data.',
}),
template: attr('array', {
editType: 'searchSelect',
isSectionHeader: true,
fallbackComponent: 'string-list',
label: 'Template', // CBS TODO: make this required for making a transformation
models: ['transform/template'],
selectLimit: 1,
onlyAllowExisting: true,
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.',
}),
allowed_roles: attr('array', {
editType: 'searchSelect',
isSectionHeader: true,
label: 'Allowed roles',
fallbackComponent: 'string-list',
models: ['transform/role'],
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'];
}),
transformFieldAttrs: computed('transformAttrs', function () {
return expandAttributeMeta(this, this.transformAttrs);
}),
backend: attr('string', {
readOnly: true,
}),
});
export default attachCapabilities(ModelExport, {
updatePath: apiPath`${'backend'}/transformation/${'id'}`,
});

View File

@ -1,39 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Model, { attr } from '@ember-data/model';
import { computed } from '@ember/object';
import { apiPath } from 'vault/macros/lazy-capabilities';
import attachCapabilities from 'vault/lib/attach-capabilities';
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
const M = Model.extend({
idPrefix: 'alphabet/',
idForNav: computed('id', 'idPrefix', function () {
const modelId = this.id || '';
return `${this.idPrefix}${modelId}`;
}),
name: attr('string', {
readOnly: true,
subText: 'The alphabet name. Keep in mind that spaces are not allowed and this cannot be edited later.',
}),
alphabet: attr('string', {
label: 'Alphabet',
subText:
'Provide the set of valid UTF-8 characters contained within both the input and transformed value. Read more.',
}),
attrs: computed(function () {
const keys = ['name', 'alphabet'];
return expandAttributeMeta(this, keys);
}),
backend: attr('string', { readOnly: true }),
});
export default attachCapabilities(M, {
updatePath: apiPath`${'backend'}/alphabet/${'id'}`,
});

View File

@ -1,47 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Model, { attr } from '@ember-data/model';
import { computed } from '@ember/object';
import { apiPath } from 'vault/macros/lazy-capabilities';
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
import attachCapabilities from 'vault/lib/attach-capabilities';
const ModelExport = Model.extend({
// used for getting appropriate options for backend
idPrefix: 'role/',
// the id prefixed with `role/` so we can use it as the *secret param for the secret show route
idForNav: computed('id', 'idPrefix', function () {
const modelId = this.id || '';
return `${this.idPrefix}${modelId}`;
}),
name: attr('string', {
// TODO: make this required for making a transformation
label: 'Name',
readOnly: true,
subText: 'The name for your role. This cannot be edited later.',
}),
transformations: attr('array', {
editType: 'searchSelect',
isSectionHeader: true,
fallbackComponent: 'string-list',
label: 'Transformations',
models: ['transform'],
onlyAllowExisting: true,
subText: 'Select which transformations this role will have access to. It must already exist.',
}),
attrs: computed('transformations', function () {
const keys = ['name', 'transformations'];
return expandAttributeMeta(this, keys);
}),
backend: attr('string', { readOnly: true }),
});
export default attachCapabilities(ModelExport, {
updatePath: apiPath`${'backend'}/role/${'id'}`,
});

View File

@ -1,54 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Model, { attr } from '@ember-data/model';
import { computed } from '@ember/object';
import { apiPath } from 'vault/macros/lazy-capabilities';
import attachCapabilities from 'vault/lib/attach-capabilities';
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
const M = Model.extend({
idPrefix: 'template/',
idForNav: computed('id', 'idPrefix', function () {
const modelId = this.id || '';
return `${this.idPrefix}${modelId}`;
}),
name: attr('string', {
readOnly: true,
subText:
'Templates allow Vault to determine what and how to capture the value to be transformed. This cannot be edited later.',
}),
type: attr('string', { defaultValue: 'regex' }),
pattern: attr('string', {
editType: 'regex',
subText: 'The templates pattern defines the data format. Expressed in regex.',
}),
alphabet: attr('array', {
subText:
'Alphabet defines a set of characters (UTF-8) that is used for FPE to determine the validity of plaintext and ciphertext values. You can choose a built-in one, or create your own.',
editType: 'searchSelect',
isSectionHeader: true,
fallbackComponent: 'string-list',
label: 'Alphabet',
models: ['transform/alphabet'],
selectLimit: 1,
}),
encodeFormat: attr('string'),
decodeFormats: attr(),
backend: attr('string', { readOnly: true }),
readAttrs: computed(function () {
const keys = ['name', 'pattern', 'encodeFormat', 'decodeFormats', 'alphabet'];
return expandAttributeMeta(this, keys);
}),
writeAttrs: computed(function () {
return expandAttributeMeta(this, ['name', 'pattern', 'alphabet']);
}),
});
export default attachCapabilities(M, {
updatePath: apiPath`${'backend'}/template/${'id'}`,
});

View File

@ -25,13 +25,6 @@ const secretModel = (store, backend, key) => {
return secret;
};
const transformModel = (queryParams) => {
const modelType = 'transform';
if (!queryParams || !queryParams.itemType) return modelType;
return `${modelType}/${queryParams.itemType}`;
};
export default EditBase.extend({
store: service(),
@ -41,9 +34,6 @@ export default EditBase.extend({
if (modelType === 'role-ssh') {
return this.store.createRecord(modelType, { keyType: 'ca' });
}
if (modelType === 'transform') {
modelType = transformModel(transition.to.queryParams);
}
if (modelType === 'database/connection' && transition.to?.queryParams?.itemType === 'role') {
modelType = 'database/role';
}

View File

@ -35,25 +35,6 @@ export default Route.extend({
},
},
modelTypeForTransform(tab) {
let modelType;
switch (tab) {
case 'role':
modelType = 'transform/role';
break;
case 'template':
modelType = 'transform/template';
break;
case 'alphabet':
modelType = 'transform/alphabet';
break;
default: // CBS TODO: transform/transformation
modelType = 'transform';
break;
}
return modelType;
},
secretParam() {
const { secret } = this.paramsFor(this.routeName);
return secret ? normalizePath(secret) : '';
@ -95,7 +76,6 @@ export default Route.extend({
database: tab === 'role' ? 'database/role' : 'database/connection',
transit: 'transit-key',
ssh: 'role-ssh',
transform: this.modelTypeForTransform(tab),
pki: `pki/${tab || 'pki-role'}`,
// secret or secret-v2
cubbyhole: 'secret',

View File

@ -38,38 +38,12 @@ export default Route.extend(UnloadModelRoute, {
path = backend + '/keys/' + secret;
} else if (backendType === 'ssh') {
path = backend + '/roles/' + secret;
} else if (modelType.startsWith('transform/')) {
path = this.buildTransformPath(backend, secret, modelType);
} else {
path = backend + '/' + secret;
}
return this.store.findRecord('capabilities', path);
},
buildTransformPath(backend, secret, modelType) {
const noun = modelType.split('/')[1];
return `${backend}/${noun}/${secret}`;
},
modelTypeForTransform(secretName) {
if (!secretName) return 'transform';
if (secretName.startsWith('role/')) {
return 'transform/role';
}
if (secretName.startsWith('template/')) {
return 'transform/template';
}
if (secretName.startsWith('alphabet/')) {
return 'transform/alphabet';
}
return 'transform'; // TODO: transform/transformation;
},
transformSecretName(secret, modelType) {
const noun = modelType.split('/')[1];
return secret.replace(`${noun}/`, '');
},
backendType() {
return this.modelFor('vault.cluster.secrets.backend').get('engineType');
},
@ -107,7 +81,6 @@ export default Route.extend(UnloadModelRoute, {
database: secret && secret.startsWith('role/') ? 'database/role' : 'database/connection',
transit: 'transit-key',
ssh: 'role-ssh',
transform: this.modelTypeForTransform(secret),
pki: secret && secret.startsWith('cert/') ? 'pki/cert' : 'pki/pki-role',
cubbyhole: 'secret',
kv: backendModel.modelTypeForKV,
@ -241,9 +214,6 @@ export default Route.extend(UnloadModelRoute, {
if (modelType === 'pki/cert') {
secret = secret.replace('cert/', '');
}
if (modelType.startsWith('transform/')) {
secret = this.transformSecretName(secret, modelType);
}
if (modelType === 'database/role') {
secret = secret.replace('role/', '');
}

View File

@ -1,37 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationSerializer from './application';
export default ApplicationSerializer.extend({
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
if (payload.data?.masking_character) {
payload.data.masking_character = String.fromCharCode(payload.data.masking_character);
}
return this._super(store, primaryModelClass, payload, id, requestType);
},
serialize() {
const json = this._super(...arguments);
if (json.template && Array.isArray(json.template)) {
// Transformations should only ever have one template
json.template = json.template[0];
}
return json;
},
extractLazyPaginatedData(payload) {
return payload.data.keys.map((key) => {
const model = {
id: key,
name: key,
};
if (payload.backend) {
model.backend = payload.backend;
}
return model;
});
},
});

View File

@ -1,23 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationSerializer from '../application';
export default ApplicationSerializer.extend({
primaryKey: 'name',
extractLazyPaginatedData(payload) {
return payload.data.keys.map((key) => {
const model = {
id: key,
name: key,
};
if (payload.backend) {
model.backend = payload.backend;
}
return model;
});
},
});

View File

@ -1,22 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationSerializer from '../application';
export default ApplicationSerializer.extend({
primaryKey: 'name',
extractLazyPaginatedData(payload) {
return payload.data.keys.map((key) => {
const model = {
id: key,
name: key,
};
if (payload.backend) {
model.backend = payload.backend;
}
return model;
});
},
});

View File

@ -1,60 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationSerializer from '../application';
export default ApplicationSerializer.extend({
primaryKey: 'name',
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
if (payload.data?.alphabet) {
payload.data.alphabet = [payload.data.alphabet];
}
// strip out P character from any named capture groups
if (payload.data?.pattern) {
this._formatNamedCaptureGroups(payload.data, '?P', '?');
}
return this._super(store, primaryModelClass, payload, id, requestType);
},
serialize() {
const json = this._super(...arguments);
if (json.alphabet && Array.isArray(json.alphabet)) {
// Templates should only ever have one alphabet
json.alphabet = json.alphabet[0];
}
// add P character to any named capture groups
if (json.pattern) {
this._formatNamedCaptureGroups(json, '?', '?P');
}
return json;
},
_formatNamedCaptureGroups(json, replace, replaceWith) {
// named capture groups are handled differently between Go and js
// first look for named capture groups in pattern string
const regex = new RegExp(/\?P?(<(.+?)>)/, 'g');
const namedGroups = json.pattern.match(regex);
if (namedGroups) {
namedGroups.forEach((group) => {
// add or remove P depending on destination
json.pattern = json.pattern.replace(group, group.replace(replace, replaceWith));
});
}
},
extractLazyPaginatedData(payload) {
return payload.data.keys.map((key) => {
const model = {
id: key,
name: key,
};
if (payload.backend) {
model.backend = payload.backend;
}
return model;
});
},
});

View File

@ -1,19 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
.copy-text {
background: $ui-gray-010;
& > code {
color: $ui-gray-800;
padding: 14px;
}
}
.transform-pattern-text div:not(:first-child) {
font-family: $family-monospace;
}
.transform-decode-formats:not(:last-child) {
margin-bottom: $spacing-s;
}

View File

@ -118,7 +118,6 @@
@import './components/token-expire-warning';
@import './components/toolbar';
@import './components/tool-tip';
@import './components/transform-edit';
@import './components/transit-card';
@import './components/ttl-picker';
@import './components/unseal-warning';

View File

@ -1,133 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
<PageHeader as |p|>
<p.top>
<KeyValueHeader
@baseKey={{hash display=this.model.id id=this.model.idForNav}}
@path="vault.cluster.secrets.backend.list"
@mode={{this.mode}}
@root={{this.root}}
@showCurrent={{true}}
/>
</p.top>
<p.levelLeft>
<h1 class="title is-3" data-test-secret-header="true">
{{#if (eq this.mode "create")}}
Create Alphabet
{{else if (eq this.mode "edit")}}
Edit Alphabet
{{else}}
Alphabet
<code>{{this.model.id}}</code>
{{/if}}
</h1>
</p.levelLeft>
</PageHeader>
{{#if (eq this.mode "show")}}
<Toolbar>
<ToolbarActions>
{{#if this.capabilities.canDelete}}
<button type="button" class="toolbar-link" onclick={{action "delete"}} data-test-transformation-alphabet-delete>
Delete alphabet
</button>
<div class="toolbar-separator"></div>
{{/if}}
{{#if this.capabilities.canUpdate}}
<ToolbarSecretLink
@secret={{concat this.model.idPrefix this.model.id}}
@mode="edit"
data-test-edit-link={{true}}
@replace={{true}}
>
Edit alphabet
</ToolbarSecretLink>
{{/if}}
</ToolbarActions>
</Toolbar>
{{/if}}
{{#if (or (eq this.mode "edit") (eq this.mode "create"))}}
<form onsubmit={{action "createOrUpdate" this.mode}}>
<div class="box is-sideless is-fullwidth is-marginless">
<MessageError @model={{this.model}} />
<NamespaceReminder @mode={{this.mode}} @noun="transform alphabet" />
{{#each this.model.attrs as |attr|}}
{{#if (and (eq attr.name "name") (eq this.mode "edit"))}}
<label for={{attr.name}} class="is-label">
{{attr.options.label}}
</label>
{{#if attr.options.subText}}
<p class="sub-text">{{attr.options.subText}}</p>
{{/if}}
<input
data-test-input={{attr.name}}
id={{attr.name}}
autocomplete="off"
spellcheck="false"
value={{or (get this.model attr.name) this.model.id}}
readonly
class="field input is-readOnly"
type={{attr.type}}
/>
{{else}}
<FormField data-test-field @attr={{attr}} @model={{this.model}} />
{{/if}}
{{/each}}
</div>
<div class="field is-grouped-split box is-fullwidth is-bottomless">
<div class="control">
<button
type="submit"
disabled={{this.buttonDisabled}}
class="button is-primary"
data-test-alphabet-transform-create={{true}}
>
{{#if (eq this.mode "create")}}
Create alphabet
{{else if (eq this.mode "edit")}}
Save
{{/if}}
</button>
<SecretLink
@mode={{if (eq this.mode "create") "list" "show"}}
class="button"
@secret={{concat this.model.idPrefix this.model.id}}
>
Cancel
</SecretLink>
</div>
</div>
</form>
{{else}}
{{#if this.model.displayErrors}}
<div class="has-top-margin-s">
<MessageError @model={{this.model}} />
</div>
{{/if}}
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
{{#each this.model.attrs as |attr|}}
{{#if (eq attr.type "object")}}
<InfoTableRow
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{stringify (get this.model attr.name)}}
/>
{{else if (eq attr.type "array")}}
<InfoTableRow
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{get this.model attr.name}}
@type={{attr.type}}
@isLink={{eq attr.name "transformations"}}
/>
{{else}}
<InfoTableRow
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{get this.model attr.name}}
/>
{{/if}}
{{/each}}
</div>
{{/if}}

View File

@ -1,75 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
{{#if (and @item.updatePath.canRead (not this.isBuiltin))}}
<LinkedBlock
@params={{array "vault.cluster.secrets.backend.show" @itemPath}}
class="list-item-row"
data-test-secret-link={{@itemPath}}
@encode={{true}}
@queryParams={{secret-query-params @backendType}}
>
<div class="columns is-mobile">
<div class="column is-10">
<SecretLink
@mode="show"
@secret={{@itemPath}}
@queryParams={{hash type=@modelType}}
class="has-text-black has-text-weight-semibold"
>
<Icon @name="file" class="has-text-grey-light" />
{{if (eq @item.id " ") "(self)" (or @item.keyWithoutParent @item.id)}}
</SecretLink>
</div>
<div class="column has-text-right">
{{#if (or @item.updatePath.canRead @item.updatePath.canUpdate)}}
<PopupMenu name="secret-menu">
<nav class="menu">
<ul class="menu-list">
{{#if @item.updatePath.canRead}}
<li class="action">
<SecretLink @mode="show" @secret={{@itemPath}} class="has-text-black has-text-weight-semibold">
Details
</SecretLink>
</li>
{{/if}}
{{#if @item.updatePath.canUpdate}}
<li class="action">
<SecretLink @mode="edit" @secret={{@itemPath}} class="has-text-black has-text-weight-semibold">
Edit
</SecretLink>
</li>
{{/if}}
</ul>
</nav>
</PopupMenu>
{{/if}}
</div>
</div>
</LinkedBlock>
{{else}}
<div class="list-item-row" data-test-view-only-list-item>
<div class="columns is-mobile">
<div class="column is-12 has-text-grey has-text-weight-semibold">
<Icon @name="file" class="has-text-grey-light" />
{{#if this.isBuiltin}}
<ToolTip @verticalPosition="above" @horizontalPosition="left" as |T|>
<T.Trigger @tabindex={{false}}>
{{@item.id}}
</T.Trigger>
<T.Content @defaultClass="tool-tip">
<div class="box">
This is a built-in HashiCorp
{{@itemType}}. It can't be viewed or edited.
</div>
</T.Content>
</ToolTip>
{{else}}
{{@item.id}}
{{/if}}
</div>
</div>
</div>
{{/if}}

View File

@ -1,70 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
{{! CBS TODO do not let click if !canRead }}
{{#if (eq @options.item "transformation")}}
<LinkedBlock
@params={{array "vault.cluster.secrets.backend.show" @item.id}}
class="list-item-row"
data-test-secret-link={{@item.id}}
@encode={{true}}
@queryParams={{secret-query-params @backendModel.type}}
>
<div class="columns is-mobile">
<div class="column is-10">
<SecretLink
@mode="show"
@secret={{@item.id}}
@queryParams={{if (eq @backendModel.type "transform") (hash tab="actions") ""}}
class="has-text-black has-text-weight-semibold"
>
<Icon @name="file" class="has-text-grey-light" />
{{if (eq @item.id " ") "(self)" (or @item.keyWithoutParent @item.id)}}
</SecretLink>
</div>
<div class="column has-text-right">
{{#if (or @item.updatePath.canRead @item.updatePath.canUpdate)}}
<PopupMenu name="secret-menu">
<nav class="menu">
<ul class="menu-list">
{{#if (or @item.versionPath.isLoading @item.secretPath.isLoading)}}
<li class="action">
<button disabled type="button" class="link button is-loading is-transparent">
loading
</button>
</li>
{{else}}
{{#if @item.updatePath.canRead}}
<li class="action">
<SecretLink @mode="show" @secret={{@item.id}} class="has-text-black has-text-weight-semibold">
Details
</SecretLink>
</li>
{{/if}}
{{#if @item.updatePath.canUpdate}}
<li class="action">
<SecretLink @mode="edit" @secret={{@item.id}} class="has-text-black has-text-weight-semibold">
Edit
</SecretLink>
</li>
{{/if}}
{{/if}}
</ul>
</nav>
</PopupMenu>
{{/if}}
</div>
</div>
</LinkedBlock>
{{else}}
<div class="list-item-row">
<div class="columns is-mobile">
<div class="column is-12 has-text-grey has-text-weight-semibold">
<Icon @name="file" class="has-text-grey-light" />
{{if (eq @item.id " ") "(self)" (or @item.keyWithoutParent @item.id)}}
</div>
</div>
</div>
{{/if}}

View File

@ -1,63 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
<ToggleButton
@isOpen={{this.showForm}}
@openLabel="Advanced templating"
@closedLabel="Advanced templating"
@onClick={{fn (mut this.showForm)}}
data-test-toggle-advanced={{true}}
/>
{{#if this.showForm}}
<div class="box has-container is-fullwidth">
<h4 class="title is-5">Advanced templating</h4>
<p>
Using your template's regex as a starting point, you can specify which parts of your input to encode and decode. For
example, you may want to handle input formatting or only decode part of an input. For more information, see
<DocLink @path="/vault/tutorials/adp/transform#advanced-handling">
our documentation.
</DocLink>
</p>
<div class="has-top-margin-l">
<RegexValidator
@value={{@model.pattern}}
@testInputLabel="Sample input"
@testInputSubText="Enter a sample input to match against your regex and identify capture groups. Optional."
@showGroups={{true}}
@onValidate={{this.setInputOptions}}
/>
</div>
<AutocompleteInput
@label="Encode format"
@subText="Use the groups above to define how the input will be encoded. Refer to each group with $N. This is optional; if not specified, pattern will be used."
@value={{@model.encodeFormat}}
@optionsTrigger="$"
@options={{this.inputOptions}}
@onChange={{fn (mut @model.encodeFormat)}}
class="has-top-margin-l"
data-test-encode-format
/>
<KvObjectEditor
@value={{@model.decodeFormats}}
@onChange={{fn (mut @model.decodeFormats)}}
@label="Decode formats"
@subText="Using the groups above, define how this data will be decoded. Multiple decode_formats can be used. Optional. If not specified, pattern will be used."
@keyPlaceholder="name"
class="has-top-margin-l"
data-test-kv-object-editor
as |kvObject kvData|
>
<AutocompleteInput
@value={{kvObject.value}}
@placeholder="format"
@optionsTrigger="$"
@options={{this.inputOptions}}
@onChange={{fn this.decodeFormatValueChange kvObject kvData}}
data-test-decode-format
/>
</KvObjectEditor>
</div>
{{/if}}

View File

@ -1,28 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
<form onsubmit={{action "createOrUpdate" "create"}}>
<div class="box is-sideless is-fullwidth is-marginless">
<MessageError @model={{this.model}} />
<NamespaceReminder @mode={{this.mode}} @noun="transformation" />
{{#each this.model.transformFieldAttrs as |attr|}}
<FormField data-test-field @attr={{attr}} @model={{this.model}} />
{{/each}}
</div>
<div class="field is-grouped-split box is-fullwidth is-bottomless">
<div class="control">
<button type="submit" disabled={{this.buttonDisabled}} class="button is-primary" data-test-transform-create={{true}}>
{{#if (eq this.mode "create")}}
Create transformation
{{else if (eq this.mode "edit")}}
Save
{{/if}}
</button>
<SecretLink @mode={{if (eq this.mode "create") "list" "show"}} class="button" @secret={{this.model.id}}>
Cancel
</SecretLink>
</div>
</div>
</form>

View File

@ -1,64 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
<form onsubmit={{action "createOrUpdate" "create"}}>
<div class="box is-sideless is-fullwidth is-marginless">
<MessageError @model={{this.model}} />
<NamespaceReminder @mode={{this.mode}} @noun="transformation" />
{{#each this.model.transformFieldAttrs as |attr|}}
{{#if (or (eq attr.name "name") (eq attr.name "type"))}}
<label for={{attr.name}} class="is-label">
{{attr.options.label}}
</label>
{{#if attr.options.subText}}
<p class="sub-text">{{attr.options.subText}}</p>
{{/if}}
{{#if attr.options.possibleValues}}
<div class="control is-expanded field is-readOnly">
<div class="select is-fullwidth">
<select name={{attr.name}} id={{attr.name}} disabled data-test-input={{attr.name}}>
<option selected={{get this.model attr.name}} value={{get this.model attr.name}}>
{{get this.model attr.name}}
</option>
</select>
</div>
</div>
{{else}}
<input
data-test-input={{attr.name}}
id={{attr.name}}
autocomplete="off"
spellcheck="false"
value={{or (get this.model attr.name) attr.options.defaultValue}}
readonly
class="field input is-readOnly"
type={{attr.type}}
/>
{{/if}}
{{else}}
<FormField data-test-field @attr={{attr}} @model={{this.model}} />
{{/if}}
{{/each}}
</div>
<div class="field is-grouped-split box is-fullwidth is-bottomless">
<div class="control">
<button
type="submit"
disabled={{this.buttonDisabled}}
class="button is-primary"
data-test-transformation-save-button={{true}}
>
{{#if (eq this.mode "create")}}
Create transformation
{{else if (eq this.mode "edit")}}
Save
{{/if}}
</button>
<SecretLink @mode={{if (eq this.mode "create") "list" "show"}} class="button" @secret={{this.model.id}}>
Cancel
</SecretLink>
</div>
</div>
</form>

View File

@ -1,122 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
<PageHeader as |p|>
<p.top>
<KeyValueHeader
@baseKey={{hash display=this.model.id id=this.model.idForNav}}
@path="vault.cluster.secrets.backend.list"
@mode={{this.mode}}
@root={{this.root}}
@showCurrent={{true}}
/>
</p.top>
<p.levelLeft>
<h1 class="title is-3" data-test-secret-header="true">
{{#if (eq this.mode "create")}}
Create Role
{{else if (eq this.mode "edit")}}
Edit Role
{{else}}
Role
<code>{{this.model.id}}</code>
{{/if}}
</h1>
</p.levelLeft>
</PageHeader>
{{#if (eq this.mode "show")}}
<Toolbar>
<ToolbarActions>
{{#if this.capabilities.canDelete}}
<ConfirmAction
@buttonClasses="toolbar-link"
@onConfirmAction={{action "delete"}}
@confirmTitle="Are you sure?"
@confirmMessage="Deleting this role means that youll need to recreate it and reassign any existing transformations to use it again."
@confirmButtonText="Delete"
data-test-transformation-role-delete
>
Delete role
</ConfirmAction>
<div class="toolbar-separator"></div>
{{/if}}
{{#if this.capabilities.canUpdate}}
<ToolbarSecretLink
@secret={{concat this.model.idPrefix this.model.id}}
@mode="edit"
data-test-edit-link={{true}}
@replace={{true}}
>
Edit role
</ToolbarSecretLink>
{{/if}}
</ToolbarActions>
</Toolbar>
{{/if}}
{{#if (or (eq this.mode "edit") (eq this.mode "create"))}}
<form onsubmit={{action "createOrUpdate" this.mode}}>
<div class="box is-sideless is-fullwidth is-marginless">
<MessageError @model={{this.model}} />
<NamespaceReminder @mode={{this.mode}} @noun="Transform role" />
{{#each this.model.attrs as |attr|}}
{{#if (and (eq this.mode "edit") attr.options.readOnly)}}
<ReadonlyFormField @attr={{attr}} @value={{get this.model attr.name}} />
{{else}}
<FormField data-test-field @attr={{attr}} @model={{this.model}} />
{{/if}}
{{/each}}
</div>
<div class="field is-grouped-split box is-fullwidth is-bottomless">
<div class="control">
<button
type="submit"
disabled={{this.buttonDisabled}}
class="button is-primary"
data-test-role-transform-create={{true}}
>
{{#if (eq this.mode "create")}}
Create role
{{else if (eq this.mode "edit")}}
Save
{{/if}}
</button>
{{#if (eq this.mode "create")}}
<LinkTo @route={{"vault.cluster.secrets.backend.list-root"}} @query={{hash tab="role"}} class="button">
Cancel
</LinkTo>
{{else if (eq this.mode "edit")}}
<LinkTo @route="vault.cluster.secrets.backend.show" @model={{concat "role/" this.model.id}} class="button">
Cancel
</LinkTo>
{{/if}}
</div>
</div>
</form>
{{else}}
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
{{#each this.model.attrs as |attr|}}
{{#if (eq attr.type "object")}}
<InfoTableRow
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{stringify (get this.model attr.name)}}
/>
{{else if (eq attr.type "array")}}
<InfoTableRow
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{get this.model attr.name}}
@type={{attr.type}}
@isLink={{eq attr.name "transformations"}}
/>
{{else}}
<InfoTableRow
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{get this.model attr.name}}
/>
{{/if}}
{{/each}}
</div>
{{/if}}

View File

@ -1,79 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
{{#each this.model.transformFieldAttrs as |attr|}}
{{#if (eq attr.type "object")}}
<InfoTableRow
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{stringify (get this.model attr.name)}}
/>
{{else if (eq attr.type "array")}}
<InfoTableRow
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{get this.model attr.name}}
@type={{attr.type}}
@isLink={{eq attr.name "allowed_roles"}}
@queryParam="role"
@modelType="transform/role"
@wildcardLabel={{attr.options.wildcardLabel}}
@backend={{this.model.backend}}
/>
{{else}}
<InfoTableRow
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{get this.model attr.name}}
@type={{attr.type}}
/>
{{/if}}
{{/each}}
</div>
<div class="has-top-margin-xl has-bottom-margin-s">
<label class="title has-border-bottom-light page-header">CLI Commands</label>
<div class="has-bottom-margin-s">
<h2 class="title is-6">Encode</h2>
<div class="has-bottom-margin-s">
<span class="helper-text has-text-grey">
To test the encoding capability of your transformation, use the following command. It will output an encoded_value.
</span>
</div>
<div class="copy-text level">
{{#let (concat "vault write " this.model.backend "/encode/" this.cliCommand) as |copyEncodeCommand|}}
<code>vault write {{this.model.backend}}/encode/{{this.cliCommand}}</code>
<CopyButton
class="button is-transparent level-right"
@clipboardText={{copyEncodeCommand}}
@buttonType="button"
@success={{action (set-flash-message "Command copied!")}}
>
<Icon @name="clipboard-copy" aria-label="Copy" />
</CopyButton>
{{/let}}
</div>
</div>
<div>
<h2 class="title is-6">Decode</h2>
<div class="has-bottom-margin-s">
<span class="helper-text has-text-grey">
To test decoding capability of your transformation, use the encoded_value in the following command. It should return
your original input.
</span>
</div>
<div class="copy-text level">
{{#let (concat "vault write " this.model.backend "/decode/" this.cliCommand) as |copyDecodeCommand|}}
<code>vault write {{this.model.backend}}/decode/{{this.cliCommand}}</code>
<CopyButton
class="button is-transparent level-right"
@clipboardText={{copyDecodeCommand}}
@buttonType="button"
@success={{action (set-flash-message "Command copied!")}}
>
<Icon @name="clipboard-copy" aria-label="Copy" />
</CopyButton>
{{/let}}
</div>
</div>
</div>

View File

@ -1,140 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
<PageHeader as |p|>
<p.top>
<KeyValueHeader
@baseKey={{hash display=this.model.id id=this.model.idForNav}}
@path="vault.cluster.secrets.backend.list"
@mode={{this.mode}}
@root={{this.root}}
@showCurrent={{true}}
/>
</p.top>
<p.levelLeft>
<h1 class="title is-3" data-test-secret-header="true">
{{#if (eq this.mode "create")}}
Create Template
{{else if (eq this.mode "edit")}}
Edit Template
{{else}}
Template
<code>{{this.model.id}}</code>
{{/if}}
</h1>
</p.levelLeft>
</PageHeader>
{{#if (eq this.mode "show")}}
<Toolbar>
<ToolbarActions>
{{#if this.capabilities.canDelete}}
<button type="button" class="toolbar-link" onclick={{action "delete"}} data-test-transformation-template-delete>
Delete template
</button>
<div class="toolbar-separator"></div>
{{/if}}
{{#if this.capabilities.canUpdate}}
<ToolbarSecretLink
@secret={{concat this.model.idPrefix this.model.id}}
@mode="edit"
data-test-edit-link={{true}}
@replace={{true}}
>
Edit template
</ToolbarSecretLink>
{{/if}}
</ToolbarActions>
</Toolbar>
{{/if}}
{{#if (or (eq this.mode "edit") (eq this.mode "create"))}}
<form onsubmit={{action "createOrUpdate" this.mode}}>
<div class="box is-sideless is-fullwidth is-marginless">
<MessageError @model={{this.model}} />
<NamespaceReminder @mode={{this.mode}} @noun="transform template" />
{{#each this.model.writeAttrs as |attr|}}
{{#if (and (eq attr.name "name") (eq this.mode "edit"))}}
<label for={{attr.name}} class="is-label">
{{attr.options.label}}
</label>
{{#if attr.options.subText}}
<p class="sub-text">{{attr.options.subText}}</p>
{{/if}}
<input
data-test-input={{attr.name}}
id={{attr.name}}
autocomplete="off"
spellcheck="false"
value={{or (get this.model attr.name) attr.options.defaultValue}}
readonly={{true}}
class="field input is-readOnly"
type={{attr.type}}
/>
{{else}}
{{#if (eq attr.name "alphabet")}}
<TransformAdvancedTemplating @model={{this.model}} />
{{/if}}
<FormField data-test-field @attr={{attr}} @model={{this.model}} />
{{/if}}
{{/each}}
</div>
<div class="field is-grouped-split box is-fullwidth is-bottomless">
<div class="control">
<button
type="submit"
disabled={{this.buttonDisabled}}
class="button is-primary"
data-test-template-transform-create={{true}}
>
{{#if (eq this.mode "create")}}
Create template
{{else if (eq this.mode "edit")}}
Save
{{/if}}
</button>
<SecretLink
@mode={{if (eq this.mode "create") "list" "show"}}
class="button"
@secret={{concat this.model.idPrefix this.model.id}}
>
Cancel
</SecretLink>
</div>
</div>
</form>
{{else}}
{{#if this.model.displayErrors}}
<div class="has-top-margin-s">
<MessageError @model={{this.model}} />
</div>
{{/if}}
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
{{#each this.model.readAttrs as |attr|}}
{{#let (capitalize (or attr.options.label (humanize (dasherize attr.name)))) as |label|}}
{{#if (eq attr.name "decodeFormats")}}
{{#if (not (is-empty-value this.model.decodeFormats))}}
<InfoTableRow @label={{label}}>
<div>
{{#each-in this.model.decodeFormats as |key value|}}
<div class="transform-decode-formats">
<p class="is-label has-text-grey-400">{{key}}</p>
<p>{{value}}</p>
</div>
{{/each-in}}
</div>
</InfoTableRow>
{{/if}}
{{else}}
<InfoTableRow
@label={{label}}
@value={{get this.model attr.name}}
class={{if (eq attr.name "pattern") "transform-pattern-text"}}
/>
{{/if}}
{{/let}}
{{/each}}
</div>
{{/if}}

View File

@ -1,125 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
<PageHeader as |p|>
<p.top>
<KeyValueHeader
@baseKey={{this.model}}
@path="vault.cluster.secrets.backend.list"
@mode={{this.mode}}
@root={{this.root}}
@showCurrent={{true}}
/>
</p.top>
<p.levelLeft>
<h1 class="title is-3" data-test-secret-header="true">
{{#if (eq this.mode "create")}}
Create transformation
{{else if (eq this.mode "edit")}}
Edit transformation
{{else}}
Transformation
<code>{{this.model.id}}</code>
{{/if}}
</h1>
</p.levelLeft>
</PageHeader>
{{#if (eq this.mode "show")}}
<Toolbar>
<ToolbarActions>
{{#if this.capabilities.canDelete}}
{{#if (gt this.model.allowed_roles.length 0)}}
<ToolTip @verticalPosition="above" @horizontalPosition="center" as |T|>
<T.Trigger @tabindex="-1">
<button class="toolbar-link" aria-disabled="true" type="button" disabled>
Delete transformation
</button>
</T.Trigger>
<T.Content @defaultClass="tool-tip">
<div class="box">
This transformation is in use by a role and cant be deleted.
</div>
</T.Content>
</ToolTip>
{{else}}
<button class="toolbar-link" onclick={{action (mut this.isDeleteModalActive) true}} type="button">
Delete transformation
</button>
{{/if}}
<div class="toolbar-separator"></div>
{{/if}}
{{#if this.capabilities.canUpdate}}
{{#if (gt this.model.allowed_roles.length 0)}}
<button
class="toolbar-link"
onclick={{action (mut this.isEditModalActive) true}}
type="button"
data-test-edit-link
>
Edit transformation
</button>
{{else}}
<ToolbarSecretLink @secret={{this.model.id}} @mode="edit" data-test-edit-link={{true}} @replace={{true}}>
Edit transformation
</ToolbarSecretLink>
{{/if}}
{{/if}}
</ToolbarActions>
</Toolbar>
{{/if}}
{{#if (eq this.mode "edit")}}
<TransformEditForm @mode={{this.mode}} @model={{this.model}} />
{{else if (eq this.mode "create")}}
<TransformCreateForm @mode={{this.mode}} @model={{this.model}} />
{{else}}
<TransformShowTransformation @model={{this.model}} />
{{/if}}
<ConfirmationModal
@title="Delete transformation"
@onClose={{action (mut this.isDeleteModalActive) false}}
@isActive={{this.isDeleteModalActive}}
@confirmText={{this.model.name}}
@toConfirmMsg="deleting the transformation."
@onConfirm={{action "delete"}}
>
<p class="has-bottom-margin-m">
Deleting the
<strong>{{this.model.name}}</strong>
transformation means that the underlying keys are lost and the data encoded by the transformation are unrecoverable and
cannot be decoded.
</p>
<MessageError @model={{this.model}} @errorMessage={{this.error}} />
</ConfirmationModal>
<Modal
@title="Edit transformation"
@onClose={{action (mut this.isEditModalActive) false}}
@isActive={{this.isEditModalActive}}
@type="warning"
@showCloseButton={{true}}
>
<section class="modal-card-body">
<p>
Youre editing a transformation that is in use by at least one role. Editing it may mean that encode and decode
operations stop working. Are you sure?
</p>
</section>
<footer class="modal-card-foot modal-card-foot-outlined">
<LinkTo
@route="vault.cluster.secrets.backend.edit"
@model={{this.model.id}}
class="button is-primary"
data-test-edit-confirm-button={{true}}
>
Confirm
</LinkTo>
<button type="button" class="button is-secondary" onclick={{action (mut this.isEditModalActive) false}}>
Cancel
</button>
</footer>
</Modal>

View File

@ -18,7 +18,6 @@ const POSSIBLE_FEATURES = [
'Seal Wrapping',
'Control Groups',
'Namespaces',
'Transform Secrets Engine',
'Key Management Secrets Engine',
];

View File

@ -115,53 +115,6 @@ const SECRET_BACKENDS = {
},
],
},
transform: {
displayName: 'Transformation',
navigateTree: false,
listItemPartial: 'secret-list/transform-list-item',
firstStep: `To use transform, you'll need to create a transformation and a role.`,
tabs: [
{
name: 'transformations',
label: 'Transformations',
searchPlaceholder: 'Filter transformations',
item: 'transformation',
create: 'Create transformation',
editComponent: 'transformation-edit',
listItemPartial: 'secret-list/transform-transformation-item',
},
{
name: 'role',
modelPrefix: 'role/',
label: 'Roles',
searchPlaceholder: 'Filter roles',
item: 'role',
create: 'Create role',
tab: 'role',
editComponent: 'transform-role-edit',
},
{
name: 'template',
modelPrefix: 'template/',
label: 'Templates',
searchPlaceholder: 'Filter templates',
item: 'template',
create: 'Create template',
tab: 'template',
editComponent: 'transform-template-edit',
},
{
name: 'alphabet',
modelPrefix: 'alphabet/',
label: 'Alphabets',
searchPlaceholder: 'Filter alphabets',
item: 'alphabet',
create: 'Create alphabet',
tab: 'alphabet',
editComponent: 'alphabet-edit',
},
],
},
transit: {
searchPlaceholder: 'Filter keys',
item: 'key',

View File

@ -1,16 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1" y="1" width="10" height="10" rx="1" fill="white" stroke="#BAC1CC" stroke-width="2"/>
<rect x="13" y="13" width="10" height="10" rx="1" fill="white" stroke="#BAC1CC" stroke-width="2"/>
<rect x="19" y="6" width="2" height="2" fill="#BAC1CC"/>
<rect x="19" y="9" width="2" height="2" fill="#BAC1CC"/>
<rect x="11" y="21" width="2" height="2" transform="rotate(-180 11 21)" fill="#BAC1CC"/>
<rect x="8" y="21" width="2" height="2" transform="rotate(-180 8 21)" fill="#BAC1CC"/>
<rect x="5" y="21" width="2" height="2" transform="rotate(-180 5 21)" fill="#BAC1CC"/>
<rect x="5" y="18" width="2" height="2" transform="rotate(-180 5 18)" fill="#BAC1CC"/>
<rect x="5" y="15" width="2" height="2" transform="rotate(-180 5 15)" fill="#BAC1CC"/>
<path d="M7.02393 9H8.78174L6.98438 2.65869H5.01123L3.21826 9H4.81787L5.12109 7.60693H6.72949L7.02393 9ZM5.89453 4.08691H5.97803L6.479 6.44678H5.37598L5.89453 4.08691Z" fill="#BAC1CC"/>
<path d="M15.4248 17.3833L16.9761 18.1743L15.4248 18.9653L16.0269 20.0068L17.4902 19.062L17.3979 20.8022H18.6021L18.5098 19.0576L19.9731 20.0112L20.5752 18.9653L19.0195 18.1743L20.5752 17.3833L19.9731 16.3418L18.5142 17.291L18.6021 15.5464H17.3979L17.4858 17.291L16.0269 16.3374L15.4248 17.3833Z" fill="#BAC1CC"/>
<rect x="13" y="3" width="2" height="2" fill="#BAC1CC"/>
<rect x="16" y="3" width="2" height="2" fill="#BAC1CC"/>
<rect x="19" y="3" width="2" height="2" fill="#BAC1CC"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB