diff --git a/ui/app/adapters/transform/alphabet.js b/ui/app/adapters/transform/alphabet.js new file mode 100644 index 0000000000..d703adccd7 --- /dev/null +++ b/ui/app/adapters/transform/alphabet.js @@ -0,0 +1,7 @@ +import BaseAdapter from './base'; + +export default BaseAdapter.extend({ + pathForType() { + return 'alphabet'; + }, +}); diff --git a/ui/app/adapters/transform/base.js b/ui/app/adapters/transform/base.js new file mode 100644 index 0000000000..282731e6a8 --- /dev/null +++ b/ui/app/adapters/transform/base.js @@ -0,0 +1,63 @@ +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 serializer = store.serializerFor(type.modelName); + const data = serializer.serialize(snapshot); + const { id } = snapshot; + let url = this.url(snapshot.record.get('backend'), type.modelName, 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.url(snapshot.record.get('backend'), type.modelName, id), 'DELETE'); + }, + + url(backend, modelType, id) { + let type = this.pathForType(modelType); + let 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 => { + return resp; + }); + }, + + 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, + ...result, + }; + }); + }, + + // buildUrl(modelName, id, snapshot, requestType, query, returns) {}, +}); diff --git a/ui/app/adapters/transform/role.js b/ui/app/adapters/transform/role.js index aef79adf7c..2ccef954c9 100644 --- a/ui/app/adapters/transform/role.js +++ b/ui/app/adapters/transform/role.js @@ -1,25 +1,7 @@ -import ApplicationAdapater from '../application'; -import { encodePath } from 'vault/utils/path-encoding-helpers'; - -export default ApplicationAdapater.extend({ - namespace: 'v1', +import BaseAdapter from './base'; +export default BaseAdapter.extend({ pathForType() { return 'role'; }, - - _url(backend, id) { - let type = this.pathForType(); - let base = `/v1/${encodePath(backend)}/${type}`; - if (id) { - return `${base}/${encodePath(id)}`; - } - return base + '?list=true'; - }, - - query(store, type, query) { - return this.ajax(this._url(query.backend), 'GET').then(result => { - return result; - }); - }, }); diff --git a/ui/app/adapters/transform/template.js b/ui/app/adapters/transform/template.js index e88825677b..7649f2e9ce 100644 --- a/ui/app/adapters/transform/template.js +++ b/ui/app/adapters/transform/template.js @@ -1,25 +1,7 @@ -import ApplicationAdapater from '../application'; -import { encodePath } from 'vault/utils/path-encoding-helpers'; - -export default ApplicationAdapater.extend({ - namespace: 'v1', +import BaseAdapter from './base'; +export default BaseAdapter.extend({ pathForType() { return 'template'; }, - - _url(backend, id) { - let type = this.pathForType(); - let base = `${this.buildURL()}/${encodePath(backend)}/${type}`; - if (id) { - return `${base}/${encodePath(id)}`; - } - return base + '?list=true'; - }, - - query(store, type, query) { - return this.ajax(this._url(query.backend), 'GET').then(result => { - return result; - }); - }, }); diff --git a/ui/app/adapters/transform/transformation.js b/ui/app/adapters/transform/transformation.js new file mode 100644 index 0000000000..c0bda8af7e --- /dev/null +++ b/ui/app/adapters/transform/transformation.js @@ -0,0 +1,5 @@ +import BaseAdapter from './base'; + +export default BaseAdapter.extend({ + // custom stuff for transformation +}); diff --git a/ui/app/components/transform-edit-base.js b/ui/app/components/transform-edit-base.js index 3673a4582a..21421cd923 100644 --- a/ui/app/components/transform-edit-base.js +++ b/ui/app/components/transform-edit-base.js @@ -47,6 +47,14 @@ export default Component.extend(FocusOnInsertMixin, { this.get('router').transitionTo(...arguments); }, + modelPrefixFromType(modelType) { + let modelPrefix = ''; + if (modelType && modelType.startsWith('transform/')) { + modelPrefix = `${modelType.replace('transform/', '')}/`; + } + return modelPrefix; + }, + onEscape(e) { if (e.keyCode !== keys.ESC || this.get('mode') !== 'show') { return; @@ -74,6 +82,7 @@ export default Component.extend(FocusOnInsertMixin, { createOrUpdate(type, event) { event.preventDefault(); const modelId = this.get('model.id') || this.get('model.name'); // transform comes in as model.name + const modelPrefix = this.modelPrefixFromType(this.get('model.constructor.modelName')); // prevent from submitting if there's no key // maybe do something fancier later if (type === 'create' && isBlank(modelId)) { @@ -82,7 +91,7 @@ export default Component.extend(FocusOnInsertMixin, { this.persist('save', () => { this.hasDataChanges(); - this.transitionToRoute(SHOW_ROUTE, modelId); + this.transitionToRoute(SHOW_ROUTE, `${modelPrefix}${modelId}`); }); }, diff --git a/ui/app/components/transform-role-edit.js b/ui/app/components/transform-role-edit.js new file mode 100644 index 0000000000..548e2dd85c --- /dev/null +++ b/ui/app/components/transform-role-edit.js @@ -0,0 +1,3 @@ +import TransformBase from './transform-edit-base'; + +export default TransformBase.extend({}); diff --git a/ui/app/controllers/vault/cluster/secrets/backend/create.js b/ui/app/controllers/vault/cluster/secrets/backend/create.js index abf822816e..b73dd7e591 100644 --- a/ui/app/controllers/vault/cluster/secrets/backend/create.js +++ b/ui/app/controllers/vault/cluster/secrets/backend/create.js @@ -3,9 +3,10 @@ import BackendCrumbMixin from 'vault/mixins/backend-crumb'; export default Controller.extend(BackendCrumbMixin, { backendController: controller('vault.cluster.secrets.backend'), - queryParams: ['initialKey'], + queryParams: ['initialKey', 'itemType'], initialKey: '', + itemType: '', actions: { refresh: function() { diff --git a/ui/app/helpers/options-for-backend.js b/ui/app/helpers/options-for-backend.js index 667d55ce5f..79575157ee 100644 --- a/ui/app/helpers/options-for-backend.js +++ b/ui/app/helpers/options-for-backend.js @@ -68,40 +68,38 @@ const SECRET_BACKENDS = { create: 'Create transformation', editComponent: 'transformation-edit', }, - // TODO: Add tabs as needed - // { - // name: 'roles', - // modelPrefix: 'role/', - // label: 'Roles', - // searchPlaceholder: 'Filter roles', - // item: 'roles', - // create: 'Create role', - // tab: 'role', - // listItemPartial: 'partials/secret-list/item', - // editComponent: 'transform-role-edit', - // }, - // { - // name: 'templates', - // modelPrefix: 'template/', - // label: 'Templates', - // searchPlaceholder: 'Filter templates', - // item: 'templates', - // create: 'Create template', - // tab: 'template', - // listItemPartial: 'partials/secret-list/item', - // editComponent: 'transform-template-edit', - // }, - // { - // name: 'alphabets', - // modelPrefix: 'alphabet/', - // label: 'Alphabets', - // searchPlaceholder: 'Filter alphabets', - // item: 'alphabets', - // create: 'Create alphabet', - // tab: 'alphabet', - // listItemPartial: 'partials/secret-list/item', - // editComponent: 'alphabet-edit', - // }, + { + name: 'role', + modelPrefix: 'role/', + label: 'Roles', + searchPlaceholder: 'Filter roles', + item: 'role', + create: 'Create role', + tab: 'role', + editComponent: 'transform-role-edit', + }, + { + name: 'templates', + modelPrefix: 'template/', + label: 'Templates', + searchPlaceholder: 'Filter templates', + item: 'templates', + create: 'Create template', + tab: 'templates', + editComponent: 'transform-template-edit', + hideCreate: true, + }, + { + name: 'alphabets', + modelPrefix: 'alphabet/', + label: 'Alphabets', + searchPlaceholder: 'Filter alphabets', + item: 'alphabets', + create: 'Create alphabet', + tab: 'alphabets', + editComponent: 'alphabet-edit', + hideCreate: true, + }, ], }, transit: { diff --git a/ui/app/models/transform.js b/ui/app/models/transform.js index ccafb7f283..04e66f516a 100644 --- a/ui/app/models/transform.js +++ b/ui/app/models/transform.js @@ -92,5 +92,6 @@ const Model = DS.Model.extend({ }); export default attachCapabilities(Model, { + // TODO: Update to dynamic backend name updatePath: apiPath`transform/transformation/${'id'}`, }); diff --git a/ui/app/models/transform/alphabet.js b/ui/app/models/transform/alphabet.js new file mode 100644 index 0000000000..cf2a838964 --- /dev/null +++ b/ui/app/models/transform/alphabet.js @@ -0,0 +1,5 @@ +import DS from 'ember-data'; + +export default DS.Model.extend({ + templates: DS.hasMany('template'), +}); diff --git a/ui/app/models/transform/role.js b/ui/app/models/transform/role.js index 40c6bd6be1..6288fdbc96 100644 --- a/ui/app/models/transform/role.js +++ b/ui/app/models/transform/role.js @@ -1,3 +1,42 @@ +import { computed } from '@ember/object'; import DS from 'ember-data'; +import { apiPath } from 'vault/macros/lazy-capabilities'; +import { expandAttributeMeta } from 'vault/utils/field-to-attrs'; +import attachCapabilities from 'vault/lib/attach-capabilities'; -export default DS.Model.extend({}); +const { attr } = DS; + +const Model = DS.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() { + let modelId = this.id || ''; + return `${this.idPrefix}${modelId}`; + }), + + name: attr('string', { + // TODO: make this required for making a transformation + label: 'Name', + fieldValue: 'id', + readOnly: true, + subText: 'The name for your role. This cannot be edited later.', + }), + transformations: attr('string', { + editType: 'searchSelect', + fallbackComponent: 'string-list', + label: 'Transformations', + models: ['transform'], + subLabel: 'Transformations', + subText: 'Select which transformations this role will have access to. It must already exist.', + }), + + attrs: computed('transformations', function() { + let keys = ['name', 'transformations']; + return expandAttributeMeta(this, keys); + }), +}); + +export default attachCapabilities(Model, { + updatePath: apiPath`${'backend'}/role/${'id'}`, +}); diff --git a/ui/app/models/transform/template.js b/ui/app/models/transform/template.js index 40c6bd6be1..c84c59268c 100644 --- a/ui/app/models/transform/template.js +++ b/ui/app/models/transform/template.js @@ -1,3 +1,7 @@ import DS from 'ember-data'; -export default DS.Model.extend({}); +export default DS.Model.extend({ + name: DS.attr('string'), + alphabet: DS.belongsTo('transform/alphabet'), + transformations: DS.hasMany('transformation'), +}); diff --git a/ui/app/models/transform/transformation.js b/ui/app/models/transform/transformation.js new file mode 100644 index 0000000000..a99377051e --- /dev/null +++ b/ui/app/models/transform/transformation.js @@ -0,0 +1,7 @@ +import DS from 'ember-data'; + +export default DS.Model.extend({ + name: DS.attr('string'), + template: DS.belongsTo('transform/template'), + roles: DS.belongsTo('transform/role'), +}); diff --git a/ui/app/routes/vault/cluster/secrets/backend/create-root.js b/ui/app/routes/vault/cluster/secrets/backend/create-root.js index bc994308e5..87dd1aaec1 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/create-root.js +++ b/ui/app/routes/vault/cluster/secrets/backend/create-root.js @@ -20,14 +20,24 @@ let secretModel = (store, backend, key) => { return secret; }; +const transformModel = queryParams => { + let modelType = 'transform'; + if (!queryParams || !queryParams.itemType) return modelType; + + return `${modelType}/${queryParams.itemType}`; +}; + export default EditBase.extend({ wizard: service(), createModel(transition) { const { backend } = this.paramsFor('vault.cluster.secrets.backend'); - const modelType = this.modelType(backend); + let modelType = this.modelType(backend); if (modelType === 'role-ssh') { return this.store.createRecord(modelType, { keyType: 'ca' }); } + if (modelType === 'transform') { + modelType = transformModel(transition.queryParams); + } if (modelType !== 'secret' && modelType !== 'secret-v2') { if (this.get('wizard.featureState') === 'details' && this.get('wizard.componentState') === 'transit') { this.get('wizard').transitionFeatureMachine('details', 'CONTINUE', 'transit'); diff --git a/ui/app/routes/vault/cluster/secrets/backend/list.js b/ui/app/routes/vault/cluster/secrets/backend/list.js index eff65f4b6a..9c0c5e57e6 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/list.js +++ b/ui/app/routes/vault/cluster/secrets/backend/list.js @@ -22,6 +22,25 @@ export default Route.extend({ }, }, + modelTypeForTransform(tab) { + let modelType; + switch (tab) { + case 'role': + modelType = 'transform/role'; + break; + case 'templates': + modelType = 'transform/template'; + break; + case 'alphabets': + modelType = 'transform/alphabet'; + break; + default: + modelType = 'transform'; // CBS TODO: transform/transformation + break; + } + return modelType; + }, + secretParam() { let { secret } = this.paramsFor(this.routeName); return secret ? normalizePath(secret) : ''; @@ -56,7 +75,7 @@ export default Route.extend({ let types = { transit: 'transit-key', ssh: 'role-ssh', - transform: 'transform', + transform: this.modelTypeForTransform(tab), aws: 'role-aws', pki: tab === 'certs' ? 'pki-certificate' : 'role-pki', // secret or secret-v2 diff --git a/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js b/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js index 6fe3edd952..53ad0147fa 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js +++ b/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js @@ -71,7 +71,7 @@ export default Route.extend(UnloadModelRoute, { let types = { transit: 'transit-key', ssh: 'role-ssh', - transform: 'transform', + transform: secret && secret.startsWith('role/') ? 'transform/role' : 'transform', // CBS TODO: switch out better aws: 'role-aws', pki: secret && secret.startsWith('cert/') ? 'pki-certificate' : 'role-pki', cubbyhole: 'secret', @@ -192,13 +192,16 @@ export default Route.extend(UnloadModelRoute, { let secret = this.secretParam(); let backend = this.enginePathParam(); let modelType = this.modelType(backend, secret); - if (!secret) { secret = '\u0020'; } if (modelType === 'pki-certificate') { secret = secret.replace('cert/', ''); } + if (modelType.startsWith('transform/')) { + // CBS TODO: we'll have more things to replace than just role/ + secret = secret.replace('role/', ''); + } let secretModel; let capabilities = this.capabilities(secret); diff --git a/ui/app/templates/components/transform-role-edit.hbs b/ui/app/templates/components/transform-role-edit.hbs new file mode 100644 index 0000000000..d4bd13ff5e --- /dev/null +++ b/ui/app/templates/components/transform-role-edit.hbs @@ -0,0 +1,101 @@ + + + {{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 Role + {{else if (eq mode "edit")}} + Edit Role + {{else}} + Role {{model.id}} + {{/if}} +

+
+
+ +{{#if (eq mode "show")}} + + + {{!-- TODO: Ability to delete and edit role + {{#if (or capabilities.canUpdate capabilities.canDelete)}} +
+ {{/if}} + {{#if capabilities.canDelete}} + + Delete role + + {{/if}} + {{#if capabilities.canUpdate }} + + Edit role + + {{/if}} --}} + + +{{/if}} + +{{#if (or (eq mode 'edit') (eq mode 'create'))}} +
+
+ {{message-error model=model}} + + {{#each model.attrs as |attr|}} + + {{/each}} +
+
+
+ + {{#secret-link + mode=(if (eq mode "create") "list" "show") + class="button" + secret=model.id + }} + Cancel + {{/secret-link}} +
+
+
+{{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}} + {{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-transformation-item.hbs b/ui/app/templates/partials/secret-list/transform-transformation-item.hbs index 94ccc1f898..a6153585a0 100644 --- a/ui/app/templates/partials/secret-list/transform-transformation-item.hbs +++ b/ui/app/templates/partials/secret-list/transform-transformation-item.hbs @@ -1,60 +1,76 @@ {{!-- TODO do not let click if !canRead --}} -{{#linked-block - "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) -}} -
-
- - - {{if (eq item.id ' ') '(self)' (or item.keyWithoutParent item.id)}} - -
-
- - + + {{/if}} +
-
-{{/linked-block}} + {{/linked-block}} +{{else}} +
+
+
+ + {{if (eq item.id ' ') '(self)' (or item.keyWithoutParent item.id)}} +
+
+
+{{/if}} \ No newline at end of file diff --git a/ui/app/templates/vault/cluster/secrets/backend/list.hbs b/ui/app/templates/vault/cluster/secrets/backend/list.hbs index de56c78996..6ff8a89311 100644 --- a/ui/app/templates/vault/cluster/secrets/backend/list.hbs +++ b/ui/app/templates/vault/cluster/secrets/backend/list.hbs @@ -42,15 +42,17 @@ {{/if}} - - {{options.create}} - + {{#unless options.hideCreate}} + + {{options.create}} + + {{/unless}}
{{/if}} @@ -84,7 +86,7 @@ {{#secret-link mode="create" secret='' - queryParams=(query-params initialKey=(or filter baseKey.id)) + queryParams=(query-params initialKey=(or filter baseKey.id) itemType=tab) class="link" }} {{options.create}} diff --git a/ui/tests/integration/components/transform-role-edit-test.js b/ui/tests/integration/components/transform-role-edit-test.js new file mode 100644 index 0000000000..47b9592d6c --- /dev/null +++ b/ui/tests/integration/components/transform-role-edit-test.js @@ -0,0 +1,25 @@ +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-role-edit', function(hooks) { + setupRenderingTest(hooks); + + skip('it renders', async function(assert) { + // TODO: Fill out these tests, merging without to unblock other work + + await render(hbs`{{transform-role-edit}}`); + + assert.equal(this.element.textContent.trim(), ''); + + // Template block usage: + await render(hbs` + {{#transform-role-edit}} + template block text + {{/transform-role-edit}} + `); + + assert.equal(this.element.textContent.trim(), 'template block text'); + }); +});