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 @@
+
+ {{#if (eq mode "create") }}
+ Create Role
+ {{else if (eq mode "edit")}}
+ Edit Role
+ {{else}}
+ Role
+ {{model.id}}
+ {{/if}}
+