From 74dce07c96d2397325cacdfc7061f60b7eeab514 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Fri, 15 May 2026 14:25:36 -0600 Subject: [PATCH] VAULT-44211 - removed legacy Key Management adapter, model, and serializer files (#14838) (#14839) Co-authored-by: mohit-hashicorp --- ui/app/adapters/keymgmt/key.js | 179 ------------------ ui/app/adapters/keymgmt/provider.js | 70 ------- ui/app/models/keymgmt/key.js | 127 ------------- ui/app/models/keymgmt/provider.js | 171 ----------------- .../vault/cluster/secrets/backend/list.js | 6 + .../cluster/secrets/backend/secret-edit.js | 3 +- ui/app/serializers/keymgmt/key.js | 39 ---- ui/app/serializers/keymgmt/provider.js | 29 --- ui/tests/acceptance/enterprise-kmse-test.js | 11 +- .../components/keymgmt/provider-edit-test.js | 1 - 10 files changed, 12 insertions(+), 624 deletions(-) delete mode 100644 ui/app/adapters/keymgmt/key.js delete mode 100644 ui/app/adapters/keymgmt/provider.js delete mode 100644 ui/app/models/keymgmt/key.js delete mode 100644 ui/app/models/keymgmt/provider.js delete mode 100644 ui/app/serializers/keymgmt/key.js delete mode 100644 ui/app/serializers/keymgmt/provider.js diff --git a/ui/app/adapters/keymgmt/key.js b/ui/app/adapters/keymgmt/key.js deleted file mode 100644 index d1765a158b..0000000000 --- a/ui/app/adapters/keymgmt/key.js +++ /dev/null @@ -1,179 +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'; -import ControlGroupError from '../../lib/control-group-error'; -import { service } from '@ember/service'; - -function pickKeys(obj, picklist) { - const data = {}; - Object.keys(obj).forEach((key) => { - if (picklist.indexOf(key) >= 0) { - data[key] = obj[key]; - } - }); - return data; -} - -export default class KeymgmtKeyAdapter extends ApplicationAdapter { - @service store; - namespace = 'v1'; - - pathForType() { - // backend name prepended in buildURL method - return 'key'; - } - - buildURL(modelName, id, snapshot, requestType, query) { - let url = super.buildURL(...arguments); - if (snapshot) { - url = url.replace('key', `${snapshot.attr('backend')}/key`); - } else if (query) { - url = url.replace('key', `${query.backend}/key`); - } - return url; - } - - url(backend, id, type) { - const url = `${this.buildURL()}/${backend}/key`; - if (id) { - if (type === 'ROTATE') { - return url + '/' + encodePath(id) + '/rotate'; - } else if (type === 'PROVIDERS') { - return url + '/' + encodePath(id) + '/kms'; - } - return url + '/' + encodePath(id); - } - return url; - } - - _updateKey(backend, name, serialized) { - // Only these two attributes are allowed to be updated - const data = pickKeys(serialized, ['deletion_allowed', 'min_enabled_version']); - return this.ajax(this.url(backend, name), 'PUT', { data }); - } - - _createKey(backend, name, serialized) { - // Only type is allowed on create - const data = pickKeys(serialized, ['type']); - return this.ajax(this.url(backend, name), 'POST', { data }); - } - - async createRecord(store, type, snapshot) { - const data = store.serializerFor(type.modelName).serialize(snapshot); - const name = snapshot.attr('name'); - const backend = snapshot.attr('backend'); - // Keys must be created and then updated - await this._createKey(backend, name, data); - if (snapshot.attr('deletionAllowed')) { - try { - await this._updateKey(backend, name, data); - } catch { - throw new Error(`Key ${name} was created, but not all settings were saved`); - } - } - return { - data: { - ...data, - id: name, - backend, - }, - }; - } - - updateRecord(store, type, snapshot) { - const data = store.serializerFor(type.modelName).serialize(snapshot); - const name = snapshot.attr('name'); - const backend = snapshot.attr('backend'); - return this._updateKey(backend, name, data); - } - - distribute(backend, kms, key, data) { - return this.ajax(`${this.buildURL()}/${backend}/kms/${encodePath(kms)}/key/${encodePath(key)}`, 'PUT', { - data: { ...data }, - }); - } - - async getProvider(backend, name) { - try { - const resp = await this.ajax(this.url(backend, name, 'PROVIDERS'), 'GET', { - data: { - list: true, - }, - }); - return resp.data.keys ? resp.data.keys[0] : null; - } catch (e) { - if (e.httpStatus === 404) { - // No results, not distributed yet - return null; - } else if (e.httpStatus === 403) { - return { permissionsError: true }; - } - throw e; - } - } - - getDistribution(backend, kms, key) { - const url = `${this.buildURL()}/${backend}/kms/${kms}/key/${key}`; - return this.ajax(url, 'GET') - .then((res) => { - return { - ...res.data, - purposeArray: res.data.purpose.split(','), - }; - }) - .catch((e) => { - if (e instanceof ControlGroupError) { - throw e; - } - return null; - }); - } - - async queryRecord(store, type, query) { - const { id, backend, recordOnly = false } = query; - const keyData = await this.ajax(this.url(backend, id), 'GET'); - keyData.data.id = id; - keyData.data.backend = backend; - let provider, distribution; - if (!recordOnly) { - provider = await this.getProvider(backend, id); - if (provider && !provider.permissionsError) { - distribution = await this.getDistribution(backend, provider, id); - } - } - return { ...keyData, provider, distribution }; - } - - async query(store, type, query) { - const { backend, provider } = query; - const providerAdapter = store.adapterFor('keymgmt/provider'); - const url = provider ? providerAdapter.buildKeysURL(query) : this.url(backend); - - return this.ajax(url, 'GET', { - data: { - list: true, - }, - }).then((res) => { - res.backend = backend; - return res; - }); - } - - async rotateKey(backend, id) { - const keyModel = this.store.peekRecord('keymgmt/key', id); - const result = await this.ajax(this.url(backend, id, 'ROTATE'), 'PUT'); - await keyModel.reload(); - return result; - } - - removeFromProvider(model) { - const url = `${this.buildURL()}/${model.backend}/kms/${model.provider}/key/${model.name}`; - return this.ajax(url, 'DELETE').then(() => { - model.provider = null; - }); - } -} diff --git a/ui/app/adapters/keymgmt/provider.js b/ui/app/adapters/keymgmt/provider.js deleted file mode 100644 index 4112309c9a..0000000000 --- a/ui/app/adapters/keymgmt/provider.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import ApplicationAdapter from '../application'; -import { all } from 'rsvp'; - -export default class KeymgmtKeyAdapter extends ApplicationAdapter { - namespace = 'v1'; - listPayload = { data: { list: true } }; - - pathForType() { - // backend name prepended in buildURL method - return 'kms'; - } - buildURL(modelName, id, snapshot, requestType, query) { - let url = super.buildURL(...arguments); - if (snapshot) { - url = url.replace('kms', `${snapshot.attr('backend')}/kms`); - } else if (query) { - url = url.replace('kms', `${query.backend}/kms`); - } - return url; - } - buildKeysURL(query) { - const url = this.buildURL('keymgmt/provider', null, null, 'query', query); - return `${url}/${query.provider}/key`; - } - async createRecord(store, { modelName }, snapshot) { - // create uses PUT instead of POST - const data = store.serializerFor(modelName).serialize(snapshot); - const url = this.buildURL(modelName, snapshot.attr('name'), snapshot, 'updateRecord'); - return this.ajax(url, 'PUT', { data }).then(() => data); - } - findRecord(store, type, name) { - return super.findRecord(...arguments).then((resp) => { - resp.data = { ...resp.data, name }; - return resp; - }); - } - async query(store, type, query) { - const { backend } = query; - const url = this.buildURL(type.modelName, null, null, 'query', query); - return this.ajax(url, 'GET', this.listPayload).then(async (resp) => { - // additional data is needed to fullfil the list view requirements - // pull in full record for listed items - const records = await all( - resp.data.keys.map((name) => this.findRecord(store, type, name, this._mockSnapshot(query.backend))) - ); - resp.data.keys = records.map((record) => record.data); - resp.backend = backend; - return resp; - }); - } - async queryRecord(store, type, query) { - return this.findRecord(store, type, query.id, this._mockSnapshot(query.backend)); - } - - // when using find in query or queryRecord overrides snapshot is not available - // ultimately buildURL requires the snapshot to pull the backend name for the dynamic segment - // since we have the backend value from the query generate a mock snapshot - _mockSnapshot(backend) { - return { - attr(prop) { - return prop === 'backend' ? backend : null; - }, - }; - } -} diff --git a/ui/app/models/keymgmt/key.js b/ui/app/models/keymgmt/key.js deleted file mode 100644 index d28968f1ac..0000000000 --- a/ui/app/models/keymgmt/key.js +++ /dev/null @@ -1,127 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import Model, { attr } from '@ember-data/model'; -import { expandAttributeMeta } from 'vault/utils/field-to-attrs'; -import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; -import { KeyManagementUpdateKeyRequestTypeEnum } from '@hashicorp/vault-client-typescript'; - -const KEY_TYPES = Object.values(KeyManagementUpdateKeyRequestTypeEnum); -export default class KeymgmtKeyModel extends Model { - @attr('string', { - label: 'Key name', - subText: 'This is the name of the key that shows in Vault.', - }) - name; - - @attr('string') - backend; - - @attr('string', { - subText: 'The type of cryptographic key that will be created.', - possibleValues: KEY_TYPES, - defaultValue: 'rsa-2048', - }) - type; - - @attr('boolean', { - label: 'Allow deletion', - defaultValue: false, - }) - deletionAllowed; - - @attr('number', { - label: 'Current version', - }) - latestVersion; - - @attr('number', { - defaultValue: 0, - defaultShown: 'All versions enabled', - }) - minEnabledVersion; - - @attr('array') - versions; - - // The following are calculated in serializer - @attr('date') - created; - - @attr('date', { - defaultShown: 'Not yet rotated', - }) - lastRotated; - - // The following are from endpoints other than the main read one - @attr() provider; // string, or object with permissions error - @attr() distribution; - - icon = 'key'; - - get hasVersions() { - return this.versions.length > 1; - } - - get createFields() { - const createFields = ['name', 'type', 'deletionAllowed']; - return expandAttributeMeta(this, createFields); - } - - get updateFields() { - return expandAttributeMeta(this, ['minEnabledVersion', 'deletionAllowed']); - } - get showFields() { - return expandAttributeMeta(this, [ - 'name', - 'created', - 'type', - 'deletionAllowed', - 'latestVersion', - 'minEnabledVersion', - 'lastRotated', - ]); - } - - get keyTypeOptions() { - return expandAttributeMeta(this, ['type'])[0]; - } - - get distFields() { - return [ - { - name: 'name', - type: 'string', - label: 'Distributed name', - subText: 'The name given to the key by the provider.', - }, - { name: 'purpose', type: 'string', label: 'Key Purpose' }, - { name: 'protection', type: 'string', subText: 'Where cryptographic operations are performed.' }, - ]; - } - - @lazyCapabilities(apiPath`${'backend'}/key/${'id'}`, 'backend', 'id') keyPath; - @lazyCapabilities(apiPath`${'backend'}/key`, 'backend') keysPath; - @lazyCapabilities(apiPath`${'backend'}/key/${'id'}/kms`, 'backend', 'id') keyProvidersPath; - - get canCreate() { - return this.keyPath.get('canCreate'); - } - get canDelete() { - return this.keyPath.get('canDelete'); - } - get canEdit() { - return this.keyPath.get('canUpdate'); - } - get canRead() { - return this.keyPath.get('canRead'); - } - get canList() { - return this.keysPath.get('canList'); - } - get canListProviders() { - return this.keyProvidersPath.get('canList'); - } -} diff --git a/ui/app/models/keymgmt/provider.js b/ui/app/models/keymgmt/provider.js deleted file mode 100644 index c9f034615b..0000000000 --- a/ui/app/models/keymgmt/provider.js +++ /dev/null @@ -1,171 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import Model, { attr } from '@ember-data/model'; -import { tracked } from '@glimmer/tracking'; -import { expandAttributeMeta } from 'vault/utils/field-to-attrs'; -import { withModelValidations } from 'vault/decorators/model-validations'; -import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; -import { service } from '@ember/service'; - -const CRED_PROPS = { - azurekeyvault: ['client_id', 'client_secret', 'tenant_id'], - awskms: ['access_key', 'secret_key', 'session_token', 'endpoint'], - gcpckms: ['service_account_file'], -}; - -const OPTIONAL_CRED_PROPS = ['session_token', 'endpoint']; - -// since we have dynamic credential attributes based on provider we need a dynamic presence validator -// add validators for all cred props and return true for value if not associated with selected provider -const credValidators = Object.keys(CRED_PROPS).reduce((obj, providerKey) => { - CRED_PROPS[providerKey].forEach((prop) => { - if (!OPTIONAL_CRED_PROPS.includes(prop)) { - obj[`credentials.${prop}`] = [ - { - message: `${prop} is required`, - validator(model) { - return model.credentialProps.includes(prop) ? model.credentials[prop] : true; - }, - }, - ]; - } - }); - return obj; -}, {}); - -const validations = { - name: [{ type: 'presence', message: 'Provider name is required' }], - keyCollection: [{ type: 'presence', message: 'Key Vault instance name' }], - ...credValidators, -}; - -@withModelValidations(validations) -export default class KeymgmtProviderModel extends Model { - @service pagination; - @attr('string') backend; - @attr('string', { - label: 'Provider name', - subText: 'This is the name of the provider that will be displayed in Vault. This cannot be edited later.', - }) - name; - - @attr('string', { - label: 'Type', - subText: 'Choose the provider type.', - possibleValues: ['azurekeyvault', 'awskms', 'gcpckms'], - noDefault: true, - }) - provider; - - @attr('string', { - label: 'Key Vault instance name', - subText: 'The name of a Key Vault instance must be supplied. This cannot be edited later.', - }) - keyCollection; - - idPrefix = 'provider/'; - type = 'provider'; - - @tracked keys = []; - @tracked credentials = null; // never returned from API -- set only during create/edit - - get icon() { - return { - azurekeyvault: 'azure-color', - awskms: 'aws-color', - gcpckms: 'gcp-color', - }[this.provider]; - } - get typeName() { - return { - azurekeyvault: 'Azure Key Vault', - awskms: 'AWS Key Management Service', - gcpckms: 'Google Cloud Key Management Service', - }[this.provider]; - } - get showFields() { - const attrs = expandAttributeMeta(this, ['name', 'keyCollection']); - attrs.splice(1, 0, { hasBlock: true, label: 'Type', value: this.typeName, icon: this.icon }); - const l = this.keys.length; - const value = l - ? `${l} ${l > 1 ? 'keys' : 'key'}` - : this.canListKeys - ? 'None' - : 'You do not have permission to list keys'; - attrs.push({ hasBlock: true, isLink: l, label: 'Keys', value }); - return attrs; - } - get credentialProps() { - if (!this.provider) return []; - return CRED_PROPS[this.provider]; - } - get credentialFields() { - const [creds, fields] = this.credentialProps.reduce( - ([creds, fields], prop) => { - creds[prop] = null; - const field = { name: `credentials.${prop}`, type: 'string', options: { label: prop } }; - if (prop === 'service_account_file') { - field.options.subText = 'The path to a Google service account key file, not the file itself.'; - } - fields.push(field); - return [creds, fields]; - }, - [{}, []] - ); - this.credentials = creds; - return fields; - } - get createFields() { - return expandAttributeMeta(this, ['provider', 'name', 'keyCollection']); - } - - async fetchKeys(page) { - if (this.canListKeys === false) { - this.keys = []; - } else { - // try unless capabilities returns false - try { - this.keys = await this.pagination.lazyPaginatedQuery('keymgmt/key', { - backend: this.backend, - provider: this.name, - responsePath: 'data.keys', - page: Number(page) || 1, - }); - } catch (error) { - this.keys = []; - if (error.httpStatus !== 404) { - throw error; - } - } - } - } - - @lazyCapabilities(apiPath`${'backend'}/kms/${'id'}`, 'backend', 'id') providerPath; - @lazyCapabilities(apiPath`${'backend'}/kms`, 'backend') providersPath; - @lazyCapabilities(apiPath`${'backend'}/kms/${'id'}/key`, 'backend', 'id') providerKeysPath; - - get canCreate() { - return this.providerPath.get('canCreate'); - } - get canDelete() { - return this.providerPath.get('canDelete'); - } - get canEdit() { - return this.providerPath.get('canUpdate'); - } - get canRead() { - return this.providerPath.get('canRead'); - } - get canList() { - return this.providersPath.get('canList'); - } - get canListKeys() { - return this.providerKeysPath.get('canList'); - } - get canCreateKeys() { - return this.providerKeysPath.get('canCreate'); - } -} diff --git a/ui/app/routes/vault/cluster/secrets/backend/list.js b/ui/app/routes/vault/cluster/secrets/backend/list.js index d2286fded1..1ad8985f06 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/list.js +++ b/ui/app/routes/vault/cluster/secrets/backend/list.js @@ -17,6 +17,7 @@ import { getEffectiveEngineType } from 'vault/utils/external-plugin-helpers'; import { getKeymgmtProviderIcon } from 'vault/utils/keymgmt-provider-utils'; import { getModelTypeForEngine } from 'vault/utils/model-helpers/secret-engine-helpers'; import { normalizePath } from 'vault/utils/path-encoding-helpers'; +import { resolve } from 'rsvp'; import { SecretsApiKeyManagementListKeysListEnum, SecretsApiKeyManagementListKmsProvidersListEnum, @@ -96,6 +97,11 @@ export default Route.extend({ return this.router.transitionTo('vault.cluster.secrets.backend.kv.list', backend); } const modelType = this.getModelType(effectiveType, tab); + // Keymgmt routes use API-backed forms instead of Ember Data models, so skip model hydration. + if (effectiveType === 'keymgmt') { + return resolve(); + } + return this.pathHelp.hydrateModel(modelType, backend).then(() => { this.store.unloadAll('capabilities'); }); diff --git a/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js b/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js index 33b0f6721c..4f7ceb1cc2 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js +++ b/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js @@ -111,7 +111,8 @@ export default Route.extend({ buildModel(secret, queryParams) { const backend = getEnginePathParam(this); const modelType = this.modelType(backend, secret, { queryParams }); - if (modelType === 'secret') { + // Keymgmt resources are loaded through API-backed forms, so Ember Data hydration is unnecessary. + if (modelType === 'secret' || modelType.startsWith('keymgmt/')) { return resolve(); } return this.pathHelp.hydrateModel(modelType, backend); diff --git a/ui/app/serializers/keymgmt/key.js b/ui/app/serializers/keymgmt/key.js deleted file mode 100644 index 5a08ee21c4..0000000000 --- a/ui/app/serializers/keymgmt/key.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import ApplicationSerializer from '../application'; - -export default class KeymgmtKeySerializer extends ApplicationSerializer { - normalizeItems(payload) { - const normalized = super.normalizeItems(payload); - // Transform versions from object with number keys to array with key ids - if (normalized.versions) { - let lastRotated; - let created; - const versions = []; - Object.keys(normalized.versions).forEach((key, i, arr) => { - versions.push({ - id: parseInt(key, 10), - ...normalized.versions[key], - }); - if (i === 0) { - created = normalized.versions[key].creation_time; - } else if (arr.length - 1 === i) { - // Set lastRotated to the last key - lastRotated = normalized.versions[key].creation_time; - } - }); - normalized.versions = versions; - return { ...normalized, last_rotated: lastRotated, created }; - } else if (Array.isArray(normalized)) { - return normalized.map((key) => ({ - id: key.id, - name: key.id, - backend: payload.backend, - })); - } - return normalized; - } -} diff --git a/ui/app/serializers/keymgmt/provider.js b/ui/app/serializers/keymgmt/provider.js deleted file mode 100644 index 6fb40f8fe9..0000000000 --- a/ui/app/serializers/keymgmt/provider.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2025 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import ApplicationSerializer from '../application'; - -export default class KeymgmtProviderSerializer extends ApplicationSerializer { - primaryKey = 'name'; - - normalizeItems(payload) { - const normalized = super.normalizeItems(payload); - if (Array.isArray(normalized)) { - normalized.forEach((provider) => { - provider.id = provider.name; - provider.backend = payload.backend; - }); - } - return normalized; - } - - serialize(snapshot) { - const json = super.serialize(...arguments); - return { - ...json, - credentials: snapshot.record.credentials, - }; - } -} diff --git a/ui/tests/acceptance/enterprise-kmse-test.js b/ui/tests/acceptance/enterprise-kmse-test.js index c07c97f075..b7bb2e9d96 100644 --- a/ui/tests/acceptance/enterprise-kmse-test.js +++ b/ui/tests/acceptance/enterprise-kmse-test.js @@ -40,19 +40,17 @@ module('Acceptance | Enterprise | keymgmt', function (hooks) { await runCmd([`delete sys/mounts/${engine.type}`]); }); - // TODO: Fix this test - it requires proper provider capability mocking - // This test is for provider functionality which still uses Ember Data - // and is unrelated to the key migration to API service pattern - test.skip('it should add new key and distribute to provider', async function (assert) { + test('it should add new key and distribute to provider', async function (assert) { const path = `keymgmt-${Date.now()}`; this.server.post(`/${path}/key/test-key`, () => ({})); - this.server.put(`/${path}/kms/test-keyvault/key/test-key`, () => ({})); + this.server.post(`/${path}/kms/test-keyvault/key/test-key`, () => ({})); + this.server.get(`/${path}/kms/test-keyvault/key`, () => ({ data: { keys: ['test-key'] } })); await mountSecrets.enable('keymgmt', path); await click(SES.createSecretLink); await fillIn('[data-test-input="provider"]', 'azurekeyvault'); await fillIn('[data-test-input="name"]', 'test-keyvault'); - await fillIn('[data-test-input="keyCollection"]', 'test-keycollection'); + await fillIn('[data-test-input="key_collection"]', 'test-keycollection'); await fillIn('[data-test-input="credentials.client_id"]', '123'); await fillIn('[data-test-input="credentials.client_secret"]', '456'); await fillIn('[data-test-input="credentials.tenant_id"]', '789'); @@ -65,7 +63,6 @@ module('Acceptance | Enterprise | keymgmt', function (hooks) { await click('[data-test-operation="encrypt"]'); await fillIn('[data-test-protection="hsm"]', 'hsm'); - this.server.get(`/${path}/kms/test-keyvault/key`, () => ({ data: { keys: ['test-key'] } })); await click('[data-test-secret-save]'); await click('[data-test-kms-provider-tab="keys"] a'); assert.dom('[data-test-secret-link="test-key"]').exists('Key is listed under keys tab of provider'); diff --git a/ui/tests/integration/components/keymgmt/provider-edit-test.js b/ui/tests/integration/components/keymgmt/provider-edit-test.js index 00bfb054fe..067a26211e 100644 --- a/ui/tests/integration/components/keymgmt/provider-edit-test.js +++ b/ui/tests/integration/components/keymgmt/provider-edit-test.js @@ -61,7 +61,6 @@ module('Integration | Component | keymgmt/provider-edit', function (hooks) { this.capabilities = { canDelete: true, canListKeys: true, canEdit: false, canCreateKeys: false }; - this.server.post('/sys/capabilities-self', () => ({})); this.server.get('/keymgmt/kms/foo-bar/key', () => { return { data: {