Merge remote-tracking branch 'remotes/from/ce/main'
Some checks failed
build / setup (push) Has been cancelled
build / hcp-setup (push) Has been cancelled
CI / setup (push) Has been cancelled
Run linters / Setup (push) Has been cancelled
Run linters / Semgrep (push) Has been cancelled
Check Copywrite Headers / copywrite (push) Has been cancelled
Security Scan / scan (push) Has been cancelled
build / Check ce/* Pull Requests (push) Has been cancelled
build / ui (push) Has been cancelled
build / artifacts-ce (push) Has been cancelled
build / artifacts-ent (push) Has been cancelled
build / hcp-image (push) Has been cancelled
build / test (push) Has been cancelled
build / test-hcp-image (push) Has been cancelled
build / completed-successfully (push) Has been cancelled
CI / Run Autopilot upgrade tool (push) Has been cancelled
CI / Run Go tests (push) Has been cancelled
CI / Run Go tests tagged with testonly (push) Has been cancelled
CI / Run Go tests with data race detection (push) Has been cancelled
CI / Run Go tests with FIPS configuration (push) Has been cancelled
CI / Test UI (push) Has been cancelled
CI / tests-completed (push) Has been cancelled
Run linters / Deprecated functions (push) Has been cancelled
Run linters / Code checks (push) Has been cancelled
Run linters / Protobuf generate delta (push) Has been cancelled
Run linters / Format (push) Has been cancelled

This commit is contained in:
hc-github-team-secure-vault-core 2026-05-15 21:21:42 +00:00
commit eb5e22a029
10 changed files with 12 additions and 624 deletions

View file

@ -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;
});
}
}

View file

@ -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;
},
};
}
}

View file

@ -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');
}
}

View file

@ -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');
}
}

View file

@ -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');
});

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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,
};
}
}

View file

@ -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');

View file

@ -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: {