diff --git a/ui/app/adapters/transform.js b/ui/app/adapters/transform.js index 65ffca614e..cc81eeae72 100644 --- a/ui/app/adapters/transform.js +++ b/ui/app/adapters/transform.js @@ -66,22 +66,17 @@ export default ApplicationAdapter.extend({ results.forEach(result => { if (result.value) { - if (result.value.data.roles) { - // TODO: Check if this is needed and remove if not - resp.data = assign({}, resp.data, { zero_address_roles: result.value.data.roles }); - } else { - 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); + 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; diff --git a/ui/app/adapters/transform/base.js b/ui/app/adapters/transform/base.js index 282731e6a8..c7f4ccf9cb 100644 --- a/ui/app/adapters/transform/base.js +++ b/ui/app/adapters/transform/base.js @@ -42,7 +42,10 @@ export default ApplicationAdapter.extend({ fetchByQuery(query) { const { backend, modelName, id } = query; return this.ajax(this.url(backend, modelName, id), 'GET').then(resp => { - return resp; + return { + ...resp, + backend, + }; }); }, diff --git a/ui/app/components/transform-edit-base.js b/ui/app/components/transform-edit-base.js index 21421cd923..097c6ce691 100644 --- a/ui/app/components/transform-edit-base.js +++ b/ui/app/components/transform-edit-base.js @@ -1,22 +1,17 @@ import { inject as service } from '@ember/service'; import { or } from '@ember/object/computed'; import { isBlank } from '@ember/utils'; -import { task, waitForEvent } from 'ember-concurrency'; import Component from '@ember/component'; import { set, get } from '@ember/object'; import FocusOnInsertMixin from 'vault/mixins/focus-on-insert'; -import keys from 'vault/lib/keycodes'; const LIST_ROOT_ROUTE = 'vault.cluster.secrets.backend.list-root'; const SHOW_ROUTE = 'vault.cluster.secrets.backend.show'; export default Component.extend(FocusOnInsertMixin, { router: service(), - wizard: service(), mode: null, - // TODO: Investigate if we need all of these - emptyData: '{\n}', onDataChange() {}, onRefresh() {}, model: null, @@ -34,15 +29,6 @@ export default Component.extend(FocusOnInsertMixin, { } }, - waitForKeyUp: task(function*() { - while (true) { - let event = yield waitForEvent(document.body, 'keyup'); - this.onEscape(event); - } - }) - .on('didInsertElement') - .cancelOn('willDestroyElement'), - transitionToRoute() { this.get('router').transitionTo(...arguments); }, @@ -54,45 +40,33 @@ export default Component.extend(FocusOnInsertMixin, { } return modelPrefix; }, - - onEscape(e) { - if (e.keyCode !== keys.ESC || this.get('mode') !== 'show') { - return; - } - this.transitionToRoute(LIST_ROOT_ROUTE); - }, - - hasDataChanges() { - get(this, 'onDataChange')(get(this, 'model.hasDirtyAttributes')); - }, - persist(method, successCallback) { const model = get(this, 'model'); return model[method]().then(() => { - if (!get(model, 'isError')) { - if (this.get('wizard.featureState') === 'role') { - this.get('wizard').transitionFeatureMachine('role', 'CONTINUE', this.get('backendType')); - } - successCallback(model); - } + successCallback(model); + }); + }, + + applyChanges(type, callback = () => {}) { + 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)) { + return; + } + + this.persist('save', () => { + callback(); + this.transitionToRoute(SHOW_ROUTE, `${modelPrefix}${modelId}`); }); }, actions: { 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)) { - return; - } - this.persist('save', () => { - this.hasDataChanges(); - this.transitionToRoute(SHOW_ROUTE, `${modelPrefix}${modelId}`); - }); + this.applyChanges(type); }, setValue(key, event) { diff --git a/ui/app/components/transform-role-edit.js b/ui/app/components/transform-role-edit.js index 548e2dd85c..81a45d8cac 100644 --- a/ui/app/components/transform-role-edit.js +++ b/ui/app/components/transform-role-edit.js @@ -1,3 +1,130 @@ import TransformBase from './transform-edit-base'; +import { inject as service } from '@ember/service'; -export default TransformBase.extend({}); +const addToList = (list, itemToAdd) => { + if (!list || !Array.isArray(list)) return list; + list.push(itemToAdd); + return list.uniq(); +}; + +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 TransformBase.extend({ + store: service(), + flashMessages: service(), + + initialTransformations: null, + + init() { + this._super(...arguments); + this.set('initialTransformations', this.get('model.transformations')); + }, + + handleUpdateTransformations(updateTransformations, roleId, type = 'update') { + if (!updateTransformations) return; + const backend = this.get('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() + .then(() => { + return 'Successfully saved'; + }) + .catch(e => { + return { errorStatus: e.httpStatus, ...transform }; + }); + }); + }); + + Promise.all(promises).then(res => { + let hasError = res.find(r => !!r.errorStatus); + + if (hasError) { + let errorAdding = res.find(r => r.errorStatus === 403 && r.action === 'ADD'); + let 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.get('flashMessages').stickyInfo(message); + } + }); + }, + + actions: { + createOrUpdate(type, event) { + event.preventDefault(); + + this.applyChanges('save', () => { + const roleId = this.get('model.id'); + const newModelTransformations = this.get('model.transformations'); + + if (!this.initialTransformations) { + this.handleUpdatedTransformations( + 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); + }); + }, + }, +}); diff --git a/ui/app/models/transform.js b/ui/app/models/transform.js index 5fd21c933c..7212176b64 100644 --- a/ui/app/models/transform.js +++ b/ui/app/models/transform.js @@ -90,9 +90,12 @@ const Model = DS.Model.extend({ transformFieldAttrs: computed('transformAttrs', function() { return expandAttributeMeta(this, this.get('transformAttrs')); }), + + backend: attr('string', { + readOnly: true, + }), }); export default attachCapabilities(Model, { - // TODO: Update to dynamic backend name - updatePath: apiPath`transform/transformation/${'id'}`, + updatePath: apiPath`${'backend'}/transformation/${'id'}`, }); diff --git a/ui/app/models/transform/role.js b/ui/app/models/transform/role.js index fb59c245e0..57e5f4ab00 100644 --- a/ui/app/models/transform/role.js +++ b/ui/app/models/transform/role.js @@ -36,6 +36,8 @@ const Model = DS.Model.extend({ let keys = ['name', 'transformations']; return expandAttributeMeta(this, keys); }), + + backend: attr('string', { readOnly: true }), }); export default attachCapabilities(Model, { diff --git a/ui/app/routes/vault/cluster/secrets/backend/list.js b/ui/app/routes/vault/cluster/secrets/backend/list.js index 9c0c5e57e6..669e121640 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/list.js +++ b/ui/app/routes/vault/cluster/secrets/backend/list.js @@ -54,7 +54,7 @@ export default Route.extend({ beforeModel() { let secret = this.secretParam(); let backend = this.enginePathParam(); - let { tab } = this.paramsFor('vault.cluster.secrets.backend'); + let { tab } = this.paramsFor('vault.cluster.secrets.backend.list-root'); let secretEngine = this.store.peekRecord('secret-engine', backend); let type = secretEngine && secretEngine.get('engineType'); if (!type || !SUPPORTED_BACKENDS.includes(type)) { 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 53ad0147fa..0849a65453 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js +++ b/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js @@ -17,7 +17,7 @@ export default Route.extend(UnloadModelRoute, { let { backend } = this.paramsFor('vault.cluster.secrets.backend'); return backend; }, - capabilities(secret) { + capabilities(secret, modelType) { const backend = this.enginePathParam(); let backendModel = this.modelFor('vault.cluster.secrets.backend'); let backendType = backendModel.engineType; @@ -28,12 +28,38 @@ export default Route.extend(UnloadModelRoute, { path = backend + '/keys/' + secret; } else if (backendType === 'ssh' || backendType === 'aws') { 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) { + let 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'); }, @@ -71,7 +97,7 @@ export default Route.extend(UnloadModelRoute, { let types = { transit: 'transit-key', ssh: 'role-ssh', - transform: secret && secret.startsWith('role/') ? 'transform/role' : 'transform', // CBS TODO: switch out better + transform: this.modelTypeForTransform(secret), aws: 'role-aws', pki: secret && secret.startsWith('cert/') ? 'pki-certificate' : 'role-pki', cubbyhole: 'secret', @@ -199,12 +225,11 @@ export default Route.extend(UnloadModelRoute, { secret = secret.replace('cert/', ''); } if (modelType.startsWith('transform/')) { - // CBS TODO: we'll have more things to replace than just role/ - secret = secret.replace('role/', ''); + secret = this.transformSecretName(secret, modelType); } let secretModel; - let capabilities = this.capabilities(secret); + let capabilities = this.capabilities(secret, modelType); try { secretModel = await this.store.queryRecord(modelType, { id: secret, backend }); } catch (err) { diff --git a/ui/app/serializers/transform.js b/ui/app/serializers/transform.js index ab30958d06..95017c57f7 100644 --- a/ui/app/serializers/transform.js +++ b/ui/app/serializers/transform.js @@ -16,4 +16,18 @@ export default ApplicationSerializer.extend({ } 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/serializers/transform/role.js b/ui/app/serializers/transform/role.js new file mode 100644 index 0000000000..8a3d8f9eb3 --- /dev/null +++ b/ui/app/serializers/transform/role.js @@ -0,0 +1,17 @@ +import ApplicationSerializer from '../application'; +export default ApplicationSerializer.extend({ + extractLazyPaginatedData(payload) { + // TODO: do this for transform too? + 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-role-edit.hbs b/ui/app/templates/components/transform-role-edit.hbs index a5ae6aec4a..f7195f8185 100644 --- a/ui/app/templates/components/transform-role-edit.hbs +++ b/ui/app/templates/components/transform-role-edit.hbs @@ -24,17 +24,17 @@ {{#if (eq mode "show")}} - {{#if (or capabilities.canUpdate capabilities.canDelete)}} -
- {{/if}} {{#if capabilities.canDelete}} - Delete role - + {{/if}} {{#if capabilities.canUpdate }}