[UI] Ember Data Migration - KMIP Cleanup (#11838) (#11865)

* updates kmip scope roles route to ts

* updates kmip scope roles route to use api service and adds page component

* converts kmip role route to ts

* fixes a11y error in kmip header-credentials component

* updates kmip role route to use api service and adds page component

* removes kmip operation-field-display component that was moved into role page component

* converts kmip role create route to ts

* moves kmip role form component to component directory root

* converts kmip role form component to ts

* adds operation-groups helper and refactors kmip role page to use it

* adds operation-label helper and updates kmip role page to use it

* converts kmip edit role route to ts

* updates kmip role test to use operation-groups helper

* adds kmip role form

* updates kmip role edit and create routes to use api service and form class

* updates kmip role form component to work with form class

* updates kmip acceptance tests

* converts kmip credentials index route to ts

* updates kmip credentials route to use api service

* adds kmip credentials page component

* converts kmip credentials show route to ts

* updates kmip credentials show route to use api service and adds page component

* fixes flash message issue on kmip role form submit success

* converts kmip credentials generate route to ts

* reverts kmip credentials show page component in favor of details-credentials component which is also used in generate route

* fixes kmip details-credentials tests

* update kmip credentials generate route to use api service and updates page component

* removes store and pagination services from kmip engine

* converts kmip breadcrumb component to ts

* converts kmip header scope component to ts

* removes kmip Ember Data models and adapters

* removes store reference from kmip acceptance tests

* fixes issues routing back to secrets engine via breadcrumb in kmip roles and credentials routes

* removes kmip role adapter test

* updates open api helpers tests

Co-authored-by: Jordan Reimer <zofskeez@gmail.com>
This commit is contained in:
Vault Automation 2026-01-21 12:20:19 -05:00 committed by GitHub
parent 1f883d8d59
commit 60eb60c24f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 40 additions and 556 deletions

View File

@ -1,69 +0,0 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationAdapter from '../application';
import { encodePath } from 'vault/utils/path-encoding-helpers';
export default ApplicationAdapter.extend({
namespace: 'v1',
pathForType(type) {
return type.replace('kmip/', '');
},
_url(modelType, meta = {}, id) {
const { backend, scope, role } = meta;
const type = this.pathForType(modelType);
let base;
switch (type) {
case 'scope':
base = `${encodePath(backend)}/scope`;
break;
case 'role':
base = `${encodePath(backend)}/scope/${encodePath(scope)}/role`;
break;
case 'credential':
base = `${encodePath(backend)}/scope/${encodePath(scope)}/role/${encodePath(role)}/credential`;
break;
}
if (id && type === 'credential') {
return `/v1/${base}/lookup?serial_number=${encodePath(id)}`;
}
if (id) {
return `/v1/${base}/${encodePath(id)}`;
}
return `/v1/${base}`;
},
urlForQuery(query, modelType) {
const base = this._url(modelType, query);
return base + '?list=true';
},
query(store, type, query) {
return this.ajax(this.urlForQuery(query, type.modelName), 'GET').then((resp) => {
// remove pagination query items here
const { ...modelAttrs } = query;
resp._requestQuery = modelAttrs;
return resp;
});
},
queryRecord(store, type, query) {
const id = query.id;
delete query.id;
return this.ajax(this._url(type.modelName, query, id), 'GET').then((resp) => {
resp.id = id;
resp = { ...resp, ...query };
return resp;
});
},
buildURL(modelName, id, snapshot, requestType, query) {
if (requestType === 'createRecord') {
return this._super(...arguments);
}
return this._super(`${modelName}`, id, snapshot, requestType, query);
},
});

View File

@ -1,13 +0,0 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import BaseAdapter from './base';
export default BaseAdapter.extend({
urlForFindRecord(id, modelName, snapshot) {
const name = this.pathForType(modelName);
return this.buildURL(id, name, snapshot);
},
});

View File

@ -1,33 +0,0 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import BaseAdapter from './base';
export default BaseAdapter.extend({
_url(id, modelName, snapshot) {
const name = this.pathForType(modelName);
// id here will be the mount path,
// modelName will be config so we want to transpose the first two call args
return this.buildURL(id, name, snapshot);
},
urlForFindRecord() {
return this._url(...arguments);
},
urlForCreateRecord(modelName, snapshot) {
const id = snapshot.record.mutableId;
return this._url(id, modelName, snapshot);
},
urlForUpdateRecord() {
return this._url(...arguments);
},
createRecord(store, type, snapshot) {
return this._super(...arguments).then(() => {
// saving returns a 204, return object with id to please ember-data...
const id = snapshot.record.mutableId;
return { id };
});
},
});

View File

@ -1,35 +0,0 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import BaseAdapter from './base';
export default BaseAdapter.extend({
createRecord(store, type, snapshot) {
let url = this._url(type.modelName, {
backend: snapshot.record.backend,
scope: snapshot.record.scope,
role: snapshot.record.role,
});
url = `${url}/generate`;
return this.ajax(url, 'POST', { data: snapshot.serialize() }).then((model) => {
model.data.id = model.data.serial_number;
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,
},
});
},
});

View File

@ -1,78 +0,0 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import BaseAdapter from './base';
import { decamelize } from '@ember/string';
import { getProperties } from '@ember/object';
import { nonOperationFields } from 'vault/utils/model-helpers/kmip-role-fields';
export default BaseAdapter.extend({
createRecord(store, type, snapshot) {
const name = snapshot.id || snapshot.record.role;
const url = this._url(
type.modelName,
{
backend: snapshot.record.backend,
scope: snapshot.record.scope,
},
name
);
const data = this.serialize(snapshot);
return this.ajax(url, 'POST', { data }).then(() => {
return {
id: name,
role: name,
backend: snapshot.record.backend,
scope: snapshot.record.scope,
};
});
},
deleteRecord(store, type, snapshot) {
// records must always have IDs
const name = snapshot.id;
const url = this._url(
type.modelName,
{
backend: snapshot.record.backend,
scope: snapshot.record.scope,
},
name
);
return this.ajax(url, 'DELETE');
},
updateRecord() {
return this.createRecord(...arguments);
},
serialize(snapshot) {
// the endpoint here won't allow sending `operation_all` and `operation_none` at the same time or with
// other operation_ values, so we manually check for them and send an abbreviated object
const json = snapshot.serialize();
const keys = nonOperationFields(snapshot.record.editableFields).map(decamelize);
const nonOp = getProperties(json, keys);
for (const field in nonOp) {
if (nonOp[field] == null) {
delete nonOp[field];
}
}
if (json.operation_all) {
return {
operation_all: true,
...nonOp,
};
}
if (json.operation_none) {
return {
operation_none: true,
...nonOp,
};
}
delete json.operation_none;
delete json.operation_all;
return json;
},
});

View File

@ -1,26 +0,0 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import BaseAdapter from './base';
export default BaseAdapter.extend({
createRecord(store, type, snapshot) {
const name = snapshot.attr('name');
return this.ajax(this._url(type.modelName, { backend: snapshot.record.backend }, name), 'POST').then(
() => {
return {
id: name,
name,
};
}
);
},
deleteRecord(store, type, snapshot) {
let url = this._url(type.modelName, { backend: snapshot.record.backend }, snapshot.id);
url = `${url}?force=true`;
return this.ajax(url, 'DELETE');
},
});

View File

@ -62,8 +62,6 @@ export default class App extends Application {
'namespace',
'path-help',
{ 'app-router': 'router' },
'store',
'pagination',
'version',
'secret-mount-path',
],

View File

@ -1,13 +0,0 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import Model, { belongsTo, attr } from '@ember-data/model';
export default Model.extend({
config: belongsTo('kmip/config', { async: false, inverse: 'ca' }),
caPem: attr('string', {
label: 'CA PEM',
}),
});

View File

@ -1,20 +0,0 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import Model, { belongsTo } from '@ember-data/model';
import { computed } from '@ember/object';
import { combineFieldGroups } from 'vault/utils/openapi-to-attrs';
import fieldToAttrs from 'vault/utils/field-to-attrs';
export default Model.extend({
ca: belongsTo('kmip/ca', { async: false, inverse: 'config' }),
fieldGroups: computed('newFields', function () {
let groups = [{ default: ['listenAddrs', 'connectionTimeout'] }];
groups = combineFieldGroups(groups, this.newFields, []);
return fieldToAttrs(this, groups);
}),
});

View File

@ -1,42 +0,0 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import Model, { attr } from '@ember-data/model';
import fieldToAttrs from 'vault/utils/field-to-attrs';
import { computed } from '@ember/object';
import apiPath from 'vault/utils/api-path';
import lazyCapabilities from 'vault/macros/lazy-capabilities';
export default Model.extend({
backend: attr({ readOnly: true }),
scope: attr({ readOnly: true }),
role: attr({ readOnly: true }),
certificate: attr('string', { readOnly: true }),
caChain: attr({ readOnly: true }),
privateKey: attr('string', {
readOnly: true,
sensitive: true,
}),
format: attr('string', {
possibleValues: ['pem', 'der', 'pem_bundle'],
defaultValue: 'pem',
label: 'Certificate format',
}),
fieldGroups: computed(function () {
const groups = [
{
default: ['format'],
},
];
return fieldToAttrs(this, groups);
}),
deletePath: lazyCapabilities(
apiPath`${'backend'}/scope/${'scope'}/role/${'role'}/credentials/revoke`,
'backend',
'scope',
'role'
),
});

View File

@ -1,71 +0,0 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import Model, { attr } from '@ember-data/model';
import apiPath from 'vault/utils/api-path';
import lazyCapabilities from 'vault/macros/lazy-capabilities';
import { withExpandedAttributes } from 'vault/decorators/model-expanded-attributes';
import {
operationFields,
operationFieldsWithoutSpecial,
tlsFields,
} from 'vault/utils/model-helpers/kmip-role-fields';
import { removeManyFromArray } from 'vault/helpers/remove-from-array';
@withExpandedAttributes()
export default class KmipRoleModel extends Model {
@attr({ readOnly: true }) backend;
@attr({ readOnly: true }) scope;
get editableFields() {
return Object.keys(this.allByKey).filter((k) => !['backend', 'scope', 'role'].includes(k));
}
get fieldGroups() {
const tls = tlsFields();
const groups = [{ TLS: tls }];
// op fields are shown in OperationFieldDisplay
const opFields = operationFields(this.editableFields);
// not op fields, tls fields, or role/backend/scope
const defaultFields = this.editableFields.filter((f) => ![...opFields, ...tls].includes(f));
if (defaultFields.length) {
groups.unshift({ default: defaultFields });
}
return this._expandGroups(groups);
}
get operationFormFields() {
const objects = [
'operationCreate',
'operationActivate',
'operationGet',
'operationLocate',
'operationRekey',
'operationRevoke',
'operationDestroy',
];
const attributes = ['operationAddAttribute', 'operationGetAttributes'];
const server = ['operationDiscoverVersions'];
const others = removeManyFromArray(operationFieldsWithoutSpecial(this.editableFields), [
...objects,
...attributes,
...server,
]);
const groups = [
{ 'Managed Cryptographic Objects': objects },
{ 'Object Attributes': attributes },
{ Server: server },
];
if (others.length) {
groups.push({
Other: others,
});
}
return this._expandGroups(groups);
}
@lazyCapabilities(apiPath`${'backend'}/scope/${'scope'}/role/${'id'}`, 'backend', 'scope', 'id') updatePath;
}

View File

@ -1,20 +0,0 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import Model, { attr } from '@ember-data/model';
import { computed } from '@ember/object';
import apiPath from 'vault/utils/api-path';
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
import lazyCapabilities from 'vault/macros/lazy-capabilities';
export default Model.extend({
name: attr('string'),
backend: attr({ readOnly: true }),
attrs: computed(function () {
return expandAttributeMeta(this, ['name']);
}),
updatePath: lazyCapabilities(apiPath`${'backend'}/scope/${'id'}`, 'backend', 'id'),
});

View File

@ -208,16 +208,6 @@ export function filterPathsByItemType(pathInfo: PathInfo, itemType: string): Pat
* This object maps model names to the openAPI path that hydrates the model, given the backend path.
*/
const OPENAPI_POWERED_MODELS = {
'kmip/config': (backend: string) => `/v1/${backend}/config?help=1`,
'kmip/role': (backend: string) => `/v1/${backend}/scope/example/role/example?help=1`,
'pki/certificate/generate': (backend: string) => `/v1/${backend}/issue/example?help=1`,
'pki/certificate/sign': (backend: string) => `/v1/${backend}/sign/example?help=1`,
'pki/config/acme': (backend: string) => `/v1/${backend}/config/acme?help=1`,
'pki/config/cluster': (backend: string) => `/v1/${backend}/config/cluster?help=1`,
'pki/config/urls': (backend: string) => `/v1/${backend}/config/urls?help=1`,
'pki/role': (backend: string) => `/v1/${backend}/roles/example?help=1`,
'pki/sign-intermediate': (backend: string) => `/v1/${backend}/issuer/example/sign-intermediate?help=1`,
'pki/tidy': (backend: string) => `/v1/${backend}/config/auto-tidy?help=1`,
'role-ssh': (backend: string) => `/v1/${backend}/roles/example?help=1`,
};

View File

@ -1,12 +0,0 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@ember/component';
import { service } from '@ember/service';
export default Component.extend({
tagName: '',
secretMountPath: service(),
});

View File

@ -0,0 +1,13 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { service } from '@ember/service';
import type SecretMountPath from 'vault/services/secret-mount-path';
export default class HeaderScopeComponent extends Component {
@service declare readonly secretMountPath: SecretMountPath;
}

View File

@ -1,18 +0,0 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@ember/component';
import { service } from '@ember/service';
import { or } from '@ember/object/computed';
export default Component.extend({
tagName: '',
secretMountPath: service(),
shouldShowPath: or('showPath', 'scope', 'role'),
showPath: false,
path: null,
scope: null,
role: null,
});

View File

@ -0,0 +1,25 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { service } from '@ember/service';
import type SecretMountPath from 'vault/services/secret-mount-path';
interface Args {
currentRoute: string;
showPath?: boolean;
scope?: string;
role?: string;
}
export default class KmipBreadcrumbComponent extends Component<Args> {
@service declare secretMountPath: SecretMountPath;
get shouldShowPath() {
const { showPath, scope, role } = this.args;
return showPath || scope || role;
}
}

View File

@ -81,6 +81,7 @@
@currentPage={{@credentials.meta.currentPage}}
@currentPageSize={{@credentials.meta.pageSize}}
@route="credentials.index"
@models={{array @scope @role}}
@showSizeSelector={{false}}
@totalItems={{@credentials.meta.filteredTotal}}
@queryFunction={{this.paginationQueryParams}}

View File

@ -97,6 +97,7 @@
@currentPage={{@roles.meta.currentPage}}
@currentPageSize={{@roles.meta.pageSize}}
@route="scope.roles"
@models={{array @scope}}
@showSizeSelector={{false}}
@totalItems={{@roles.meta.filteredTotal}}
@queryFunction={{this.paginationQueryParams}}

View File

@ -22,8 +22,6 @@ export default class KmipEngine extends Engine {
'namespace',
'path-help',
'app-router',
'store',
'pagination',
'version',
'secret-mount-path',
],

View File

@ -386,7 +386,6 @@ module('Acceptance | Enterprise | KMIP secrets', function (hooks) {
// the kmip/role model relies on openApi so testing the form via an acceptance test
module('kmip role edit form', function (hooks) {
hooks.beforeEach(async function () {
this.store = this.owner.lookup('service:store');
this.scope = 'my-scope';
this.name = 'my-role';

View File

@ -1,90 +0,0 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Adapter | kmip/role', function (hooks) {
setupTest(hooks);
// these are only some of the actual editable fields
const editableFields = ['tlsTtl', 'operationAll', 'operationNone', 'operationGet', 'operationCreate'];
const serializeTests = [
[
'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: {
editableFields,
},
},
{
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: {
editableFields,
},
},
{
operation_all: true,
},
],
[
'operation_none is the only operation item present after serialization',
{
serialize() {
return { operation_none: true, operation_get: true, operation_add_attribute: true, tls_ttl: '10s' };
},
record: {
editableFields,
},
},
{
operation_none: true,
tls_ttl: '10s',
},
],
[
'operation_all and operation_none are removed if not truthy',
{
serialize() {
return {
operation_all: false,
operation_none: false,
operation_get: true,
operation_add_attribute: true,
operation_destroy: true,
};
},
record: {
editableFields,
},
},
{
operation_get: true,
operation_add_attribute: true,
operation_destroy: true,
},
],
];
for (const testCase of serializeTests) {
const [name, snapshotStub, expected] = testCase;
test(`adapter serialize: ${name}`, function (assert) {
const adapter = this.owner.lookup('adapter:kmip/role');
const result = adapter.serialize(snapshotStub);
assert.deepEqual(result, expected, 'output matches expected');
});
}
});

View File

@ -46,7 +46,6 @@ module('Unit | Utility | OpenAPI helper utils', function (hooks) {
test(`getHelpUrlForModel`, function (assert) {
[
{ modelType: 'kmip/config', result: '/v1/foobar/config?help=1' },
{ modelType: 'does-not-exist', result: null },
{ modelType: 4, result: null },
{ modelType: '', result: null },