diff --git a/ui/app/adapters/transform/base.js b/ui/app/adapters/transform/base.js
index c7f4ccf9cb..4cac36605b 100644
--- a/ui/app/adapters/transform/base.js
+++ b/ui/app/adapters/transform/base.js
@@ -55,12 +55,11 @@ export default ApplicationAdapter.extend({
queryRecord(store, type, query) {
return this.ajax(this.url(query.backend, type.modelName, query.id), 'GET').then(result => {
+ // CBS TODO: Add name to response and unmap name <> id on models
return {
id: query.id,
...result,
};
});
},
-
- // buildUrl(modelName, id, snapshot, requestType, query, returns) {},
});
diff --git a/ui/app/components/transform-list-item.js b/ui/app/components/transform-list-item.js
new file mode 100644
index 0000000000..393ad3a96b
--- /dev/null
+++ b/ui/app/components/transform-list-item.js
@@ -0,0 +1,32 @@
+/**
+ * @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
+ *
+ * ```
+ * @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: '',
+
+ itemViewable: computed('item', 'itemType', function() {
+ const item = this.get('item');
+ if (this.itemType === 'alphabet' || this.itemType === 'template') {
+ return !item.get('id').startsWith('builtin/');
+ }
+ return true;
+ }),
+
+ backendType: 'transform',
+});
diff --git a/ui/app/components/transform-template-edit.js b/ui/app/components/transform-template-edit.js
new file mode 100644
index 0000000000..548e2dd85c
--- /dev/null
+++ b/ui/app/components/transform-template-edit.js
@@ -0,0 +1,3 @@
+import TransformBase from './transform-edit-base';
+
+export default TransformBase.extend({});
diff --git a/ui/app/helpers/options-for-backend.js b/ui/app/helpers/options-for-backend.js
index e805f097c4..8f0a7916b9 100644
--- a/ui/app/helpers/options-for-backend.js
+++ b/ui/app/helpers/options-for-backend.js
@@ -80,15 +80,14 @@ const SECRET_BACKENDS = {
editComponent: 'transform-role-edit',
},
{
- name: 'templates',
+ name: 'template',
modelPrefix: 'template/',
label: 'Templates',
searchPlaceholder: 'Filter templates',
- item: 'templates',
+ item: 'template',
create: 'Create template',
- tab: 'templates',
+ tab: 'template',
editComponent: 'transform-template-edit',
- hideCreate: true,
},
{
name: 'alphabets',
diff --git a/ui/app/models/transform/template.js b/ui/app/models/transform/template.js
index c84c59268c..1cae81c037 100644
--- a/ui/app/models/transform/template.js
+++ b/ui/app/models/transform/template.js
@@ -1,7 +1,52 @@
+import { computed } from '@ember/object';
import DS from 'ember-data';
+import { apiPath } from 'vault/macros/lazy-capabilities';
+import attachCapabilities from 'vault/lib/attach-capabilities';
+import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
-export default DS.Model.extend({
- name: DS.attr('string'),
- alphabet: DS.belongsTo('transform/alphabet'),
- transformations: DS.hasMany('transformation'),
+const { attr } = DS;
+
+const Model = DS.Model.extend({
+ idPrefix: 'template/',
+ idForNav: computed('id', 'idPrefix', function() {
+ let modelId = this.id || '';
+ return `${this.idPrefix}${modelId}`;
+ }),
+
+ name: attr('string', {
+ label: 'Name',
+ fieldValue: 'id',
+ 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', {
+ subText: 'The template’s 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',
+ fallbackComponent: 'string-list',
+ label: 'Alphabet',
+ models: ['transform/alphabet'],
+ selectLimit: 1,
+ }),
+
+ attrs: computed('pattern', 'alphabet', function() {
+ let keys = ['name', 'pattern', 'alphabet'];
+ return expandAttributeMeta(this, keys);
+ }),
+
+ editableAttrs: computed('pattern', 'alphabet', function() {
+ let keys = ['pattern', 'alphabet'];
+ return expandAttributeMeta(this, keys);
+ }),
+
+ backend: attr('string', { readOnly: true }),
+});
+
+export default attachCapabilities(Model, {
+ updatePath: apiPath`${'backend'}/template/${'id'}`,
});
diff --git a/ui/app/routes/vault/cluster/secrets/backend/list.js b/ui/app/routes/vault/cluster/secrets/backend/list.js
index 669e121640..e5c87f57a8 100644
--- a/ui/app/routes/vault/cluster/secrets/backend/list.js
+++ b/ui/app/routes/vault/cluster/secrets/backend/list.js
@@ -28,7 +28,7 @@ export default Route.extend({
case 'role':
modelType = 'transform/role';
break;
- case 'templates':
+ case 'template':
modelType = 'transform/template';
break;
case 'alphabets':
diff --git a/ui/app/serializers/transform/role.js b/ui/app/serializers/transform/role.js
index ed04af7935..5e35ae6a2c 100644
--- a/ui/app/serializers/transform/role.js
+++ b/ui/app/serializers/transform/role.js
@@ -1,4 +1,5 @@
import ApplicationSerializer from '../application';
+
export default ApplicationSerializer.extend({
extractLazyPaginatedData(payload) {
let ret;
diff --git a/ui/app/serializers/transform/template.js b/ui/app/serializers/transform/template.js
new file mode 100644
index 0000000000..18bb7bf280
--- /dev/null
+++ b/ui/app/serializers/transform/template.js
@@ -0,0 +1,34 @@
+import ApplicationSerializer from '../application';
+
+export default ApplicationSerializer.extend({
+ normalizeResponse(store, primaryModelClass, payload, id, requestType) {
+ payload.data.name = payload.id;
+ if (payload.data.alphabet) {
+ payload.data.alphabet = [payload.data.alphabet];
+ }
+ return this._super(store, primaryModelClass, payload, id, requestType);
+ },
+
+ serialize() {
+ let json = this._super(...arguments);
+ if (json.alphabet && Array.isArray(json.alphabet)) {
+ // Templates should only ever have one alphabet
+ json.alphabet = json.alphabet[0];
+ }
+ return json;
+ },
+
+ extractLazyPaginatedData(payload) {
+ let ret;
+ ret = payload.data.keys.map(key => {
+ let model = {
+ id: key,
+ };
+ if (payload.backend) {
+ model.backend = payload.backend;
+ }
+ return model;
+ });
+ return ret;
+ },
+});
diff --git a/ui/app/templates/components/transform-list-item.hbs b/ui/app/templates/components/transform-list-item.hbs
new file mode 100644
index 0000000000..13f4087099
--- /dev/null
+++ b/ui/app/templates/components/transform-list-item.hbs
@@ -0,0 +1,66 @@
+{{#if (and itemViewable item.updatePath.canRead)}}
+ {{#linked-block
+ "vault.cluster.secrets.backend.show"
+ itemPath
+ class="list-item-row"
+ data-test-secret-link=itemPath
+ encode=true
+ queryParams=(secret-query-params backendType)
+ }}
+
+
+
+
+ {{if (eq item.id ' ') '(self)' (or item.keyWithoutParent item.id)}}
+
+
+
+ {{#if (or item.updatePath.canRead item.updatePath.canUpdate)}}
+
+
+
+ {{/if}}
+
+
+ {{/linked-block}}
+{{else}}
+
+
+
+
+ {{if (eq item.id ' ') '(self)' (or item.keyWithoutParent item.id)}}
+
+
+
+{{/if}}
diff --git a/ui/app/templates/components/transform-template-edit.hbs b/ui/app/templates/components/transform-template-edit.hbs
new file mode 100644
index 0000000000..18c31fb5f5
--- /dev/null
+++ b/ui/app/templates/components/transform-template-edit.hbs
@@ -0,0 +1,123 @@
+
+
+ {{key-value-header
+ baseKey=(hash display=model.id id=model.idForNav)
+ path="vault.cluster.secrets.backend.list"
+ mode=mode
+ root=root
+ showCurrent=true
+ }}
+
+
+
+ {{#if (eq mode "create") }}
+ Create Template
+ {{else if (eq mode "edit")}}
+ Edit Template
+ {{else}}
+ Template {{model.id}}
+ {{/if}}
+
+
+
+
+{{#if (eq mode "show")}}
+
+
+ {{#if capabilities.canDelete}}
+
+ {{/if}}
+ {{#if capabilities.canUpdate }}
+
+ Edit template
+
+ {{/if}}
+
+
+{{/if}}
+
+{{#if (or (eq mode 'edit') (eq mode 'create'))}}
+
+{{else}}
+
+ {{#each model.attrs as |attr|}}
+ {{#if (eq attr.type "object")}}
+ {{info-table-row label=(capitalize (or attr.options.label (humanize (dasherize attr.name)))) value=(stringify (get model attr.name))}}
+ {{else if (eq attr.type "array")}}
+ {{info-table-row
+ label=(capitalize (or attr.options.label (humanize (dasherize attr.name))))
+ value=(get model attr.name)
+ type=attr.type
+ viewAll=attr.name
+ }}
+ {{else}}
+ {{info-table-row label=(capitalize (or attr.options.label (humanize (dasherize attr.name)))) value=(get model attr.name)}}
+ {{/if}}
+ {{/each}}
+
+{{/if}}
diff --git a/ui/app/templates/partials/secret-list/transform-list-item.hbs b/ui/app/templates/partials/secret-list/transform-list-item.hbs
index dd3951b324..5f2ba5027e 100644
--- a/ui/app/templates/partials/secret-list/transform-list-item.hbs
+++ b/ui/app/templates/partials/secret-list/transform-list-item.hbs
@@ -1,77 +1,5 @@
-{{!-- TODO do not let click if !canRead --}}
-{{#if (eq options.item "role")}}
- {{#let (concat options.modelPrefix item.id) as |itemPath|}}
- {{#linked-block
- "vault.cluster.secrets.backend.show"
- itemPath
- class="list-item-row"
- data-test-secret-link=itemPath
- encode=true
- queryParams=(secret-query-params backendModel.type)
- }}
-
-
-
-
- {{if (eq item.id ' ') '(self)' (or item.keyWithoutParent item.id)}}
-
-
-
- {{#if (or item.updatePath.canRead item.updatePath.canUpdate)}}
-
-
-
- {{/if}}
-
-
- {{/linked-block}}
- {{/let}}
-{{else}}
-
-
-
-
- {{if (eq item.id ' ') '(self)' (or item.keyWithoutParent item.id)}}
-
-
-
-{{/if}}
+
diff --git a/ui/tests/integration/components/transform-list-item-test.js b/ui/tests/integration/components/transform-list-item-test.js
new file mode 100644
index 0000000000..b0ee8c56f0
--- /dev/null
+++ b/ui/tests/integration/components/transform-list-item-test.js
@@ -0,0 +1,121 @@
+import EmberObject from '@ember/object';
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, findAll, click } from '@ember/test-helpers';
+import hbs from 'htmlbars-inline-precompile';
+
+module('Integration | Component | transform-list-item', function(hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders un-clickable item if no read capability', async function(assert) {
+ let item = EmberObject.create({
+ id: 'foo',
+ updatePath: {
+ canRead: false,
+ canDelete: true,
+ canUpdate: true,
+ },
+ });
+ this.set('itemPath', 'role/foo');
+ this.set('itemType', 'role');
+ this.set('item', item);
+ await render(hbs``);
+
+ assert.dom('[data-test-view-only-list-item]').exists('shows view only list item');
+ assert.dom('[data-test-view-only-list-item]').hasText(item.id, 'has correct label');
+ });
+
+ test('it is clickable with details menu item if read capability', async function(assert) {
+ let item = EmberObject.create({
+ id: 'foo',
+ updatePath: {
+ canRead: true,
+ canDelete: false,
+ canUpdate: false,
+ },
+ });
+ this.set('itemPath', 'template/foo');
+ this.set('itemType', 'template');
+ this.set('item', item);
+ await render(hbs``);
+
+ assert.dom('[data-test-secret-link="template/foo"]').exists('shows clickable list item');
+ await click('button.popup-menu-trigger');
+ assert.equal(findAll('.popup-menu-content li').length, 1, 'has one option');
+ });
+
+ test('it has details and edit menu item if read & edit capabilities', async function(assert) {
+ let item = EmberObject.create({
+ id: 'foo',
+ updatePath: {
+ canRead: true,
+ canDelete: true,
+ canUpdate: true,
+ },
+ });
+ this.set('itemPath', 'alphabet/foo');
+ this.set('itemType', 'alphabet');
+ this.set('item', item);
+ await render(hbs``);
+
+ assert.dom('[data-test-secret-link="alphabet/foo"]').exists('shows clickable list item');
+ await click('button.popup-menu-trigger');
+ assert.equal(findAll('.popup-menu-content li').length, 2, 'has both options');
+ });
+
+ test('it is not clickable if built-in template with all capabilities', async function(assert) {
+ let item = EmberObject.create({
+ id: 'builtin/foo',
+ updatePath: {
+ canRead: true,
+ canDelete: true,
+ canUpdate: true,
+ },
+ });
+ this.set('itemPath', 'template/builtin/foo');
+ this.set('itemType', 'template');
+ this.set('item', item);
+ await render(hbs``);
+
+ assert.dom('[data-test-view-only-list-item]').exists('shows view only list item');
+ assert.dom('[data-test-view-only-list-item]').hasText(item.id, 'has correct label');
+ });
+
+ test('it is not clickable if built-in alphabet', async function(assert) {
+ let item = EmberObject.create({
+ id: 'builtin/foo',
+ updatePath: {
+ canRead: true,
+ canDelete: true,
+ canUpdate: true,
+ },
+ });
+ this.set('itemPath', 'alphabet/builtin/foo');
+ this.set('itemType', 'alphabet');
+ this.set('item', item);
+ await render(hbs``);
+
+ assert.dom('[data-test-view-only-list-item]').exists('shows view only list item');
+ assert.dom('[data-test-view-only-list-item]').hasText(item.id, 'has correct label');
+ });
+});
diff --git a/ui/tests/integration/components/transform-template-edit-test.js b/ui/tests/integration/components/transform-template-edit-test.js
new file mode 100644
index 0000000000..f364e5a1a7
--- /dev/null
+++ b/ui/tests/integration/components/transform-template-edit-test.js
@@ -0,0 +1,26 @@
+import { module, skip } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render } from '@ember/test-helpers';
+import hbs from 'htmlbars-inline-precompile';
+
+module('Integration | Component | transform-template-edit', function(hooks) {
+ setupRenderingTest(hooks);
+
+ skip('it renders', async function(assert) {
+ // Set any properties with this.set('myProperty', 'value');
+ // Handle any actions with this.set('myAction', function(val) { ... });
+
+ await render(hbs`{{transform-template-edit}}`);
+
+ assert.equal(this.element.textContent.trim(), '');
+
+ // Template block usage:
+ await render(hbs`
+ {{#transform-template-edit}}
+ template block text
+ {{/transform-template-edit}}
+ `);
+
+ assert.equal(this.element.textContent.trim(), 'template block text');
+ });
+});