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}}
-
-
-
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}}
+
+
+ Enable
+
+
+ {{/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"
+ }}
+
+
+
+ {{#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}}
+ }}
+
+
+
+ {{#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 @@
}}
+ {{#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}}
-
-
+
+
+
+ {{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 @@
-{{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}}
-
-
+
+
+
+ {{#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 @@
{{#each (tabs-for-identity-show model.identityType model.type) as |tab|}}
{{#link-to "vault.cluster.access.identity.show" model.id tab tagName="li"}}
-
+
{{capitalize tab}}
{{/link-to}}
diff --git a/ui/app/templates/vault/cluster/policy/edit.hbs b/ui/app/templates/vault/cluster/policy/edit.hbs
index 850bcb8f3a34..e565135585fd 100644
--- a/ui/app/templates/vault/cluster/policy/edit.hbs
+++ b/ui/app/templates/vault/cluster/policy/edit.hbs
@@ -92,7 +92,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'
);