diff --git a/ui/app/components/confirm-action.js b/ui/app/components/confirm-action.js index ca6fdf519057..d3532584d121 100644 --- a/ui/app/components/confirm-action.js +++ b/ui/app/components/confirm-action.js @@ -16,6 +16,7 @@ export default Ember.Component.extend({ class={{buttonClasses}} type="button" disabled={{disabled}} + data-test-confirm-action-trigger=true {{action 'toggleConfirm'}} > {{yield}} diff --git a/ui/app/components/identity/_popup-base.js b/ui/app/components/identity/_popup-base.js new file mode 100644 index 000000000000..0cda95bc235a --- /dev/null +++ b/ui/app/components/identity/_popup-base.js @@ -0,0 +1,40 @@ +import Ember from 'ember'; +const { assert, inject, Component } = Ember; + +export default Component.extend({ + tagName: '', + flashMessages: inject.service(), + params: null, + successMessage() { + return 'Save was successful'; + }, + errorMessage() { + return 'There was an error saving'; + }, + onError(model) { + if (model && model.rollbackAttributes) { + model.rollbackAttributes(); + } + }, + onSuccess(){}, + // override and return a promise + transaction() { + assert('override transaction call in an extension of popup-base', false); + }, + + actions: { + performTransaction() { + let args = [...arguments]; + let messageArgs = this.messageArgs(...args); + return this.transaction(...args) + .then(() => { + this.get('onSuccess')(); + this.get('flashMessages').success(this.successMessage(...messageArgs)); + }) + .catch(e => { + this.onError(...messageArgs); + this.get('flashMessages').success(this.errorMessage(e, ...messageArgs)); + }); + }, + }, +}); diff --git a/ui/app/components/identity/edit-form.js b/ui/app/components/identity/edit-form.js index 64d222848fc6..d9f1eb8c9dd6 100644 --- a/ui/app/components/identity/edit-form.js +++ b/ui/app/components/identity/edit-form.js @@ -2,20 +2,23 @@ import Ember from 'ember'; import { task } from 'ember-concurrency'; import { humanize } from 'vault/helpers/humanize'; -const { computed } = Ember; +const { computed, inject } = Ember; export default Ember.Component.extend({ + flashMessages: inject.service(), + 'data-test-component': 'identity-edit-form', model: null, + + // 'create', 'edit', 'merge' mode: 'create', /* * @param Function * @public * - * Optional param to call a function upon successfully mounting a backend - * + * Optional param to call a function upon successfully saving an entity */ onSave: () => {}, - cancelLink: computed('mode', 'model', function() { + cancelLink: computed('mode', 'model.identityType', function() { let { model, mode } = this.getProperties('model', 'mode'); let key = `${mode}-${model.get('identityType')}`; let routes = { @@ -33,16 +36,17 @@ export default Ember.Component.extend({ return routes[key]; }), - getMessage(model) { + getMessage(model, isDelete = false) { let mode = this.get('mode'); let typeDisplay = humanize([model.get('identityType')]); + let action = isDelete ? 'deleted' : 'saved'; if (mode === 'merge') { return 'Successfully merged entities'; } if (model.get('id')) { - return `Successfully saved ${typeDisplay} ${model.id}.`; + return `Successfully ${action} ${typeDisplay} ${model.id}.`; } - return `Successfully saved ${typeDisplay}.`; + return `Successfully ${action} ${typeDisplay}.`; }, save: task(function*() { @@ -56,13 +60,26 @@ export default Ember.Component.extend({ return; } this.get('flashMessages').success(message); - yield this.get('onSave')(model); + yield this.get('onSave')({saveType: 'save', model}); }).drop(), willDestroy() { let model = this.get('model'); - if (!model.isDestroyed || !model.isDestroying) { + if ((model.get('isDirty') && !model.isDestroyed) || !model.isDestroying) { model.rollbackAttributes(); } }, + + actions: { + deleteItem(model) { + let message = this.getMessage(model, true); + let flash = this.get('flashMessages'); + model + .destroyRecord() + .then(() => { + flash.success(message); + return this.get('onSave')({saveType: 'delete', model}); + }); + }, + }, }); diff --git a/ui/app/components/identity/item-details.js b/ui/app/components/identity/item-details.js new file mode 100644 index 000000000000..eae4d92b2a2b --- /dev/null +++ b/ui/app/components/identity/item-details.js @@ -0,0 +1,23 @@ +import Ember from 'ember'; + +const { inject } = Ember; + +export default Ember.Component.extend({ + flashMessages: inject.service(), + + actions: { + enable(model) { + model.set('disabled', false); + + model.save(). + then(() => { + this.get('flashMessages').success(`Successfully enabled entity: ${model.id}`); + }) + .catch(e => { + this.get('flashMessages').success( + `There was a problem enabling the entity: ${model.id} - ${e.error.join(' ') || e.message}` + ); + }); + } + } +}); diff --git a/ui/app/components/identity/popup-alias.js b/ui/app/components/identity/popup-alias.js new file mode 100644 index 000000000000..20852421c4b7 --- /dev/null +++ b/ui/app/components/identity/popup-alias.js @@ -0,0 +1,22 @@ +import Base from './_popup-base'; + +export default Base.extend({ + messageArgs(model) { + let type = model.get('identityType'); + let id = model.id; + return [type, id]; + }, + + successMessage(type, id) { + return `Successfully deleted ${type}: ${id}`; + }, + + errorMessage(e, type, id) { + let error = e.errors ? e.errors.join(' ') : e.message; + return `There was a problem deleting ${type}: ${id} - ${error}`; + }, + + transaction(model) { + return model.destroyRecord(); + }, +}); diff --git a/ui/app/components/identity/popup-members.js b/ui/app/components/identity/popup-members.js new file mode 100644 index 000000000000..6c096916f742 --- /dev/null +++ b/ui/app/components/identity/popup-members.js @@ -0,0 +1,34 @@ +import Base from './_popup-base'; +import Ember from 'ember'; +const { computed } = Ember; + +export default Base.extend({ + model: computed.alias('params.firstObject'), + + groupArray: computed('params', function() { + return this.get('params').objectAt(1); + }), + + memberId: computed('params', function() { + return this.get('params').objectAt(2); + }), + + messageArgs(/*model, groupArray, memberId*/) { + return [...arguments]; + }, + + successMessage(model, groupArray, memberId) { + return `Successfully removed '${memberId}' from the group`; + }, + + errorMessage(e, model, groupArray, memberId) { + let error = e.errors ? e.errors.join(' ') : e.message; + return `There was a problem removing '${memberId}' from the group - ${error}`; + }, + + transaction(model, groupArray, memberId) { + let members = model.get(groupArray); + model.set(groupArray, members.without(memberId)); + return model.save(); + }, +}); diff --git a/ui/app/components/identity/popup-metadata.js b/ui/app/components/identity/popup-metadata.js new file mode 100644 index 000000000000..c6d99fc43f75 --- /dev/null +++ b/ui/app/components/identity/popup-metadata.js @@ -0,0 +1,29 @@ +import Base from './_popup-base'; +import Ember from 'ember'; +const { computed } = Ember; + +export default Base.extend({ + model: computed.alias('params.firstObject'), + key: computed('params', function() { + return this.get('params').objectAt(1); + }), + + messageArgs(model, key) { + return [model, key]; + }, + + successMessage(model, key) { + return `Successfully removed '${key}' from metadata`; + }, + errorMessage(e, model, key) { + let error = e.errors ? e.errors.join(' ') : e.message; + return `There was a problem removing '${key}' from the metadata - ${error}`; + }, + + transaction(model, key) { + let metadata = model.get('metadata'); + delete metadata[key]; + model.set('metadata', { ...metadata }); + return model.save(); + }, +}); diff --git a/ui/app/components/identity/popup-policy.js b/ui/app/components/identity/popup-policy.js new file mode 100644 index 000000000000..b626b23c2b1f --- /dev/null +++ b/ui/app/components/identity/popup-policy.js @@ -0,0 +1,29 @@ +import Base from './_popup-base'; +import Ember from 'ember'; +const { computed } = Ember; + +export default Base.extend({ + model: computed.alias('params.firstObject'), + policyName: computed('params', function() { + return this.get('params').objectAt(1); + }), + + messageArgs(model, policyName) { + return [model, policyName]; + }, + + successMessage(model, policyName) { + return `Successfully removed '${policyName}' policy from ${model.id} `; + }, + + errorMessage(e, model, policyName) { + let error = e.errors ? e.errors.join(' ') : e.message; + return `There was a problem removing '${policyName}' policy - ${error}`; + }, + + transaction(model, policyName) { + let policies = model.get('policies'); + model.set('policies', policies.without(policyName)); + return model.save(); + }, +}); diff --git a/ui/app/components/info-table-row.js b/ui/app/components/info-table-row.js index 914573614f28..0c12595f1d04 100644 --- a/ui/app/components/info-table-row.js +++ b/ui/app/components/info-table-row.js @@ -1,6 +1,7 @@ import Ember from 'ember'; export default Ember.Component.extend({ + 'data-test-component': 'info-table-row', classNames: ['info-table-row'], isVisible: Ember.computed.or('alwaysRender', 'value'), diff --git a/ui/app/components/message-in-page.js b/ui/app/components/message-in-page.js index f25076d90ae5..736739ab35a8 100644 --- a/ui/app/components/message-in-page.js +++ b/ui/app/components/message-in-page.js @@ -6,6 +6,8 @@ const { computed } = Ember; export default Ember.Component.extend({ type: null, + yieldWithoutColumn: false, + classNameBindings: ['containerClass'], containerClass: computed('type', function() { diff --git a/ui/app/components/secret-list-header.js b/ui/app/components/secret-list-header.js index 63aeac44396e..3835ff8ad445 100644 --- a/ui/app/components/secret-list-header.js +++ b/ui/app/components/secret-list-header.js @@ -9,6 +9,4 @@ export default Ember.Component.extend({ baseKey: null, backendCrumb: null, model: null, - - }); diff --git a/ui/app/components/tool-actions-form.js b/ui/app/components/tool-actions-form.js index 0f09e9dda430..61c9bea3357e 100644 --- a/ui/app/components/tool-actions-form.js +++ b/ui/app/components/tool-actions-form.js @@ -71,7 +71,7 @@ export default Ember.Component.extend(DEFAULTS, { handleSuccess(resp, action) { let props = {}; - let secret = resp && resp.data || resp.auth; + let secret = (resp && resp.data) || resp.auth; if (secret && action === 'unwrap') { props = Ember.assign({}, props, { unwrap_data: secret }); } diff --git a/ui/app/controllers/vault/cluster/access/identity/aliases/index.js b/ui/app/controllers/vault/cluster/access/identity/aliases/index.js index fd2e726634fd..a35db205a2ba 100644 --- a/ui/app/controllers/vault/cluster/access/identity/aliases/index.js +++ b/ui/app/controllers/vault/cluster/access/identity/aliases/index.js @@ -1,4 +1,10 @@ import Ember from 'ember'; import ListController from 'vault/mixins/list-controller'; -export default Ember.Controller.extend(ListController); +export default Ember.Controller.extend(ListController, { + actions: { + onDelete() { + this.send('reload'); + } + } +}); diff --git a/ui/app/controllers/vault/cluster/access/identity/create.js b/ui/app/controllers/vault/cluster/access/identity/create.js index 46d9d6437725..46c8a30c6e1a 100644 --- a/ui/app/controllers/vault/cluster/access/identity/create.js +++ b/ui/app/controllers/vault/cluster/access/identity/create.js @@ -4,7 +4,26 @@ import { task } from 'ember-concurrency'; export default Ember.Controller.extend({ showRoute: 'vault.cluster.access.identity.show', showTab: 'details', - navToShow: task(function*(model) { - yield this.transitionToRoute(this.get('showRoute'), model.id, this.get('showTab')); + navAfterSave: task(function*({saveType, model}) { + let isDelete = saveType === 'delete'; + let type = model.get('identityType'); + let listRoutes= { + 'entity-alias': 'vault.cluster.access.identity.aliases.index', + 'group-alias': 'vault.cluster.access.identity.aliases.index', + 'group': 'vault.cluster.access.identity.index', + 'entity': 'vault.cluster.access.identity.index', + }; + let routeName = listRoutes[type] + if (!isDelete) { + yield this.transitionToRoute( + this.get('showRoute'), + model.id, + this.get('showTab') + ); + return; + } + yield this.transitionToRoute( + routeName + ); }), }); diff --git a/ui/app/controllers/vault/cluster/access/identity/index.js b/ui/app/controllers/vault/cluster/access/identity/index.js index fd2e726634fd..6cc77a8dc726 100644 --- a/ui/app/controllers/vault/cluster/access/identity/index.js +++ b/ui/app/controllers/vault/cluster/access/identity/index.js @@ -1,4 +1,46 @@ import Ember from 'ember'; import ListController from 'vault/mixins/list-controller'; -export default Ember.Controller.extend(ListController); +const { inject } = Ember; + +export default Ember.Controller.extend(ListController, { + flashMessages: inject.service(), + + actions: { + delete(model) { + let type = model.get('identityType'); + let id = model.id; + return model + .destroyRecord() + .then(() => { + this.send('reload'); + this.get('flashMessages').success(`Successfully deleted ${type}: ${id}`); + }) + .catch(e => { + this.get('flashMessages').success( + `There was a problem deleting ${type}: ${id} - ${e.error.join(' ') || e.message}` + ); + }); + }, + + toggleDisabled(model) { + let action = model.get('disabled') ? ['enabled', 'enabling'] : ['disabled', 'disabling']; + let type = model.get('identityType'); + let id = model.id; + model.toggleProperty('disabled'); + + model.save(). + then(() => { + this.get('flashMessages').success(`Successfully ${action[0]} ${type}: ${id}`); + }) + .catch(e => { + this.get('flashMessages').success( + `There was a problem ${action[1]} ${type}: ${id} - ${e.error.join(' ') || e.message}` + ); + }); + }, + reloadRecord(model) { + model.reload(); + }, + }, +}); diff --git a/ui/app/controllers/vault/cluster/access/leases/list.js b/ui/app/controllers/vault/cluster/access/leases/list.js index 955192bbc2f5..feaa06b7f296 100644 --- a/ui/app/controllers/vault/cluster/access/leases/list.js +++ b/ui/app/controllers/vault/cluster/access/leases/list.js @@ -1,9 +1,11 @@ import Ember from 'ember'; import utils from 'vault/lib/key-utils'; -export default Ember.Controller.extend({ - flashMessages: Ember.inject.service(), - clusterController: Ember.inject.controller('vault.cluster'), +const { inject, computed, Controller } = Ember; +export default Controller.extend({ + flashMessages: inject.service(), + store: inject.service(), + clusterController: inject.controller('vault.cluster'), queryParams: { page: 'page', pageFilter: 'pageFilter', @@ -13,7 +15,7 @@ export default Ember.Controller.extend({ pageFilter: null, filter: null, - backendCrumb: Ember.computed(function() { + backendCrumb: computed(function() { return { label: 'leases', text: 'leases', @@ -24,13 +26,13 @@ export default Ember.Controller.extend({ isLoading: false, - filterMatchesKey: Ember.computed('filter', 'model', 'model.[]', function() { + filterMatchesKey: computed('filter', 'model', 'model.[]', function() { var filter = this.get('filter'); var content = this.get('model'); return !!(content.length && content.findBy('id', filter)); }), - firstPartialMatch: Ember.computed('filter', 'model', 'model.[]', 'filterMatchesKey', function() { + firstPartialMatch: computed('filter', 'model', 'model.[]', 'filterMatchesKey', function() { var filter = this.get('filter'); var content = this.get('model'); var filterMatchesKey = this.get('filterMatchesKey'); @@ -42,7 +44,7 @@ export default Ember.Controller.extend({ }); }), - filterIsFolder: Ember.computed('filter', function() { + filterIsFolder: computed('filter', function() { return !!utils.keyIsFolder(this.get('filter')); }), @@ -56,7 +58,7 @@ export default Ember.Controller.extend({ }, revokePrefix(prefix, isForce) { - const adapter = this.model.store.adapterFor('lease'); + const adapter = this.get('store').adapterFor('lease'); const method = isForce ? 'forceRevokePrefix' : 'revokePrefix'; const fn = adapter[method]; fn diff --git a/ui/app/controllers/vault/cluster/secrets/backend/list.js b/ui/app/controllers/vault/cluster/secrets/backend/list.js index a113f5cb1a4a..3f540baf8af4 100644 --- a/ui/app/controllers/vault/cluster/secrets/backend/list.js +++ b/ui/app/controllers/vault/cluster/secrets/backend/list.js @@ -66,6 +66,7 @@ export default Ember.Controller.extend(BackendCrumbMixin, { delete(item) { const name = item.id; item.destroyRecord().then(() => { + this.send('reload'); this.get('flashMessages').success(`${name} was successfully deleted.`); }); }, diff --git a/ui/app/macros/identity-capabilities.js b/ui/app/macros/identity-capabilities.js new file mode 100644 index 000000000000..f36eb29052e4 --- /dev/null +++ b/ui/app/macros/identity-capabilities.js @@ -0,0 +1,5 @@ +import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; + +export default function() { + return lazyCapabilities(apiPath`identity/${'identityType'}/id/${'id'}`, 'id', 'identityType'); +} diff --git a/ui/app/macros/lazy-capabilities.js b/ui/app/macros/lazy-capabilities.js new file mode 100644 index 000000000000..108d66a45a02 --- /dev/null +++ b/ui/app/macros/lazy-capabilities.js @@ -0,0 +1,25 @@ +import { queryRecord } from 'ember-computed-query'; + +export function apiPath(strings, ...keys) { + return function(data) { + let dict = data || {}; + let result = [strings[0]]; + keys.forEach((key, i) => { + result.push(dict[key], strings[i + 1]); + }); + return result.join(''); + }; +} + +export default function() { + let [templateFn, ...keys] = arguments; + return queryRecord( + 'capabilities', + context => { + return { + id: templateFn(context.getProperties(...keys)), + }; + }, + ...keys + ); +} diff --git a/ui/app/models/identity/entity-alias.js b/ui/app/models/identity/entity-alias.js index 6e5dbaeaf8eb..b38b823f23c6 100644 --- a/ui/app/models/identity/entity-alias.js +++ b/ui/app/models/identity/entity-alias.js @@ -1,8 +1,12 @@ import IdentityModel from './_base'; import DS from 'ember-data'; +import Ember from 'ember'; +import identityCapabilities from 'vault/macros/identity-capabilities'; const { attr, belongsTo } = DS; +const { computed } = Ember; export default IdentityModel.extend({ + parentType: 'entity', formFields: ['name', 'mountAccessor', 'metadata'], entity: belongsTo('identity/entity', { readOnly: true, async: false }), @@ -12,7 +16,7 @@ export default IdentityModel.extend({ label: 'Auth Backend', editType: 'mountAccessor', }), - metadata: attr('object', { + metadata: attr({ editType: 'kv', }), mountPath: attr('string', { @@ -28,4 +32,8 @@ export default IdentityModel.extend({ readOnly: true, }), mergedFromCanonicalIds: attr(), + + updatePath: identityCapabilities(), + canDelete: computed.alias('updatePath.canDelete'), + canEdit: computed.alias('updatePath.canUpdate'), }); diff --git a/ui/app/models/identity/entity.js b/ui/app/models/identity/entity.js index 77b6853189a6..2f63eaca0c92 100644 --- a/ui/app/models/identity/entity.js +++ b/ui/app/models/identity/entity.js @@ -1,12 +1,23 @@ +import Ember from 'ember'; import IdentityModel from './_base'; import DS from 'ember-data'; +import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; +import identityCapabilities from 'vault/macros/identity-capabilities'; + +const { computed } = Ember; + const { attr, hasMany } = DS; export default IdentityModel.extend({ - formFields: ['name', 'policies', 'metadata'], + formFields: ['name', 'disabled', 'policies', 'metadata'], name: attr('string'), + disabled: attr('boolean', { + defaultValue: false, + label: 'Disable entity', + helpText: 'All associated tokens cannot be used, but are not revoked.', + }), mergedEntityIds: attr(), - metadata: attr('object', { + metadata: attr({ editType: 'kv', }), policies: attr({ @@ -28,4 +39,11 @@ export default IdentityModel.extend({ inheritedGroupIds: attr({ readOnly: true, }), + + updatePath: identityCapabilities(), + canDelete: computed.alias('updatePath.canDelete'), + canEdit: computed.alias('updatePath.canUpdate'), + + aliasPath: lazyCapabilities(apiPath`identity/entity-alias`), + canAddAlias: computed.alias('aliasPath.canCreate'), }); diff --git a/ui/app/models/identity/group-alias.js b/ui/app/models/identity/group-alias.js index 14af8c1105a7..3f40ca7126c9 100644 --- a/ui/app/models/identity/group-alias.js +++ b/ui/app/models/identity/group-alias.js @@ -1,8 +1,13 @@ import IdentityModel from './_base'; import DS from 'ember-data'; +import Ember from 'ember'; +import identityCapabilities from 'vault/macros/identity-capabilities'; + const { attr, belongsTo } = DS; +const { computed } = Ember; export default IdentityModel.extend({ + parentType: 'group', formFields: ['name', 'mountAccessor'], group: belongsTo('identity/group', { readOnly: true, async: false }), @@ -26,4 +31,9 @@ export default IdentityModel.extend({ lastUpdateTime: attr('string', { readOnly: true, }), + + updatePath: identityCapabilities(), + canDelete: computed.alias('updatePath.canDelete'), + canEdit: computed.alias('updatePath.canUpdate'), + }); diff --git a/ui/app/models/identity/group.js b/ui/app/models/identity/group.js index 37212c0c90ba..47483dd529a2 100644 --- a/ui/app/models/identity/group.js +++ b/ui/app/models/identity/group.js @@ -1,6 +1,8 @@ import Ember from 'ember'; import IdentityModel from './_base'; import DS from 'ember-data'; +import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; +import identityCapabilities from 'vault/macros/identity-capabilities'; const { computed } = Ember; const { attr, belongsTo } = DS; @@ -52,4 +54,18 @@ export default IdentityModel.extend({ ), alias: belongsTo('identity/group-alias', { async: false, readOnly: true }), + updatePath: identityCapabilities(), + canDelete: computed.alias('updatePath.canDelete'), + canEdit: computed.alias('updatePath.canUpdate'), + + aliasPath: lazyCapabilities(apiPath`identity/group-alias`), + canAddAlias: computed('aliasPath.canCreate', 'type', 'alias', function() { + let type = this.get('type'); + let alias = this.get('alias'); + // internal groups can't have aliases, and external groups can only have one + if (type === 'internal' || alias) { + return false; + } + return this.get('aliasPath.canCreate'); + }), }); diff --git a/ui/app/routes/vault/cluster/access/identity/aliases/add.js b/ui/app/routes/vault/cluster/access/identity/aliases/add.js index 5d9af4ba3ea2..bfd9ce6dcff7 100644 --- a/ui/app/routes/vault/cluster/access/identity/aliases/add.js +++ b/ui/app/routes/vault/cluster/access/identity/aliases/add.js @@ -1,6 +1,8 @@ import Ember from 'ember'; +import UnloadModelRoute from 'vault/mixins/unload-model-route'; +import UnsavedModelRoute from 'vault/mixins/unsaved-model-route'; -export default Ember.Route.extend({ +export default Ember.Route.extend(UnloadModelRoute, UnsavedModelRoute, { model(params) { let itemType = this.modelFor('vault.cluster.access.identity'); let modelType = `identity/${itemType}-alias`; diff --git a/ui/app/routes/vault/cluster/access/identity/aliases/edit.js b/ui/app/routes/vault/cluster/access/identity/aliases/edit.js index 15e9b36dc3b2..9c2c87eb2a8d 100644 --- a/ui/app/routes/vault/cluster/access/identity/aliases/edit.js +++ b/ui/app/routes/vault/cluster/access/identity/aliases/edit.js @@ -1,6 +1,8 @@ import Ember from 'ember'; +import UnloadModelRoute from 'vault/mixins/unload-model-route'; +import UnsavedModelRoute from 'vault/mixins/unsaved-model-route'; -export default Ember.Route.extend({ +export default Ember.Route.extend(UnloadModelRoute, UnsavedModelRoute, { model(params) { let itemType = this.modelFor('vault.cluster.access.identity'); let modelType = `identity/${itemType}-alias`; diff --git a/ui/app/routes/vault/cluster/access/identity/aliases/index.js b/ui/app/routes/vault/cluster/access/identity/aliases/index.js index 027cc1f7d5ee..df1100eae9e2 100644 --- a/ui/app/routes/vault/cluster/access/identity/aliases/index.js +++ b/ui/app/routes/vault/cluster/access/identity/aliases/index.js @@ -27,10 +27,14 @@ export default Ember.Route.extend(ListRoute, { actions: { willTransition(transition) { window.scrollTo(0, 0); - if (transition.targetName !== this.routeName) { + if (!transition || transition.targetName !== this.routeName) { this.store.clearAllDatasets(); } return true; }, + reload() { + this.store.clearAllDatasets(); + this.refresh(); + } }, }); diff --git a/ui/app/routes/vault/cluster/access/identity/create.js b/ui/app/routes/vault/cluster/access/identity/create.js index a147b9deb810..c0231a009906 100644 --- a/ui/app/routes/vault/cluster/access/identity/create.js +++ b/ui/app/routes/vault/cluster/access/identity/create.js @@ -1,6 +1,8 @@ import Ember from 'ember'; +import UnloadModelRoute from 'vault/mixins/unload-model-route'; +import UnsavedModelRoute from 'vault/mixins/unsaved-model-route'; -export default Ember.Route.extend({ +export default Ember.Route.extend(UnloadModelRoute, UnsavedModelRoute, { model() { let itemType = this.modelFor('vault.cluster.access.identity'); let modelType = `identity/${itemType}`; diff --git a/ui/app/routes/vault/cluster/access/identity/edit.js b/ui/app/routes/vault/cluster/access/identity/edit.js index df1e5327e437..495e5e9aaba8 100644 --- a/ui/app/routes/vault/cluster/access/identity/edit.js +++ b/ui/app/routes/vault/cluster/access/identity/edit.js @@ -1,6 +1,8 @@ import Ember from 'ember'; +import UnloadModelRoute from 'vault/mixins/unload-model-route'; +import UnsavedModelRoute from 'vault/mixins/unsaved-model-route'; -export default Ember.Route.extend({ +export default Ember.Route.extend(UnloadModelRoute, UnsavedModelRoute, { model(params) { let itemType = this.modelFor('vault.cluster.access.identity'); let modelType = `identity/${itemType}`; diff --git a/ui/app/routes/vault/cluster/access/identity/index.js b/ui/app/routes/vault/cluster/access/identity/index.js index 200061ed72e2..cf43c6d6df67 100644 --- a/ui/app/routes/vault/cluster/access/identity/index.js +++ b/ui/app/routes/vault/cluster/access/identity/index.js @@ -34,5 +34,9 @@ export default Ember.Route.extend(ListRoute, { } return true; }, + reload() { + this.store.clearAllDatasets(); + this.refresh(); + } }, }); diff --git a/ui/app/routes/vault/cluster/access/identity/show.js b/ui/app/routes/vault/cluster/access/identity/show.js index bb4d43277ec0..11361085e40f 100644 --- a/ui/app/routes/vault/cluster/access/identity/show.js +++ b/ui/app/routes/vault/cluster/access/identity/show.js @@ -13,13 +13,36 @@ export default Ember.Route.extend({ Ember.set(error, 'httpStatus', 404); throw error; } - // TODO peekRecord here to see if we have the record already + + // if the record is in the store use that + let model = this.store.peekRecord(modelType, params.item_id); + + // if we don't have creationTime, we only have a partial model so reload + if (model && !model.get('creationTime')) { + model = model.reload(); + } + + // if there's no model, we need to fetch it + if (!model) { + model = this.store.findRecord(modelType, params.item_id); + } + return Ember.RSVP.hash({ - model: this.store.findRecord(modelType, params.item_id), + model, section, }); }, + activate() { + // if we're just entering the route, and it's not a hard reload + // reload to make sure we have the newest info + if (this.currentModel) { + Ember.run.next(() => { + this.controller.get('model').reload(); + }); + } + }, + afterModel(resolvedModel) { let { section, model } = resolvedModel; if (model.get('identityType') === 'group' && model.get('type') === 'internal' && section === 'aliases') { diff --git a/ui/app/routes/vault/cluster/secrets/backend/list.js b/ui/app/routes/vault/cluster/secrets/backend/list.js index d3ebd7851ba2..dfbde5b705ff 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/list.js +++ b/ui/app/routes/vault/cluster/secrets/backend/list.js @@ -159,5 +159,9 @@ export default Ember.Route.extend({ } return true; }, + reload() { + this.refresh(); + this.store.clearAllDatasets(); + } }, }); diff --git a/ui/app/services/store.js b/ui/app/services/store.js index bc092f923760..711cbdb8503a 100644 --- a/ui/app/services/store.js +++ b/ui/app/services/store.js @@ -131,13 +131,25 @@ export default DS.Store.extend({ // pushes records into the store and returns the result fetchPage(modelName, query) { const response = this.constructResponse(modelName, query); - this.unloadAll(modelName); - this.push( - this.serializerFor(modelName).normalizeResponse(this, this.modelFor(modelName), response, null, 'query') - ); - const model = this.peekAll(modelName); - model.set('meta', response.meta); - return model; + this.peekAll(modelName).forEach(record => { + record.unloadRecord(); + }); + return new Ember.RSVP.Promise(resolve => { + Ember.run.schedule('destroy', () => { + this.push( + this.serializerFor(modelName).normalizeResponse( + this, + this.modelFor(modelName), + response, + null, + 'query' + ) + ); + let model = this.peekAll(modelName).toArray(); + model.set('meta', response.meta); + resolve(model); + }); + }); }, // get cached data diff --git a/ui/app/styles/components/popup-menu.scss b/ui/app/styles/components/popup-menu.scss index 5155b1237096..83b92a43a283 100644 --- a/ui/app/styles/components/popup-menu.scss +++ b/ui/app/styles/components/popup-menu.scss @@ -49,6 +49,7 @@ height: auto; width: 100%; text-align: left; + text-decoration: none; &:hover { background-color: $menu-item-hover-background-color; diff --git a/ui/app/styles/core/buttons.scss b/ui/app/styles/core/buttons.scss index 5f961c3b674c..5efa15a61ea5 100644 --- a/ui/app/styles/core/buttons.scss +++ b/ui/app/styles/core/buttons.scss @@ -133,6 +133,17 @@ $button-box-shadow-standard: 0 3px 1px 0 rgba($black, 0.12); } } + &.is-orange { + background-color: $orange; + border-color: $orange; + color: $white; + + &:hover, + &.is-hovered { + background-color: darken($orange, 5%); + border-color: darken($orange, 5%); + } + } &.is-compact { height: 2rem; padding: $size-11 $size-8; diff --git a/ui/app/templates/components/identity/edit-form.hbs b/ui/app/templates/components/identity/edit-form.hbs index 8ed98941f727..fb8cedb161e2 100644 --- a/ui/app/templates/components/identity/edit-form.hbs +++ b/ui/app/templates/components/identity/edit-form.hbs @@ -10,24 +10,39 @@ {{form-field data-test-field attr=attr model=model}} {{/each}} -
-
- + {{#if (or (eq mode "merge") (eq mode "create" ))}} + + Cancel + {{else}} - Save + + Cancel + {{/if}} - - {{#if (or (eq mode "merge") (eq mode "create" ))}} - - Cancel - - {{else}} - - Cancel - - {{/if}} +
+ + {{#if (and (eq mode "edit") model.canDelete)}} + {{#confirm-action + buttonClasses="button is-ghost" + onConfirmAction=(action "deleteItem" model) + confirmMessage=(concat "Are you sure you want to delete " model.id "?") + data-test-entity-item-delete=true + }} + Delete + {{/confirm-action}} + {{/if}} + diff --git a/ui/app/templates/components/identity/entity-nav.hbs b/ui/app/templates/components/identity/entity-nav.hbs index a7e34fa120fd..1c38812a8640 100644 --- a/ui/app/templates/components/identity/entity-nav.hbs +++ b/ui/app/templates/components/identity/entity-nav.hbs @@ -7,12 +7,12 @@
{{#if (eq identityType "entity")}} - + Merge {{pluralize identityType}} {{i-con glyph="chevron-right" size=11}} {{/if}} - + Create {{identityType}} {{i-con glyph="chevron-right" size=11}} diff --git a/ui/app/templates/components/identity/item-alias/alias-details.hbs b/ui/app/templates/components/identity/item-alias/alias-details.hbs index d7ecb17c5687..6ccbcb8448be 100644 --- a/ui/app/templates/components/identity/item-alias/alias-details.hbs +++ b/ui/app/templates/components/identity/item-alias/alias-details.hbs @@ -1,4 +1,4 @@ -{{info-table-row label="Name" value=model.name }} +{{info-table-row label="Name" value=model.name data-test-alias-name=true}} {{info-table-row label="ID" value=model.id }} {{#info-table-row label=(if (eq model.identityType "entity-alias") "Entity ID" "Group ID") value=model.canonicalId}}
+ {{#if model.canEdit}} + {{identity/popup-metadata params=(array model key)}} + {{/if}}
diff --git a/ui/app/templates/components/identity/item-aliases.hbs b/ui/app/templates/components/identity/item-aliases.hbs index f972272ebd09..72aaaaab3f00 100644 --- a/ui/app/templates/components/identity/item-aliases.hbs +++ b/ui/app/templates/components/identity/item-aliases.hbs @@ -18,6 +18,7 @@ {{item.mountAccessor}}
+ {{identity/popup-alias params=(array item)}}
{{/linked-block}} diff --git a/ui/app/templates/components/identity/item-details.hbs b/ui/app/templates/components/identity/item-details.hbs index 75ba63cd16f8..55ecb3d59cec 100644 --- a/ui/app/templates/components/identity/item-details.hbs +++ b/ui/app/templates/components/identity/item-details.hbs @@ -1,4 +1,20 @@ -{{info-table-row label="Name" value=model.name }} +{{#if model.disabled}} +
+ {{#message-in-page type="warning" yieldWithoutColumn=true messageClass="message-body is-marginless" data-test-disabled-warning=true}} +
+ Attention This {{model.identityType}} is disabled. All associated tokens cannot be used, but are not revoked. +
+ {{#if model.canEdit}} +
+ +
+ {{/if}} + {{/message-in-page}} +
+{{/if}} +{{info-table-row label="Name" value=model.name data-test-identity-item-name=true}} {{info-table-row label="Type" value=model.type }} {{info-table-row label="ID" value=model.id }} {{#info-table-row label="Merged Ids" value=model.mergedEntityIds }} diff --git a/ui/app/templates/components/identity/item-members.hbs b/ui/app/templates/components/identity/item-members.hbs index f31f16f6241b..17aba2c3a0a7 100644 --- a/ui/app/templates/components/identity/item-members.hbs +++ b/ui/app/templates/components/identity/item-members.hbs @@ -1,21 +1,56 @@ {{#if model.hasMembers}} {{#each model.memberGroupIds as |gid|}} -
{{i-con - glyph='folder' - size=14 - class="has-text-grey-light" - }}{{gid}} + + {{#linked-block + "vault.cluster.access.identity.show" + "groups" + gid + details + class="box is-sideless is-marginless" + }} +
+
+ {{i-con + glyph='folder' + size=14 + class="has-text-grey-light" + }}{{gid}} +
+
+ {{#if model.canEdit}} + {{identity/popup-members params=(array model "memberGroupIds" gid)}} + {{/if}} +
+
+ {{/linked-block}} {{/each}} {{#each model.memberEntityIds as |gid|}} - {{i-con - glyph='role' - size=14 - class="has-text-grey-light" - }}{{gid}} + }} +
+
+ {{i-con + glyph='role' + size=14 + class="has-text-grey-light" + }}{{gid}} +
+
+ {{#if model.canEdit}} + {{identity/popup-members params=(array model "memberEntityIds" gid)}} + {{/if}} +
+
+ {{/linked-block}} {{/each}} {{else}}
diff --git a/ui/app/templates/components/identity/item-metadata.hbs b/ui/app/templates/components/identity/item-metadata.hbs index efa6658b85f4..f87dcb24bdd2 100644 --- a/ui/app/templates/components/identity/item-metadata.hbs +++ b/ui/app/templates/components/identity/item-metadata.hbs @@ -8,6 +8,9 @@ {{value}}
+ {{#if model.canEdit}} + {{identity/popup-metadata params=(array model key)}} + {{/if}}
diff --git a/ui/app/templates/components/identity/item-policies.hbs b/ui/app/templates/components/identity/item-policies.hbs index 2211c16f7b6d..649d1609b4e2 100644 --- a/ui/app/templates/components/identity/item-policies.hbs +++ b/ui/app/templates/components/identity/item-policies.hbs @@ -1,4 +1,4 @@ -{{#each model.policies as |item|}} +{{#each model.policies as |policyName|}} {{#linked-block "vault.cluster.policy.show" "acl" @@ -7,12 +7,15 @@ }}
- {{item}} + {{policyName}}
+ {{#if model.canEdit}} + {{identity/popup-policy params=(array model policyName)}} + {{/if}}
{{/linked-block}} diff --git a/ui/app/templates/components/identity/popup-alias.hbs b/ui/app/templates/components/identity/popup-alias.hbs new file mode 100644 index 000000000000..b708eed5fd03 --- /dev/null +++ b/ui/app/templates/components/identity/popup-alias.hbs @@ -0,0 +1,45 @@ +{{#popup-menu name="alias-menu"}} + {{#with params.firstObject as |item|}} + +{{/with}} +{{/popup-menu}} diff --git a/ui/app/templates/components/identity/popup-members.hbs b/ui/app/templates/components/identity/popup-members.hbs new file mode 100644 index 000000000000..2b8f4cfc6bb1 --- /dev/null +++ b/ui/app/templates/components/identity/popup-members.hbs @@ -0,0 +1,21 @@ +{{#popup-menu name="member-edit-menu"}} + +{{/popup-menu}} diff --git a/ui/app/templates/components/identity/popup-metadata.hbs b/ui/app/templates/components/identity/popup-metadata.hbs new file mode 100644 index 000000000000..32ec5500a61e --- /dev/null +++ b/ui/app/templates/components/identity/popup-metadata.hbs @@ -0,0 +1,21 @@ +{{#popup-menu name="metadata-edit-menu"}} + +{{/popup-menu}} diff --git a/ui/app/templates/components/identity/popup-policy.hbs b/ui/app/templates/components/identity/popup-policy.hbs new file mode 100644 index 000000000000..55a5fc483110 --- /dev/null +++ b/ui/app/templates/components/identity/popup-policy.hbs @@ -0,0 +1,31 @@ +{{#popup-menu name="policy-menu"}} + +{{/popup-menu}} diff --git a/ui/app/templates/components/message-in-page.hbs b/ui/app/templates/components/message-in-page.hbs index be3d60501c6c..2052b226c53e 100644 --- a/ui/app/templates/components/message-in-page.hbs +++ b/ui/app/templates/components/message-in-page.hbs @@ -8,11 +8,15 @@ excludeIconClass=true }} -
-

- {{alertType.text}} + {{#if yieldWithoutColumn}} {{yield}} -

-
+ {{else}} +
+

+ {{alertType.text}} + {{yield}} +

+
+ {{/if}} diff --git a/ui/app/templates/vault/cluster/access/identity/aliases/add.hbs b/ui/app/templates/vault/cluster/access/identity/aliases/add.hbs index 5b41e4cfa14a..dde38a4a5b37 100644 --- a/ui/app/templates/vault/cluster/access/identity/aliases/add.hbs +++ b/ui/app/templates/vault/cluster/access/identity/aliases/add.hbs @@ -8,4 +8,4 @@ -{{identity/edit-form model=model onSave=(perform navToShow)}} +{{identity/edit-form model=model onSave=(perform navAfterSave)}} diff --git a/ui/app/templates/vault/cluster/access/identity/aliases/edit.hbs b/ui/app/templates/vault/cluster/access/identity/aliases/edit.hbs index a13fc9e44bb0..002496fa43e6 100644 --- a/ui/app/templates/vault/cluster/access/identity/aliases/edit.hbs +++ b/ui/app/templates/vault/cluster/access/identity/aliases/edit.hbs @@ -8,4 +8,4 @@ -{{identity/edit-form mode="edit" model=model onSave=(perform navToShow)}} +{{identity/edit-form mode="edit" model=model onSave=(perform navAfterSave)}} diff --git a/ui/app/templates/vault/cluster/access/identity/aliases/index.hbs b/ui/app/templates/vault/cluster/access/identity/aliases/index.hbs index 7d715af328e0..f75ba0d43624 100644 --- a/ui/app/templates/vault/cluster/access/identity/aliases/index.hbs +++ b/ui/app/templates/vault/cluster/access/identity/aliases/index.hbs @@ -1,23 +1,33 @@ {{identity/entity-nav identityType=identityType}} {{#if model.meta.total}} {{#each model as |item|}} - - {{i-con - glyph="role" - size=14 - class="has-text-grey-light" - }} - - {{item.id}} - - +
+
+ {{i-con + glyph="role" + size=14 + class="has-text-grey-light" + }}{{item.id}} +
+
+ {{identity/popup-alias params=(array item) onSuccess=(action "onDelete")}} +
+
+ {{/linked-block}} {{/each}} {{else}}
diff --git a/ui/app/templates/vault/cluster/access/identity/aliases/show.hbs b/ui/app/templates/vault/cluster/access/identity/aliases/show.hbs index 24f7d9cdfe5b..927190090a83 100644 --- a/ui/app/templates/vault/cluster/access/identity/aliases/show.hbs +++ b/ui/app/templates/vault/cluster/access/identity/aliases/show.hbs @@ -16,7 +16,7 @@
- + Edit {{lowercase (humanize model.identityType)}} {{i-con glyph="chevron-right" size=11}} diff --git a/ui/app/templates/vault/cluster/access/identity/create.hbs b/ui/app/templates/vault/cluster/access/identity/create.hbs index 3fce9e8d3406..a97a60823798 100644 --- a/ui/app/templates/vault/cluster/access/identity/create.hbs +++ b/ui/app/templates/vault/cluster/access/identity/create.hbs @@ -8,4 +8,4 @@
-{{identity/edit-form model=model onSave=(perform navToShow)}} +{{identity/edit-form model=model onSave=(perform navAfterSave)}} diff --git a/ui/app/templates/vault/cluster/access/identity/edit.hbs b/ui/app/templates/vault/cluster/access/identity/edit.hbs index a13fc9e44bb0..002496fa43e6 100644 --- a/ui/app/templates/vault/cluster/access/identity/edit.hbs +++ b/ui/app/templates/vault/cluster/access/identity/edit.hbs @@ -8,4 +8,4 @@ -{{identity/edit-form mode="edit" model=model onSave=(perform navToShow)}} +{{identity/edit-form mode="edit" model=model onSave=(perform navAfterSave)}} diff --git a/ui/app/templates/vault/cluster/access/identity/index.hbs b/ui/app/templates/vault/cluster/access/identity/index.hbs index c38c5b860732..ff751dabd54b 100644 --- a/ui/app/templates/vault/cluster/access/identity/index.hbs +++ b/ui/app/templates/vault/cluster/access/identity/index.hbs @@ -1,23 +1,103 @@ {{identity/entity-nav identityType=identityType}} {{#if model.meta.total}} {{#each model as |item|}} - - {{i-con - glyph="role" - size=14 - class="has-text-grey-light" - }} - - {{item.id}} - - +
+
+ {{i-con + glyph="role" + size=14 + class="has-text-grey-light" + }}{{item.id}} +
+
+ {{#popup-menu name="identity-item" onOpen=(action "reloadRecord" item)}} + + {{/popup-menu}} +
+
+ {{/linked-block}} {{/each}} {{else}}
diff --git a/ui/app/templates/vault/cluster/access/identity/merge.hbs b/ui/app/templates/vault/cluster/access/identity/merge.hbs index 064692ce9d1f..cef3a1f2e620 100644 --- a/ui/app/templates/vault/cluster/access/identity/merge.hbs +++ b/ui/app/templates/vault/cluster/access/identity/merge.hbs @@ -8,4 +8,4 @@
-{{identity/edit-form mode="merge" model=model onSave=(perform navToShow)}} +{{identity/edit-form mode="merge" model=model onSave=(perform navAfterSave)}} diff --git a/ui/app/templates/vault/cluster/access/identity/show.hbs b/ui/app/templates/vault/cluster/access/identity/show.hbs index 41afa97966c0..ee8385c7e38b 100644 --- a/ui/app/templates/vault/cluster/access/identity/show.hbs +++ b/ui/app/templates/vault/cluster/access/identity/show.hbs @@ -17,12 +17,12 @@
{{#unless (or (and (eq model.identityType "group") (eq model.type "internal")) model.alias)}} - + Add alias {{i-con glyph="chevron-right" size=11}} {{/unless}} - + Edit {{model.identityType}} {{i-con glyph="chevron-right" size=11}} @@ -34,7 +34,7 @@
{{#if (and (not-eq model.id "default") capabilities.canDelete)}} {{#confirm-action - buttonClasses="button is-link is-outlined is-inverted" + buttonClasses="button is-ghost" onConfirmAction=(action "deletePolicy" model) confirmMessage=(concat "Are you sure you want to delete " model.id "?") data-test-policy-delete=true diff --git a/ui/tests/acceptance/access/identity/_shared-alias-tests.js b/ui/tests/acceptance/access/identity/_shared-alias-tests.js new file mode 100644 index 000000000000..284c50f7e90a --- /dev/null +++ b/ui/tests/acceptance/access/identity/_shared-alias-tests.js @@ -0,0 +1,77 @@ +import page from 'vault/tests/pages/access/identity/aliases/add'; +import aliasIndexPage from 'vault/tests/pages/access/identity/aliases/index'; +import aliasShowPage from 'vault/tests/pages/access/identity/aliases/show'; +import createItemPage from 'vault/tests/pages/access/identity/create'; +import showItemPage from 'vault/tests/pages/access/identity/show'; + +export const testAliasCRUD = (name, itemType, assert) => { + let itemID; + let aliasID; + if (itemType === 'groups') { + createItemPage.createItem(itemType, 'external'); + } else { + createItemPage.createItem(itemType); + } + andThen(() => { + let idRow = showItemPage.rows.filterBy('hasLabel').filterBy('rowLabel', 'ID')[0]; + itemID = idRow.rowValue; + page.visit({ item_type: itemType, id: itemID }); + }); + page.editForm.name(name).submit(); + andThen(() => { + let idRow = aliasShowPage.rows.filterBy('hasLabel').filterBy('rowLabel', 'ID')[0]; + aliasID = idRow.rowValue; + assert.equal( + currentRouteName(), + 'vault.cluster.access.identity.aliases.show', + 'navigates to the correct route' + ); + assert.ok( + aliasShowPage.flashMessage.latestMessage.startsWith('Successfully saved', `${itemType}: shows a flash message`) + ); + assert.ok(aliasShowPage.nameContains(name), `${itemType}: renders the name on the show page`); + }); + + aliasIndexPage.visit({ item_type: itemType }); + andThen(() => { + assert.equal(aliasIndexPage.items.filterBy('id', aliasID).length, 1, `${itemType}: lists the entity in the entity list`); + aliasIndexPage.items.filterBy('id', aliasID)[0].menu(); + }); + aliasIndexPage.delete().confirmDelete(); + + andThen(() => { + assert.equal(aliasIndexPage.items.filterBy('id', aliasID).length, 0, `${itemType}: the row is deleted`); + aliasIndexPage.flashMessage.latestMessage.startsWith('Successfully deleted', `${itemType}: shows flash message`); + }); +}; + +export const testAliasDeleteFromForm = (name, itemType, assert) => { + let itemID; + let aliasID; + if (itemType === 'groups') { + createItemPage.createItem(itemType, 'external'); + } else { + createItemPage.createItem(itemType); + } + andThen(() => { + let idRow = showItemPage.rows.filterBy('hasLabel').filterBy('rowLabel', 'ID')[0]; + itemID = idRow.rowValue; + page.visit({ item_type: itemType, id: itemID }); + }); + page.editForm.name(name).submit(); + andThen(() => { + let idRow = aliasShowPage.rows.filterBy('hasLabel').filterBy('rowLabel', 'ID')[0]; + aliasID = idRow.rowValue; + }); + aliasShowPage.edit(); + + andThen(() => { + assert.equal(currentRouteName(), 'vault.cluster.access.identity.aliases.edit', `${itemType}: navigates to edit on create`); + }); + page.editForm.delete().confirmDelete(); + andThen(() => { + assert.equal(currentRouteName(), 'vault.cluster.access.identity.aliases.index', `${itemType}: navigates to list page on delete`); + assert.equal(aliasIndexPage.items.filterBy('id', aliasID).length, 0, `${itemType}: the row does not show in the list`); + aliasIndexPage.flashMessage.latestMessage.startsWith('Successfully deleted', `${itemType}: shows flash message`); + }); +}; diff --git a/ui/tests/acceptance/access/identity/_shared-tests.js b/ui/tests/acceptance/access/identity/_shared-tests.js new file mode 100644 index 000000000000..d91409ea5da9 --- /dev/null +++ b/ui/tests/acceptance/access/identity/_shared-tests.js @@ -0,0 +1,51 @@ +import page from 'vault/tests/pages/access/identity/create'; +import showPage from 'vault/tests/pages/access/identity/show'; +import indexPage from 'vault/tests/pages/access/identity/index'; + +export const testCRUD = (name, itemType, assert) => { + let id; + page.visit({ item_type: itemType }); + page.editForm.name(name).submit(); + andThen(() => { + let idRow = showPage.rows.filterBy('hasLabel').filterBy('rowLabel', 'ID')[0]; + id = idRow.rowValue; + assert.equal(currentRouteName(), 'vault.cluster.access.identity.show', `${itemType}: navigates to show on create`); + assert.ok( + showPage.flashMessage.latestMessage.startsWith('Successfully saved', `${itemType}: shows a flash message`) + ); + assert.ok(showPage.nameContains(name), `${itemType}: renders the name on the show page`); + }); + + indexPage.visit({ item_type: itemType }); + andThen(() => { + assert.equal(indexPage.items.filterBy('id', id).length, 1, `${itemType}: lists the entity in the entity list`); + indexPage.items.filterBy('id', id)[0].menu(); + }); + indexPage.delete().confirmDelete(); + + andThen(() => { + assert.equal(indexPage.items.filterBy('id', id).length, 0, `${itemType}: the row is deleted`); + indexPage.flashMessage.latestMessage.startsWith('Successfully deleted', `${itemType}: shows flash message`); + }); +}; + + +export const testDeleteFromForm = (name, itemType, assert) => { + let id; + page.visit({ item_type: itemType }); + page.editForm.name(name).submit(); + andThen(() => { + id = showPage.rows.filterBy('hasLabel').filterBy('rowLabel', 'ID')[0].rowValue + }); + showPage.edit(); + andThen(() => { + assert.equal(currentRouteName(), 'vault.cluster.access.identity.edit', `${itemType}: navigates to edit on create`); + }); + page.editForm.delete().confirmDelete(); + andThen(() => { + assert.equal(currentRouteName(), 'vault.cluster.access.identity.index', `${itemType}: navigates to list page on delete`); + assert.equal(indexPage.items.filterBy('id', id).length, 0, `${itemType}: the row does not show in the list`); + indexPage.flashMessage.latestMessage.startsWith('Successfully deleted', `${itemType}: shows flash message`); + }); +}; + diff --git a/ui/tests/acceptance/access/identity/entities/aliases/create-test.js b/ui/tests/acceptance/access/identity/entities/aliases/create-test.js new file mode 100644 index 000000000000..81e9e2b51a6c --- /dev/null +++ b/ui/tests/acceptance/access/identity/entities/aliases/create-test.js @@ -0,0 +1,21 @@ +import { test } from 'qunit'; +import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance'; +import { testAliasCRUD, testAliasDeleteFromForm } from '../../_shared-alias-tests'; + +moduleForAcceptance('Acceptance | /access/identity/entities/aliases/add', { + beforeEach() { + return authLogin(); + }, +}); + + +test('it allows create, list, delete of an entity alias', function(assert) { + let name = `alias-${Date.now()}`; + testAliasCRUD(name, 'entities', assert); +}); + +test('it allows delete from the edit form', function(assert) { + let name = `alias-${Date.now()}`; + testAliasDeleteFromForm(name, 'entities', assert); +}); + diff --git a/ui/tests/acceptance/access/identity/entities/create-test.js b/ui/tests/acceptance/access/identity/entities/create-test.js new file mode 100644 index 000000000000..12b064229560 --- /dev/null +++ b/ui/tests/acceptance/access/identity/entities/create-test.js @@ -0,0 +1,32 @@ +import { test } from 'qunit'; +import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance'; +import page from 'vault/tests/pages/access/identity/create'; +import { testCRUD, testDeleteFromForm } from '../_shared-tests'; + +moduleForAcceptance('Acceptance | /access/identity/entities/create', { + beforeEach() { + return authLogin(); + }, +}); + +test('it visits the correct page', function(assert) { + page.visit({ item_type: 'entities' }); + andThen(() => { + assert.equal( + currentRouteName(), + 'vault.cluster.access.identity.create', + 'navigates to the correct route' + ); + }); +}); + +test('it allows create, list, delete of an entity', function(assert) { + let name = `entity-${Date.now()}`; + testCRUD(name, 'entities', assert); +}); + +test('it can be deleted from the edit form', function(assert) { + let name = `entity-${Date.now()}`; + testDeleteFromForm(name, 'entities', assert); +}); + diff --git a/ui/tests/acceptance/access/identity/entities/index-test.js b/ui/tests/acceptance/access/identity/entities/index-test.js new file mode 100644 index 000000000000..e5190868deb7 --- /dev/null +++ b/ui/tests/acceptance/access/identity/entities/index-test.js @@ -0,0 +1,23 @@ +import { test } from 'qunit'; +import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance'; +import page from 'vault/tests/pages/access/identity/index'; + +moduleForAcceptance('Acceptance | /access/identity/entities', { + beforeEach() { + return authLogin(); + }, +}); + +test('it renders the entities page', function(assert) { + page.visit({ item_type: 'entities' }); + andThen(() => { + assert.equal(currentRouteName(), 'vault.cluster.access.identity.index', 'navigates to the correct route'); + }); +}); + +test('it renders the groups page', function(assert) { + page.visit({ item_type: 'groups' }); + andThen(() => { + assert.equal(currentRouteName(), 'vault.cluster.access.identity.index', 'navigates to the correct route'); + }); +}); diff --git a/ui/tests/acceptance/access/identity/groups/aliases/create-test.js b/ui/tests/acceptance/access/identity/groups/aliases/create-test.js new file mode 100644 index 000000000000..d40f94e90709 --- /dev/null +++ b/ui/tests/acceptance/access/identity/groups/aliases/create-test.js @@ -0,0 +1,21 @@ +import { test } from 'qunit'; +import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance'; +import { testAliasCRUD, testAliasDeleteFromForm } from '../../_shared-alias-tests'; + +moduleForAcceptance('Acceptance | /access/identity/groups/aliases/add', { + beforeEach() { + return authLogin(); + }, +}); + + +test('it allows create, list, delete of an entity alias', function(assert) { + let name = `alias-${Date.now()}`; + testAliasCRUD(name, 'groups', assert); +}); + +test('it allows delete from the edit form', function(assert) { + let name = `alias-${Date.now()}`; + testAliasDeleteFromForm(name, 'groups', assert); +}); + diff --git a/ui/tests/acceptance/access/identity/groups/create-test.js b/ui/tests/acceptance/access/identity/groups/create-test.js new file mode 100644 index 000000000000..484f623d8a8a --- /dev/null +++ b/ui/tests/acceptance/access/identity/groups/create-test.js @@ -0,0 +1,31 @@ +import { test } from 'qunit'; +import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance'; +import page from 'vault/tests/pages/access/identity/create'; +import { testCRUD, testDeleteFromForm } from '../_shared-tests'; + +moduleForAcceptance('Acceptance | /access/identity/groups/create', { + beforeEach() { + return authLogin(); + }, +}); + +test('it visits the correct page', function(assert) { + page.visit({ item_type: 'groups' }); + andThen(() => { + assert.equal( + currentRouteName(), + 'vault.cluster.access.identity.create', + 'navigates to the correct route' + ); + }); +}); + +test('it allows create, list, delete of an group', function(assert) { + let name = `group-${Date.now()}`; + testCRUD(name, 'groups', assert); +}); + +test('it can be deleted from the group edit form', function(assert) { + let name = `group-${Date.now()}`; + testDeleteFromForm(name, 'groups', assert); +}); diff --git a/ui/tests/acceptance/access/identity/index-test.js b/ui/tests/acceptance/access/identity/index-test.js deleted file mode 100644 index ed8659a65e70..000000000000 --- a/ui/tests/acceptance/access/identity/index-test.js +++ /dev/null @@ -1,16 +0,0 @@ -import { test } from 'qunit'; -import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance'; -import page from 'vault/tests/pages/access/identity/index'; - -moduleForAcceptance('Acceptance | /access/identity/entities', { - beforeEach() { - return authLogin(); - }, -}); - -test('it renders the page', function(assert) { - page.visit({ item_type: 'entities' }); - andThen(() => { - assert.ok(currentRouteName(), 'vault.cluster.access.identity.index', 'navigates to the correct route'); - }); -}); diff --git a/ui/tests/acceptance/enterprise-replication-test.js b/ui/tests/acceptance/enterprise-replication-test.js index 06b9d3302654..8489bc19d07a 100644 --- a/ui/tests/acceptance/enterprise-replication-test.js +++ b/ui/tests/acceptance/enterprise-replication-test.js @@ -87,7 +87,9 @@ test('replication', function(assert) { find('[data-test-mount-config-mode]').text().trim().toLowerCase().includes(mode), 'show page renders the correct mode' ); - assert.dom('[data-test-mount-config-paths]').hasText(mountPath, 'show page renders the correct mount path'); + assert + .dom('[data-test-mount-config-paths]') + .hasText(mountPath, 'show page renders the correct mount path'); }); // click edit @@ -101,10 +103,12 @@ test('replication', function(assert) { `/vault/replication/performance/secondaries`, 'redirects to the secondaries page' ); - assert.dom('[data-test-flash-message-body]:contains(The performance mount filter)').hasText( - `The performance mount filter config for the secondary ${secondaryName} was successfully deleted.`, - 'renders success flash upon deletion' - ); + assert + .dom('[data-test-flash-message-body]:contains(The performance mount filter)') + .hasText( + `The performance mount filter config for the secondary ${secondaryName} was successfully deleted.`, + 'renders success flash upon deletion' + ); click('[data-test-flash-message-body]:contains(The performance mount filter)'); }); @@ -149,10 +153,9 @@ test('replication', function(assert) { }); click('[data-test-replication-link="secondaries"]'); andThen(() => { - assert.dom('[data-test-secondary-name]').hasText( - secondaryName, - 'it displays the secondary in the list of known secondaries' - ); + assert + .dom('[data-test-secondary-name]') + .hasText(secondaryName, 'it displays the secondary in the list of known secondaries'); }); // disable dr replication diff --git a/ui/tests/acceptance/leases-test.js b/ui/tests/acceptance/leases-test.js index 8b01af3d1d6e..1d479fe8cf58 100644 --- a/ui/tests/acceptance/leases-test.js +++ b/ui/tests/acceptance/leases-test.js @@ -51,7 +51,9 @@ test('it renders the show page', function(assert) { 'vault.cluster.access.leases.show', 'a lease for the secret is in the list' ); - assert.dom('[data-test-lease-renew-picker]').doesNotExist('non-renewable lease does not render a renew picker'); + assert + .dom('[data-test-lease-renew-picker]') + .doesNotExist('non-renewable lease does not render a renew picker'); }); }); @@ -65,7 +67,9 @@ skip('it renders the show page with a picker', function(assert) { 'vault.cluster.access.leases.show', 'a lease for the secret is in the list' ); - assert.dom('[data-test-lease-renew-picker]').exists({ count: 1 }, 'renewable lease renders a renew picker'); + assert + .dom('[data-test-lease-renew-picker]') + .exists({ count: 1 }, 'renewable lease renders a renew picker'); }); }); @@ -84,7 +88,9 @@ test('it removes leases upon revocation', function(assert) { click(`[data-test-lease-link="${this.enginePath}/"]`); click('[data-test-lease-link="data/"]'); andThen(() => { - assert.dom(`[data-test-lease-link="${this.enginePath}/data/${this.name}/"]`).doesNotExist('link to the lease was removed with revocation'); + assert + .dom(`[data-test-lease-link="${this.enginePath}/data/${this.name}/"]`) + .doesNotExist('link to the lease was removed with revocation'); }); }); @@ -99,16 +105,17 @@ test('it removes branches when a prefix is revoked', function(assert) { 'vault.cluster.access.leases.list-root', 'it navigates back to the leases root on revocation' ); - assert.dom(`[data-test-lease-link="${this.enginePath}/"]`).doesNotExist('link to the prefix was removed with revocation'); + assert + .dom(`[data-test-lease-link="${this.enginePath}/"]`) + .doesNotExist('link to the prefix was removed with revocation'); }); }); test('lease not found', function(assert) { visit('/vault/access/leases/show/not-found'); andThen(() => { - assert.dom('[data-test-lease-error]').hasText( - 'not-found is not a valid lease ID', - 'it shows an error when the lease is not found' - ); + assert + .dom('[data-test-lease-error]') + .hasText('not-found is not a valid lease ID', 'it shows an error when the lease is not found'); }); }); diff --git a/ui/tests/acceptance/policies-acl-old-test.js b/ui/tests/acceptance/policies-acl-old-test.js index 9d161a6affd3..65195689c7cf 100644 --- a/ui/tests/acceptance/policies-acl-old-test.js +++ b/ui/tests/acceptance/policies-acl-old-test.js @@ -46,7 +46,9 @@ test('policies', function(assert) { }); click('[data-test-policy-list-link]'); andThen(function() { - assert.dom(`[data-test-policy-link="${policyLower}"]`).exists({ count: 1 }, 'new policy shown in the list'); + assert + .dom(`[data-test-policy-link="${policyLower}"]`) + .exists({ count: 1 }, 'new policy shown in the list'); }); // policy deletion @@ -56,7 +58,9 @@ test('policies', function(assert) { click('[data-test-confirm-button]'); andThen(function() { assert.equal(currentURL(), `/vault/policies/acl`, 'navigates to policy list on successful deletion'); - assert.dom(`[data-test-policy-item="${policyLower}"]`).doesNotExist('deleted policy is not shown in the list'); + assert + .dom(`[data-test-policy-item="${policyLower}"]`) + .doesNotExist('deleted policy is not shown in the list'); }); }); diff --git a/ui/tests/acceptance/settings-test.js b/ui/tests/acceptance/settings-test.js index fba94b0c820c..182d1a168307 100644 --- a/ui/tests/acceptance/settings-test.js +++ b/ui/tests/acceptance/settings-test.js @@ -46,10 +46,6 @@ test('settings', function(assert) { }); andThen(() => { - assert.ok( - currentURL(), - '/vault/secrets/${path}/configuration', - 'navigates to the config page' - ); + assert.ok(currentURL(), '/vault/secrets/${path}/configuration', 'navigates to the config page'); }); }); diff --git a/ui/tests/acceptance/ssh-test.js b/ui/tests/acceptance/ssh-test.js index 0e9199286fcc..9abee52b4918 100644 --- a/ui/tests/acceptance/ssh-test.js +++ b/ui/tests/acceptance/ssh-test.js @@ -143,7 +143,9 @@ test('ssh backend', function(assert) { click(`[data-test-confirm-button]`); andThen(() => { - assert.dom(`[data-test-secret-link="${role.name}"]`).doesNotExist(`${role.type}: role is no longer in the list`); + assert + .dom(`[data-test-secret-link="${role.name}"]`) + .doesNotExist(`${role.type}: role is no longer in the list`); }); }); }); diff --git a/ui/tests/acceptance/tools-test.js b/ui/tests/acceptance/tools-test.js index 7a108eb3e2c2..e5d05d2215ef 100644 --- a/ui/tests/acceptance/tools-test.js +++ b/ui/tests/acceptance/tools-test.js @@ -118,50 +118,46 @@ test('tools functionality', function(assert) { click('[data-test-tools-b64-toggle="input"]'); click('[data-test-tools-submit]'); andThen(() => { - assert.dom('[data-test-tools-input="sum"]').hasValue( - 'LCa0a2j/xo/5m0U8HTBBNBNCLXBkg7+g+YpeiGJm564=', - 'hashes the data, encodes input' - ); + assert + .dom('[data-test-tools-input="sum"]') + .hasValue('LCa0a2j/xo/5m0U8HTBBNBNCLXBkg7+g+YpeiGJm564=', 'hashes the data, encodes input'); }); click('[data-test-tools-back]'); fillIn('[data-test-tools-input="hash-input"]', 'e2RhdGE6ImZvbyJ9'); click('[data-test-tools-submit]'); andThen(() => { - assert.dom('[data-test-tools-input="sum"]').hasValue( - 'JmSi2Hhbgu2WYOrcOyTqqMdym7KT3sohCwAwaMonVrc=', - 'hashes the data, passes b64 input through' - ); + assert + .dom('[data-test-tools-input="sum"]') + .hasValue('JmSi2Hhbgu2WYOrcOyTqqMdym7KT3sohCwAwaMonVrc=', 'hashes the data, passes b64 input through'); }); }); const AUTH_RESPONSE = { - "request_id": "39802bc4-235c-2f0b-87f3-ccf38503ac3e", - "lease_id": "", - "renewable": false, - "lease_duration": 0, - "data": null, - "wrap_info": null, - "warnings": null, - "auth": { - "client_token": "ecfc2758-588e-981d-50f4-a25883bbf03c", - "accessor": "6299780b-f2b2-1a3f-7b83-9d3d67629249", - "policies": [ - "root" - ], - "metadata": null, - "lease_duration": 0, - "renewable": false, - "entity_id": "" - } + request_id: '39802bc4-235c-2f0b-87f3-ccf38503ac3e', + lease_id: '', + renewable: false, + lease_duration: 0, + data: null, + wrap_info: null, + warnings: null, + auth: { + client_token: 'ecfc2758-588e-981d-50f4-a25883bbf03c', + accessor: '6299780b-f2b2-1a3f-7b83-9d3d67629249', + policies: ['root'], + metadata: null, + lease_duration: 0, + renewable: false, + entity_id: '', + }, }; test('ensure unwrap with auth block works properly', function(assert) { - this.server = new Pretender(function() { - this.post('/v1/sys/wrapping/unwrap', response => { - return [response, { 'Content-Type': 'application/json' }, JSON.stringify(AUTH_RESPONSE)]; - }); + this.server = new Pretender(function() { + this.post('/v1/sys/wrapping/unwrap', response => { + return [response, { 'Content-Type': 'application/json' }, JSON.stringify(AUTH_RESPONSE)]; }); + }); visit('/vault/tools'); //unwrap click('[data-test-tools-action-link="unwrap"]'); diff --git a/ui/tests/acceptance/transit-test.js b/ui/tests/acceptance/transit-test.js index def20a11774e..ca1e1110bb8c 100644 --- a/ui/tests/acceptance/transit-test.js +++ b/ui/tests/acceptance/transit-test.js @@ -101,17 +101,21 @@ const testEncryption = (assert, keyName) => { ); }, assertBeforeDecrypt: key => { - assert.dom('[data-test-transit-input="context"]').hasValue( - 'nqR8LiVgNh/lwO2rArJJE9F9DMhh0lKo4JX9DAAkCDw=', - `${key}: the ui shows the base64-encoded context` - ); + assert + .dom('[data-test-transit-input="context"]') + .hasValue( + 'nqR8LiVgNh/lwO2rArJJE9F9DMhh0lKo4JX9DAAkCDw=', + `${key}: the ui shows the base64-encoded context` + ); }, assertAfterDecrypt: key => { - assert.dom('[data-test-transit-input="plaintext"]').hasValue( - 'NaXud2QW7KjyK6Me9ggh+zmnCeBGdG93LQED49PtoOI=', - `${key}: the ui shows the base64-encoded plaintext` - ); + assert + .dom('[data-test-transit-input="plaintext"]') + .hasValue( + 'NaXud2QW7KjyK6Me9ggh+zmnCeBGdG93LQED49PtoOI=', + `${key}: the ui shows the base64-encoded plaintext` + ); }, }, // raw bytes for plaintext, string for context @@ -128,13 +132,17 @@ const testEncryption = (assert, keyName) => { ); }, assertBeforeDecrypt: key => { - assert.dom('[data-test-transit-input="context"]').hasValue(encodeString('context'), `${key}: the ui shows the input context`); + assert + .dom('[data-test-transit-input="context"]') + .hasValue(encodeString('context'), `${key}: the ui shows the input context`); }, assertAfterDecrypt: key => { - assert.dom('[data-test-transit-input="plaintext"]').hasValue( - 'NaXud2QW7KjyK6Me9ggh+zmnCeBGdG93LQED49PtoOI=', - `${key}: the ui shows the base64-encoded plaintext` - ); + assert + .dom('[data-test-transit-input="plaintext"]') + .hasValue( + 'NaXud2QW7KjyK6Me9ggh+zmnCeBGdG93LQED49PtoOI=', + `${key}: the ui shows the base64-encoded plaintext` + ); }, }, // base64 input @@ -151,10 +159,14 @@ const testEncryption = (assert, keyName) => { ); }, assertBeforeDecrypt: key => { - assert.dom('[data-test-transit-input="context"]').hasValue(encodeString('context'), `${key}: the ui shows the input context`); + assert + .dom('[data-test-transit-input="context"]') + .hasValue(encodeString('context'), `${key}: the ui shows the input context`); }, assertAfterDecrypt: key => { - assert.dom('[data-test-transit-input="plaintext"]').hasValue('This is the secret', `${key}: the ui decodes plaintext`); + assert + .dom('[data-test-transit-input="plaintext"]') + .hasValue('This is the secret', `${key}: the ui decodes plaintext`); }, }, @@ -173,11 +185,15 @@ const testEncryption = (assert, keyName) => { ); }, assertBeforeDecrypt: key => { - assert.dom('[data-test-transit-input="context"]').hasValue(encodeString('secret 2'), `${key}: the ui shows the encoded context`); + assert + .dom('[data-test-transit-input="context"]') + .hasValue(encodeString('secret 2'), `${key}: the ui shows the encoded context`); }, assertAfterDecrypt: key => { assert.ok(findWithAssert('[data-test-transit-input="plaintext"]'), `${key}: plaintext box shows`); - assert.dom('[data-test-transit-input="plaintext"]').hasValue('There are many secrets 🤐', `${key}: the ui decodes plaintext`); + assert + .dom('[data-test-transit-input="plaintext"]') + .hasValue('There are many secrets 🤐', `${key}: the ui decodes plaintext`); }, }, ]; @@ -229,12 +245,16 @@ test('transit backend', function(assert) { if (index === 0) { click('[data-test-transit-link="versions"]'); andThen(() => { - assert.dom('[data-test-transit-key-version-row]').exists({ count: 1 }, `${key.name}: only one key version`); + assert + .dom('[data-test-transit-key-version-row]') + .exists({ count: 1 }, `${key.name}: only one key version`); }); click('[data-test-transit-key-rotate] button'); click('[data-test-confirm-button]'); andThen(() => { - assert.dom('[data-test-transit-key-version-row]').exists({ count: 2 }, `${key.name}: two key versions after rotate`); + assert + .dom('[data-test-transit-key-version-row]') + .exists({ count: 2 }, `${key.name}: two key versions after rotate`); }); } click('[data-test-transit-key-actions-link]'); @@ -256,7 +276,9 @@ test('transit backend', function(assert) { `${key.name}: exportable key has a link to export action` ); } else { - assert.dom('[data-test-transit-action-link="export"]').doesNotExist(`${key.name}: non-exportable key does not link to export action`); + assert + .dom('[data-test-transit-action-link="export"]') + .doesNotExist(`${key.name}: non-exportable key does not link to export action`); } if (key.convergent && key.supportsEncryption) { testEncryption(assert, key.name); diff --git a/ui/tests/integration/components/identity/item-details-test.js b/ui/tests/integration/components/identity/item-details-test.js new file mode 100644 index 000000000000..4cced3a44095 --- /dev/null +++ b/ui/tests/integration/components/identity/item-details-test.js @@ -0,0 +1,56 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; +import sinon from 'sinon'; +import { create } from 'ember-cli-page-object'; +import itemDetails from 'vault/tests/pages/components/identity/item-details'; +import Ember from 'ember'; + +const component = create(itemDetails); +const { getOwner } = Ember; + +moduleForComponent('identity/item-details', 'Integration | Component | identity/item details', { + integration: true, + beforeEach() { + component.setContext(this); + getOwner(this).lookup('service:flash-messages').registerTypes(['success']); + }, + afterEach() { + component.removeContext(); + } +}); + +test('it renders the disabled warning', function(assert) { + let model = Ember.Object.create({ + save() { + return Ember.RSVP.resolve(); + }, + disabled: true, + canEdit: true + }); + sinon.spy(model, 'save'); + this.set('model', model); + this.render(hbs`{{identity/item-details model=model}}`); + assert.dom('[data-test-disabled-warning]').exists(); + component.enable(); + + assert.ok(model.save.calledOnce, 'clicking enable calls model save'); +}); + +test('it does not render the button if canEdit is false', function(assert) { + let model = Ember.Object.create({ + disabled: true + }); + + this.set('model', model); + this.render(hbs`{{identity/item-details model=model}}`); + assert.dom('[data-test-disabled-warning]').exists('shows the warning banner'); + assert.dom('[data-test-enable]').doesNotExist('does not show the enable button'); +}); + +test('it does not render the banner when item is enabled', function(assert) { + let model = Ember.Object.create(); + this.set('model', model); + + this.render(hbs`{{identity/item-details model=model}}`); + assert.dom('[data-test-disabled-warning]').doesNotExist('does not show the warning banner'); +}); diff --git a/ui/tests/pages/access/identity/aliases/add.js b/ui/tests/pages/access/identity/aliases/add.js new file mode 100644 index 000000000000..4be991f37ec8 --- /dev/null +++ b/ui/tests/pages/access/identity/aliases/add.js @@ -0,0 +1,7 @@ +import { create, visitable } from 'ember-cli-page-object'; +import editForm from 'vault/tests/pages/components/identity/edit-form'; + +export default create({ + visit: visitable('/vault/access/identity/:item_type/aliases/add/:id'), + editForm, +}); diff --git a/ui/tests/pages/access/identity/aliases/index.js b/ui/tests/pages/access/identity/aliases/index.js new file mode 100644 index 000000000000..297dbeab8176 --- /dev/null +++ b/ui/tests/pages/access/identity/aliases/index.js @@ -0,0 +1,13 @@ +import { create, clickable, text, visitable, collection } from 'ember-cli-page-object'; +import flashMessage from 'vault/tests/pages/components/flash-message'; + +export default create({ + visit: visitable('/vault/access/identity/:item_type/aliases'), + flashMessage, + items: collection('[data-test-identity-row]', { + menu: clickable('[data-test-popup-menu-trigger]'), + id: text('[data-test-identity-link]'), + }), + delete: clickable('[data-test-item-delete] [data-test-confirm-action-trigger]'), + confirmDelete: clickable('[data-test-item-delete] [data-test-confirm-button]'), +}); diff --git a/ui/tests/pages/access/identity/aliases/show.js b/ui/tests/pages/access/identity/aliases/show.js new file mode 100644 index 000000000000..5ca908ad8dd6 --- /dev/null +++ b/ui/tests/pages/access/identity/aliases/show.js @@ -0,0 +1,11 @@ +import { create, clickable, collection, contains, visitable } from 'ember-cli-page-object'; +import flashMessage from 'vault/tests/pages/components/flash-message'; +import infoTableRow from 'vault/tests/pages/components/info-table-row'; + +export default create({ + visit: visitable('/vault/access/identity/:item_type/aliases/:alias_id'), + flashMessage, + nameContains: contains('[data-test-alias-name]'), + rows: collection('[data-test-component="info-table-row"]', infoTableRow), + edit: clickable('[data-test-alias-edit-link]') +}); diff --git a/ui/tests/pages/access/identity/create.js b/ui/tests/pages/access/identity/create.js new file mode 100644 index 000000000000..8fbaac076a97 --- /dev/null +++ b/ui/tests/pages/access/identity/create.js @@ -0,0 +1,13 @@ +import { create, visitable } from 'ember-cli-page-object'; +import editForm from 'vault/tests/pages/components/identity/edit-form'; + +export default create({ + visit: visitable('/vault/access/identity/:item_type/create'), + editForm, + createItem(item_type, type) { + if (type) { + return this.visit({item_type}).editForm.type(type).submit(); + } + return this.visit({item_type}).editForm.submit(); + } +}); diff --git a/ui/tests/pages/access/identity/index.js b/ui/tests/pages/access/identity/index.js index 96e44360b08f..4a79ea39f6a9 100644 --- a/ui/tests/pages/access/identity/index.js +++ b/ui/tests/pages/access/identity/index.js @@ -1,4 +1,13 @@ -import { create, visitable } from 'ember-cli-page-object'; +import { create, clickable, text, visitable, collection } from 'ember-cli-page-object'; +import flashMessage from 'vault/tests/pages/components/flash-message'; + export default create({ visit: visitable('/vault/access/identity/:item_type'), + flashMessage, + items: collection('[data-test-identity-row]', { + menu: clickable('[data-test-popup-menu-trigger]'), + id: text('[data-test-identity-link]'), + }), + delete: clickable('[data-test-item-delete] [data-test-confirm-action-trigger]'), + confirmDelete: clickable('[data-test-item-delete] [data-test-confirm-button]'), }); diff --git a/ui/tests/pages/access/identity/show.js b/ui/tests/pages/access/identity/show.js new file mode 100644 index 000000000000..ffaf79da0869 --- /dev/null +++ b/ui/tests/pages/access/identity/show.js @@ -0,0 +1,11 @@ +import { create, clickable, collection, contains, visitable } from 'ember-cli-page-object'; +import flashMessage from 'vault/tests/pages/components/flash-message'; +import infoTableRow from 'vault/tests/pages/components/info-table-row'; + +export default create({ + visit: visitable('/vault/access/identity/:item_type/:item_id'), + flashMessage, + nameContains: contains('[data-test-identity-item-name]'), + rows: collection('[data-test-component="info-table-row"]', infoTableRow), + edit: clickable('[data-test-entity-edit-link]') +}); diff --git a/ui/tests/pages/components/identity/edit-form.js b/ui/tests/pages/components/identity/edit-form.js new file mode 100644 index 000000000000..a77daa8b7f21 --- /dev/null +++ b/ui/tests/pages/components/identity/edit-form.js @@ -0,0 +1,14 @@ +import { clickable, fillable, attribute } from 'ember-cli-page-object'; +import fields from '../form-field'; + +export default { + ...fields, + cancelLinkHref: attribute('href', '[data-test-cancel-link]'), + cancelLink: clickable('[data-test-cancel-link]'), + name: fillable('[data-test-input="name"]'), + disabled: clickable('[data-test-input="disabled"]'), + type: fillable('[data-test-input="type"]'), + submit: clickable('[data-test-identity-submit]'), + delete: clickable('[data-test-confirm-action-trigger]'), + confirmDelete: clickable('[data-test-confirm-button]'), +}; diff --git a/ui/tests/pages/components/identity/item-details.js b/ui/tests/pages/components/identity/item-details.js new file mode 100644 index 000000000000..9a10b66f732a --- /dev/null +++ b/ui/tests/pages/components/identity/item-details.js @@ -0,0 +1,5 @@ +import { clickable } from 'ember-cli-page-object'; + +export default { + enable: clickable('[data-test-enable]'), +}; diff --git a/ui/tests/pages/components/info-table-row.js b/ui/tests/pages/components/info-table-row.js new file mode 100644 index 000000000000..9a404f97ae0a --- /dev/null +++ b/ui/tests/pages/components/info-table-row.js @@ -0,0 +1,7 @@ +import { text, isPresent } from 'ember-cli-page-object'; + +export default { + hasLabel: isPresent('[data-test-row-label]'), + rowLabel: text('[data-test-row-label]'), + rowValue: text('[data-test-row-value]'), +}; diff --git a/ui/tests/unit/components/identity/edit-form-test.js b/ui/tests/unit/components/identity/edit-form-test.js new file mode 100644 index 000000000000..82aae87cdb3a --- /dev/null +++ b/ui/tests/unit/components/identity/edit-form-test.js @@ -0,0 +1,73 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import sinon from 'sinon'; +import Ember from 'ember'; + +moduleForComponent('identity/edit-form', 'Unit | Component | identity/edit-form', { + unit: true, + needs: ['service:auth', 'service:flash-messages'], +}); + +let testCases = [ + { + identityType: 'entity', + mode: 'create', + expected: 'vault.cluster.access.identity', + }, + { + identityType: 'entity', + mode: 'edit', + expected: 'vault.cluster.access.identity.show', + }, + { + identityType: 'entity-merge', + mode: 'merge', + expected: 'vault.cluster.access.identity', + }, + { + identityType: 'entity-alias', + mode: 'create', + expected: 'vault.cluster.access.identity.aliases', + }, + { + identityType: 'entity-alias', + mode: 'edit', + expected: 'vault.cluster.access.identity.aliases.show', + }, + { + identityType: 'group', + mode: 'create', + expected: 'vault.cluster.access.identity', + }, + { + identityType: 'group', + mode: 'edit', + expected: 'vault.cluster.access.identity.show', + }, + { + identityType: 'group-alias', + mode: 'create', + expected: 'vault.cluster.access.identity.aliases', + }, + { + identityType: 'group-alias', + mode: 'edit', + expected: 'vault.cluster.access.identity.aliases.show', + }, +]; +testCases.forEach(function(testCase) { + let model = Ember.Object.create({ + identityType: testCase.identityType, + rollbackAttributes: sinon.spy(), + }); + test(`it computes cancelLink properly: ${testCase.identityType} ${testCase.mode}`, function(assert) { + let component = this.subject(); + + component.set('mode', testCase.mode); + component.set('model', model); + assert.equal( + component.get('cancelLink'), + testCase.expected, + 'cancel link is correct' + ); + }); +}); diff --git a/ui/tests/unit/services/store-test.js b/ui/tests/unit/services/store-test.js index ab2f95fa0b9a..4476176c3d48 100644 --- a/ui/tests/unit/services/store-test.js +++ b/ui/tests/unit/services/store-test.js @@ -89,6 +89,7 @@ test('store.constructResponse', function(assert) { }); test('store.fetchPage', function(assert) { + let done = assert.async(4); const keys = ['zero', 'one', 'two', 'three', 'four', 'five', 'six']; const data = { data: { @@ -106,11 +107,14 @@ test('store.fetchPage', function(assert) { let result; Ember.run(() => { - result = store.fetchPage('transit-key', query); + store.fetchPage('transit-key', query).then(r => { + result = r; + done(); + }); }); assert.ok(result.get('length'), pageSize, 'returns the correct number of items'); - assert.deepEqual(result.toArray().mapBy('id'), keys.slice(0, pageSize), 'returns the first page of items'); + assert.deepEqual(result.mapBy('id'), keys.slice(0, pageSize), 'returns the first page of items'); assert.deepEqual( result.get('meta'), { @@ -125,44 +129,54 @@ test('store.fetchPage', function(assert) { ); Ember.run(() => { - result = store.fetchPage('transit-key', { + store.fetchPage('transit-key', { size: pageSize, page: 3, responsePath: 'data.keys', + }).then(r => { + result = r; + done() }); }); const pageThreeEnd = 3 * pageSize; const pageThreeStart = pageThreeEnd - pageSize; assert.deepEqual( - result.toArray().mapBy('id'), + result.mapBy('id'), keys.slice(pageThreeStart, pageThreeEnd), 'returns the third page of items' ); Ember.run(() => { - result = store.fetchPage('transit-key', { + store.fetchPage('transit-key', { size: pageSize, page: 99, responsePath: 'data.keys', + }).then(r => { + + result = r; + done(); }); }); assert.deepEqual( - result.toArray().mapBy('id'), + result.mapBy('id'), keys.slice(keys.length - 1), 'returns the last page when the page value is beyond the of bounds' ); Ember.run(() => { - result = store.fetchPage('transit-key', { + store.fetchPage('transit-key', { size: pageSize, page: 0, responsePath: 'data.keys', + }).then(r => { + result = r; + done(); }); }); assert.deepEqual( - result.toArray().mapBy('id'), + result.mapBy('id'), keys.slice(0, pageSize), 'returns the first page when page value is under the bounds' );