diff --git a/ui/app/adapters/kmip/credential.js b/ui/app/adapters/kmip/credential.js
index b485d1b26c..d6c6bb8d72 100644
--- a/ui/app/adapters/kmip/credential.js
+++ b/ui/app/adapters/kmip/credential.js
@@ -13,4 +13,18 @@ export default BaseAdapter.extend({
return model;
});
},
+
+ deleteRecord(store, type, snapshot) {
+ let url = this._url(type.modelName, {
+ backend: snapshot.record.backend,
+ scope: snapshot.record.scope,
+ role: snapshot.record.role,
+ });
+ url = `${url}/revoke`;
+ return this.ajax(url, 'POST', {
+ data: {
+ serial_number: snapshot.id,
+ },
+ });
+ },
});
diff --git a/ui/app/adapters/kmip/role.js b/ui/app/adapters/kmip/role.js
index dc7a9e60f2..1f76a0400c 100644
--- a/ui/app/adapters/kmip/role.js
+++ b/ui/app/adapters/kmip/role.js
@@ -1,4 +1,6 @@
import BaseAdapter from './base';
+import { decamelize } from '@ember/string';
+import { getProperties } from '@ember/object';
export default BaseAdapter.extend({
createRecord(store, type, snapshot) {
@@ -15,19 +17,47 @@ export default BaseAdapter.extend({
return {
id: name,
name,
+ backend: snapshot.record.backend,
+ scope: snapshot.record.scope,
};
});
},
+ deleteRecord(store, type, snapshot) {
+ let name = snapshot.id || snapshot.attr('name');
+ let url = this._url(
+ type.modelName,
+ {
+ backend: snapshot.record.backend,
+ scope: snapshot.record.scope,
+ },
+ name
+ );
+ return this.ajax(url, 'DELETE');
+ },
+
serialize(snapshot) {
// the endpoint here won't allow sending `operation_all` and `operation_none` at the same time or with
// other values, so we manually check for them and send an abbreviated object
let json = snapshot.serialize();
+ let keys = snapshot.record.nonOperationFields.map(decamelize);
+ let nonOperationFields = getProperties(json, keys);
+ for (let field in nonOperationFields) {
+ if (nonOperationFields[field] == null) {
+ delete nonOperationFields[field];
+ }
+ }
if (json.operation_all) {
- return { operation_all: true };
+ return {
+ operation_all: true,
+ ...nonOperationFields,
+ };
}
if (json.operation_none) {
- return { operation_none: true };
+ return {
+ operation_none: true,
+ ...nonOperationFields,
+ };
}
delete json.operation_none;
delete json.operation_all;
diff --git a/ui/app/adapters/kmip/scope.js b/ui/app/adapters/kmip/scope.js
index db3f27374b..f2d6a02e06 100644
--- a/ui/app/adapters/kmip/scope.js
+++ b/ui/app/adapters/kmip/scope.js
@@ -12,4 +12,8 @@ export default BaseAdapter.extend({
}
);
},
+
+ deleteRecord(store, type, snapshot) {
+ return this.ajax(this._url(type.modelName, { backend: snapshot.record.backend }, snapshot.id), 'DELETE');
+ },
});
diff --git a/ui/app/models/kmip/credential.js b/ui/app/models/kmip/credential.js
index bf24c97418..a8f9f03201 100644
--- a/ui/app/models/kmip/credential.js
+++ b/ui/app/models/kmip/credential.js
@@ -2,8 +2,10 @@ import DS from 'ember-data';
import fieldToAttrs from 'vault/utils/field-to-attrs';
import { computed } from '@ember/object';
const { attr } = DS;
+import apiPath from 'vault/utils/api-path';
+import attachCapabilities from 'vault/lib/attach-capabilities';
-export default DS.Model.extend({
+const Model = DS.Model.extend({
backend: attr({ readOnly: true }),
scope: attr({ readOnly: true }),
role: attr({ readOnly: true }),
@@ -28,3 +30,7 @@ export default DS.Model.extend({
return fieldToAttrs(this, groups);
}),
});
+
+export default attachCapabilities(Model, {
+ deletePath: apiPath`${'backend'}/scope/${'scope'}/role/${'role'}/credentials/revoke`,
+});
diff --git a/ui/app/models/kmip/role.js b/ui/app/models/kmip/role.js
index 6190a36755..e68ce378b4 100644
--- a/ui/app/models/kmip/role.js
+++ b/ui/app/models/kmip/role.js
@@ -6,24 +6,40 @@ import apiPath from 'vault/utils/api-path';
import attachCapabilities from 'vault/lib/attach-capabilities';
const { attr } = DS;
-const Model = DS.Model.extend({
+export const COMPUTEDS = {
+ operationFields: computed('newFields', function() {
+ return this.newFields.filter(key => key.startsWith('operation'));
+ }),
+
+ operationFieldsWithoutSpecial: computed('operationFields', function() {
+ return this.operationFields.slice().removeObjects(['operationAll', 'operationNone']);
+ }),
+
+ nonOperationFields: computed('operationFields', function() {
+ let excludeFields = ['role'].concat(this.operationFields);
+ return this.newFields.slice().removeObjects(excludeFields);
+ }),
+};
+
+const Model = DS.Model.extend(COMPUTEDS, {
useOpenAPI: true,
backend: attr({ readOnly: true }),
scope: attr({ readOnly: true }),
+ name: attr({ readOnly: true }),
getHelpUrl(path) {
return `/v1/${path}/scope/example/role/example?help=1`;
},
-
- name: attr({ readOnly: true }),
- fieldGroups: computed(function() {
- let fields = this.newFields.without('role');
- const groups = [{ 'Allowed Operations': fields }];
- return fieldToAttrs(this, groups);
+ fieldGroups: computed('fields', 'nonOperationFields', function() {
+ const groups = [{ default: this.nonOperationFields }, { 'Allowed Operations': this.operationFields }];
+ let ret = fieldToAttrs(this, groups);
+ return ret;
}),
- fields: computed(function() {
- let fields = this.newFields.removeObjects(['role', 'operationAll', 'operationNone']);
- return expandAttributeMeta(this, fields);
+ operationFormFields: computed('operationFieldsWithoutSpecial', function() {
+ return expandAttributeMeta(this, this.operationFieldsWithoutSpecial);
+ }),
+ fields: computed('nonOperationFields', function() {
+ return expandAttributeMeta(this, this.nonOperationFields);
}),
});
diff --git a/ui/app/models/kmip/scope.js b/ui/app/models/kmip/scope.js
index 107eb8d5c7..24171565bf 100644
--- a/ui/app/models/kmip/scope.js
+++ b/ui/app/models/kmip/scope.js
@@ -1,12 +1,19 @@
import { computed } from '@ember/object';
import DS from 'ember-data';
+import apiPath from 'vault/utils/api-path';
+import attachCapabilities from 'vault/lib/attach-capabilities';
const { attr } = DS;
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
-export default DS.Model.extend({
+let Model = DS.Model.extend({
name: attr('string'),
+ backend: attr({ readOnly: true }),
attrs: computed(function() {
return expandAttributeMeta(this, ['name']);
}),
});
+
+export default attachCapabilities(Model, {
+ updatePath: apiPath`${'backend'}/scope/${'id'}`,
+});
diff --git a/ui/app/serializers/application.js b/ui/app/serializers/application.js
index eabd726277..517bb37748 100644
--- a/ui/app/serializers/application.js
+++ b/ui/app/serializers/application.js
@@ -16,11 +16,10 @@ export default DS.JSONSerializer.extend({
}
let pk = this.get('primaryKey') || 'id';
let model = { [pk]: key };
- // if we've added a in the adapter, we want
+ // if we've added _requestQuery in the adapter, we want
// attach it to the individual models
if (payload._requestQuery) {
model = { ...model, ...payload._requestQuery };
- delete payload._requestQuery;
}
return model;
});
@@ -44,6 +43,7 @@ export default DS.JSONSerializer.extend({
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
const responseJSON = this.normalizeItems(payload, requestType);
+ delete payload._requestQuery;
if (id && !responseJSON.id) {
responseJSON.id = id;
}
diff --git a/ui/app/utils/api-path.js b/ui/app/utils/api-path.js
index a5f845ec0f..145205e543 100644
--- a/ui/app/utils/api-path.js
+++ b/ui/app/utils/api-path.js
@@ -13,8 +13,8 @@ export default function apiPath(strings, ...keys) {
let dict = data || {};
let result = [strings[0]];
assert(
- `Expected ${keys.length} keys in apiPath context, only recieved ${Object.keys(data).length}`,
- keys.length === Object.keys(data).length
+ `Expected ${keys.length} keys in apiPath context, only recieved ${Object.keys(data).join(',')}`,
+ Object.keys(data).length >= keys.length
);
keys.forEach((key, i) => {
result.push(dict[key], strings[i + 1]);
diff --git a/ui/lib/core/addon/components/list-item.js b/ui/lib/core/addon/components/list-item.js
index f89ccd1319..d54cd1858c 100644
--- a/ui/lib/core/addon/components/list-item.js
+++ b/ui/lib/core/addon/components/list-item.js
@@ -19,7 +19,7 @@ export default Component.extend({
successCallback();
} catch (e) {
let errString = e.errors.join(' ');
- flash.danger(failureMessage + errString);
+ flash.danger(failureMessage + ' ' + errString);
model.rollbackAttributes();
}
}),
diff --git a/ui/lib/core/addon/components/menu-loader.js b/ui/lib/core/addon/components/menu-loader.js
new file mode 100644
index 0000000000..0710d53693
--- /dev/null
+++ b/ui/lib/core/addon/components/menu-loader.js
@@ -0,0 +1,22 @@
+/**
+ * @module MenuLoader
+ * MenuLoader components are used to show a loading state when fetching data is triggered by opening a
+ * popup menu.
+ *
+ * @example
+ * ```js
+ *
+ * ```
+ *
+ * @param loadingParam {Boolean} - If the value of this param is true, the loading state will be rendered,
+ * else the component will yield.
+ */
+import Component from '@ember/component';
+import layout from '../templates/components/menu-loader';
+
+export default Component.extend({
+ tagName: 'li',
+ classNames: 'action',
+ layout,
+ loadingParam: null,
+});
diff --git a/ui/lib/core/addon/components/model-wrap.js b/ui/lib/core/addon/components/model-wrap.js
new file mode 100644
index 0000000000..1cc1cdb798
--- /dev/null
+++ b/ui/lib/core/addon/components/model-wrap.js
@@ -0,0 +1,37 @@
+/**
+ * @module ModelWrap
+ * ModelWrap components provide a way to call methods on models directly from templates. This is done by yielding callMethod task to the wrapped component.
+ *
+ * @example
+ * ```js
+ *
+
+ * ```
+ *
+ * @yields callMethod {Function}
+ *
+ */
+import { inject as service } from '@ember/service';
+import Component from '@ember/component';
+import { task } from 'ember-concurrency';
+import layout from '../templates/components/model-wrap';
+
+export default Component.extend({
+ layout,
+ flashMessages: service(),
+ tagName: '',
+
+ callMethod: task(function*(method, model, successMessage, failureMessage, successCallback = () => {}) {
+ let flash = this.get('flashMessages');
+ try {
+ yield model[method]();
+ flash.success(successMessage);
+ successCallback();
+ } catch (e) {
+ let errString = e.errors.join(' ');
+ flash.danger(failureMessage + ' ' + errString);
+ model.rollbackAttributes();
+ }
+ }),
+});
diff --git a/ui/lib/core/addon/mixins/list-controller.js b/ui/lib/core/addon/mixins/list-controller.js
index 28047330f9..8b5cda81e6 100644
--- a/ui/lib/core/addon/mixins/list-controller.js
+++ b/ui/lib/core/addon/mixins/list-controller.js
@@ -48,5 +48,9 @@ export default Mixin.create({
setFilterFocus(bool) {
this.set('filterFocused', bool);
},
+ refresh() {
+ // bubble to the list-route
+ this.send('reload');
+ },
},
});
diff --git a/ui/lib/core/addon/templates/components/menu-loader.hbs b/ui/lib/core/addon/templates/components/menu-loader.hbs
new file mode 100644
index 0000000000..82c2c31721
--- /dev/null
+++ b/ui/lib/core/addon/templates/components/menu-loader.hbs
@@ -0,0 +1,7 @@
+{{#if @loadingParam}}
+
+{{else}}
+ {{yield}}
+{{/if}}
\ No newline at end of file
diff --git a/ui/lib/core/addon/templates/components/model-wrap.hbs b/ui/lib/core/addon/templates/components/model-wrap.hbs
new file mode 100644
index 0000000000..fe0ab81093
--- /dev/null
+++ b/ui/lib/core/addon/templates/components/model-wrap.hbs
@@ -0,0 +1 @@
+{{yield (hash callMethod=callMethod)}}
diff --git a/ui/lib/core/app/components/menu-loader.js b/ui/lib/core/app/components/menu-loader.js
new file mode 100644
index 0000000000..c357979e25
--- /dev/null
+++ b/ui/lib/core/app/components/menu-loader.js
@@ -0,0 +1 @@
+export { default } from 'core/components/menu-loader';
diff --git a/ui/lib/core/app/components/model-wrap.js b/ui/lib/core/app/components/model-wrap.js
new file mode 100644
index 0000000000..6207ade266
--- /dev/null
+++ b/ui/lib/core/app/components/model-wrap.js
@@ -0,0 +1 @@
+export { default } from 'core/components/model-wrap';
diff --git a/ui/lib/kmip/addon/components/edit-form-kmip-role.js b/ui/lib/kmip/addon/components/edit-form-kmip-role.js
index 47b529a951..cd322995a2 100644
--- a/ui/lib/kmip/addon/components/edit-form-kmip-role.js
+++ b/ui/lib/kmip/addon/components/edit-form-kmip-role.js
@@ -40,7 +40,7 @@ export default EditForm.extend({
model.set('operationAll', null);
return resolve(model);
}
- model.newFields.without('role').forEach(field => {
+ model.operationFields.concat(['operationAll', 'operationNone']).forEach(field => {
// this will set operationAll or operationNone to true
if (field === display) {
model.set(field, true);
diff --git a/ui/lib/kmip/addon/routes/scope/roles/create.js b/ui/lib/kmip/addon/routes/scope/roles/create.js
index cf12111c18..467dcf1523 100644
--- a/ui/lib/kmip/addon/routes/scope/roles/create.js
+++ b/ui/lib/kmip/addon/routes/scope/roles/create.js
@@ -9,6 +9,7 @@ export default Route.extend({
return this.paramsFor('scope').scope_name;
},
beforeModel() {
+ this.store.unloadAll('kmip/role');
return this.pathHelp.getNewModel('kmip/role', this.secretMountPath.currentPath);
},
model() {
diff --git a/ui/lib/kmip/addon/routes/scopes/create.js b/ui/lib/kmip/addon/routes/scopes/create.js
index 49e9576fe0..5f46aefa8f 100644
--- a/ui/lib/kmip/addon/routes/scopes/create.js
+++ b/ui/lib/kmip/addon/routes/scopes/create.js
@@ -4,6 +4,9 @@ import { inject as service } from '@ember/service';
export default Route.extend({
store: service(),
secretMountPath: service(),
+ beforeModel() {
+ this.store.unloadAll('kmip/scope');
+ },
model() {
let model = this.store.createRecord('kmip/scope', {
backend: this.secretMountPath.currentPath,
diff --git a/ui/lib/kmip/addon/templates/components/edit-form-kmip-role.hbs b/ui/lib/kmip/addon/templates/components/edit-form-kmip-role.hbs
index 69fa889ba6..e7c6673408 100644
--- a/ui/lib/kmip/addon/templates/components/edit-form-kmip-role.hbs
+++ b/ui/lib/kmip/addon/templates/components/edit-form-kmip-role.hbs
@@ -34,7 +34,7 @@
{{/each}}
{{#if (eq this.display "choose")}}
- {{#each this.model.fields as |attr|}}
+ {{#each this.model.operationFormFields as |attr|}}
{{/if}}
+
+ {{#each this.model.fields as |attr|}}
+
+ {{/each}}
diff --git a/ui/lib/kmip/addon/templates/credentials/index.hbs b/ui/lib/kmip/addon/templates/credentials/index.hbs
index fa6d278aac..a3af166831 100644
--- a/ui/lib/kmip/addon/templates/credentials/index.hbs
+++ b/ui/lib/kmip/addon/templates/credentials/index.hbs
@@ -59,6 +59,29 @@
View credentials
{{/link-to}}
+ {{#if list.item.deletePath.canDelete}}
+
+
+ Revoke credentials
+
+
+ {{/if}}
{{else}}
diff --git a/ui/lib/kmip/addon/templates/credentials/show.hbs b/ui/lib/kmip/addon/templates/credentials/show.hbs
index a723cbb1ce..46a313d83b 100644
--- a/ui/lib/kmip/addon/templates/credentials/show.hbs
+++ b/ui/lib/kmip/addon/templates/credentials/show.hbs
@@ -15,6 +15,29 @@
>
Back to role
+ {{#if model.deletePath.canDelete}}
+
+
+ Revoke credentials
+
+
+ {{/if}}
-
- Edit role
-
+ {{#if model.updatePath.canUpdate}}
+
+
+ Delete role
+
+
+ {{/if}}
+ {{#if model.updatePath.canUpdate}}
+
+ Edit role
+
+ {{/if}}
diff --git a/ui/lib/kmip/addon/templates/scope/roles.hbs b/ui/lib/kmip/addon/templates/scope/roles.hbs
index 70f2cdab0e..ed83f2bb4d 100644
--- a/ui/lib/kmip/addon/templates/scope/roles.hbs
+++ b/ui/lib/kmip/addon/templates/scope/roles.hbs
@@ -74,6 +74,35 @@
View role
{{/link-to}}
+ {{#if list.item.updatePath.canUpdate}}
+
+ {{#link-to "role.edit" this.scope list.item.id class="is-block"}}
+ Edit role
+ {{/link-to}}
+
+ {{/if}}
+ {{#if list.item.updatePath.canDelete}}
+
+
+ Delete role
+
+
+ {{/if}}
{{else}}
diff --git a/ui/lib/kmip/addon/templates/scopes/index.hbs b/ui/lib/kmip/addon/templates/scopes/index.hbs
index 03736fb5e8..21f49f768e 100644
--- a/ui/lib/kmip/addon/templates/scopes/index.hbs
+++ b/ui/lib/kmip/addon/templates/scopes/index.hbs
@@ -59,6 +59,28 @@
View scope
{{/link-to}}
+ {{#if list.item.updatePath.canDelete}}
+
+
+ Delete scope
+
+
+ {{/if}}
{{else}}
diff --git a/ui/tests/integration/components/edit-form-kmip-role-test.js b/ui/tests/integration/components/edit-form-kmip-role-test.js
index 8852d88487..051a2b17e2 100644
--- a/ui/tests/integration/components/edit-form-kmip-role-test.js
+++ b/ui/tests/integration/components/edit-form-kmip-role-test.js
@@ -8,6 +8,7 @@ import { render, settled, click } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
import sinon from 'sinon';
import engineResolverFor from 'ember-engines/test-support/engine-resolver-for';
+import { COMPUTEDS } from 'vault/models/kmip/role';
const resolver = engineResolverFor('kmip');
const flash = Service.extend({
@@ -16,12 +17,29 @@ const flash = Service.extend({
const namespace = Service.extend({});
const createModel = options => {
- let model = EmberObject.extend({
- fields: computed('newFields', function() {
- return this.newFields.map(field => ({ name: field, type: 'boolean' }));
- }),
+ let model = EmberObject.extend(COMPUTEDS, {
/* eslint-disable ember/avoid-leaking-state-in-ember-objects */
- newFields: ['operationAll', 'operationNone', 'operationGet', 'operationCreate', 'operationDestroy'],
+ newFields: [
+ 'role',
+ 'operationActivate',
+ 'operationAddAttribute',
+ 'operationAll',
+ 'operationCreate',
+ 'operationDestroy',
+ 'operationDiscoverVersion',
+ 'operationGet',
+ 'operationGetAttributes',
+ 'operationLocate',
+ 'operationNone',
+ 'operationRekey',
+ 'operationRevoke',
+ 'tlsClientKeyBits',
+ 'tlsClientKeyType',
+ 'tlsClientTtl',
+ ],
+ fields: computed('operationFields', function() {
+ return this.operationFields.map(field => ({ name: field, type: 'boolean' }));
+ }),
destroyRecord() {
return resolve();
},
diff --git a/ui/tests/unit/adapters/kmip/role-test.js b/ui/tests/unit/adapters/kmip/role-test.js
index d33214665a..8a8a65eb2b 100644
--- a/ui/tests/unit/adapters/kmip/role-test.js
+++ b/ui/tests/unit/adapters/kmip/role-test.js
@@ -6,25 +6,47 @@ module('Unit | Adapter | kmip/role', function(hooks) {
let serializeTests = [
[
- 'operation_all is the only item present after serialization',
+ 'operation_all is the only operation item present after serialization',
+ {
+ serialize() {
+ return { operation_all: true, operation_get: true, operation_create: true, tls_ttl: '10s' };
+ },
+ record: {
+ nonOperationFields: ['tlsTtl'],
+ },
+ },
+ {
+ operation_all: true,
+ tls_ttl: '10s',
+ },
+ ],
+ [
+ 'serialize does not include nonOperationFields values if they are not set',
{
serialize() {
return { operation_all: true, operation_get: true, operation_create: true };
},
+ record: {
+ nonOperationFields: ['tlsTtl'],
+ },
},
{
operation_all: true,
},
],
[
- 'operation_none is the only item present after serialization',
+ 'operation_none is the only operation item present after serialization',
{
serialize() {
- return { operation_none: true, operation_get: true, operation_add_attribute: true };
+ return { operation_none: true, operation_get: true, operation_add_attribute: true, tls_ttl: '10s' };
+ },
+ record: {
+ nonOperationFields: ['tlsTtl'],
},
},
{
operation_none: true,
+ tls_ttl: '10s',
},
],
[
@@ -39,6 +61,9 @@ module('Unit | Adapter | kmip/role', function(hooks) {
operation_destroy: true,
};
},
+ record: {
+ nonOperationFields: ['tlsTtl'],
+ },
},
{
operation_get: true,
@@ -49,7 +74,6 @@ module('Unit | Adapter | kmip/role', function(hooks) {
];
for (let testCase of serializeTests) {
let [name, snapshotStub, expected] = testCase;
-
test(`adapter serialize: ${name}`, function(assert) {
let adapter = this.owner.lookup('adapter:kmip/role');
let result = adapter.serialize(snapshotStub);
diff --git a/ui/tests/unit/utils/api-path-test.js b/ui/tests/unit/utils/api-path-test.js
index c64673111d..169f9e4236 100644
--- a/ui/tests/unit/utils/api-path-test.js
+++ b/ui/tests/unit/utils/api-path-test.js
@@ -18,6 +18,6 @@ module('Unit | Util | api path', function() {
let ret = apiPath`foo/${'one'}/${'two'}`;
assert.throws(() => {
ret({ one: 1 });
- }, /Error: Assertion Failed: Expected 2 keys in apiPath context, only recieved 1/);
+ }, /Error: Assertion Failed: Expected 2 keys in apiPath context, only recieved one/);
});
});