x-pack/platform/plugins/shared/fleet/server/services/secrets.ts (961 lines of code) (raw):
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server';
import { get, keyBy } from 'lodash';
import { set } from '@kbn/safer-lodash-set';
import type {
FleetServerHost,
SOSecretPath,
KafkaOutput,
NewFleetServerHost,
NewRemoteElasticsearchOutput,
Output,
DownloadSource,
DownloadSourceBase,
} from '../../common/types';
import { packageHasNoPolicyTemplates } from '../../common/services/policy_template';
import type {
NewOutput,
NewPackagePolicy,
RegistryStream,
UpdatePackagePolicy,
} from '../../common';
import { SO_SEARCH_LIMIT } from '../../common';
import {
doesPackageHaveIntegrations,
getNormalizedDataStreams,
getNormalizedInputs,
} from '../../common/services';
import type {
PackageInfo,
PackagePolicy,
RegistryVarsEntry,
Secret,
VarSecretReference,
PolicySecretReference,
SecretPath,
DeletedSecretResponse,
DeletedSecretReference,
} from '../types';
import { FleetError } from '../errors';
import {
OUTPUT_SECRETS_MINIMUM_FLEET_SERVER_VERSION,
SECRETS_ENDPOINT_PATH,
SECRETS_MINIMUM_FLEET_SERVER_VERSION,
} from '../constants';
import { retryTransientEsErrors } from './epm/elasticsearch/retry';
import { auditLoggingService } from './audit_logging';
import { appContextService } from './app_context';
import { packagePolicyService } from './package_policy';
import { settingsService } from '.';
import { checkFleetServerVersionsForSecretsStorage } from './fleet_server';
export async function createSecrets(opts: {
esClient: ElasticsearchClient;
values: Array<string | string[]>;
}): Promise<Array<Secret | Secret[]>> {
const { esClient, values } = opts;
const logger = appContextService.getLogger();
const sendESRequest = (value: string): Promise<Secret> => {
return retryTransientEsErrors(
() =>
esClient.transport.request({
method: 'POST',
path: SECRETS_ENDPOINT_PATH,
body: { value },
}),
{ logger }
);
};
const secretsResponse: Array<Secret | Secret[]> = await Promise.all(
values.map(async (value) => {
try {
if (Array.isArray(value)) {
return await Promise.all(value.map(sendESRequest));
} else {
return await sendESRequest(value);
}
} catch (err) {
const msg = `Error creating secrets: ${err}`;
logger.error(msg);
throw new FleetError(msg);
}
})
);
const writeLog = (item: Secret) => {
auditLoggingService.writeCustomAuditLog({
message: `secret created: ${item.id}`,
event: {
action: 'secret_create',
category: ['database'],
type: ['access'],
outcome: 'success',
},
});
};
secretsResponse.forEach((item) => {
if (Array.isArray(item)) {
item.forEach(writeLog);
} else {
writeLog(item);
}
});
return secretsResponse;
}
export async function deleteSecretsIfNotReferenced(opts: {
esClient: ElasticsearchClient;
soClient: SavedObjectsClientContract;
ids: string[];
}): Promise<void> {
const { esClient, soClient, ids } = opts;
const logger = appContextService.getLogger();
const packagePoliciesUsingSecrets = await findPackagePoliciesUsingSecrets({
soClient,
ids,
});
if (packagePoliciesUsingSecrets.length) {
packagePoliciesUsingSecrets.forEach(({ id, policyIds }) => {
logger.debug(
`Not deleting secret with id ${id} is still in use by package policies: ${policyIds.join(
', '
)}`
);
});
}
const secretsToDelete = ids.filter((id) => {
return !packagePoliciesUsingSecrets.some((packagePolicy) => packagePolicy.id === id);
});
if (!secretsToDelete.length) {
return;
}
try {
await deleteSecrets({
esClient,
ids: secretsToDelete,
});
} catch (e) {
logger.warn(`Error cleaning up secrets ${ids.join(', ')}: ${e}`);
}
}
export async function findPackagePoliciesUsingSecrets(opts: {
soClient: SavedObjectsClientContract;
ids: string[];
}): Promise<Array<{ id: string; policyIds: string[] }>> {
const { soClient, ids } = opts;
const packagePolicies = await packagePolicyService.list(soClient, {
kuery: `ingest-package-policies.secret_references.id: (${ids.join(' or ')})`,
perPage: SO_SEARCH_LIMIT,
page: 1,
});
if (!packagePolicies.total) {
return [];
}
// create a map of secret_references.id to package policy id
const packagePoliciesBySecretId = packagePolicies.items.reduce((acc, packagePolicy) => {
packagePolicy?.secret_references?.forEach((secretReference) => {
if (Array.isArray(secretReference)) {
secretReference.forEach(({ id }) => {
if (!acc[id]) {
acc[id] = [];
}
acc[id].push(packagePolicy.id);
});
} else {
if (!acc[secretReference.id]) {
acc[secretReference.id] = [];
}
acc[secretReference.id].push(packagePolicy.id);
}
});
return acc;
}, {} as Record<string, string[]>);
const res = [];
for (const id of ids) {
if (packagePoliciesBySecretId[id]) {
res.push({
id,
policyIds: packagePoliciesBySecretId[id],
});
}
}
return res;
}
export async function deleteSecrets(opts: {
esClient: ElasticsearchClient;
ids: string[];
}): Promise<void> {
const { esClient, ids } = opts;
const logger = appContextService.getLogger();
const deletedRes: DeletedSecretReference[] = await Promise.all(
ids.map(async (id) => {
try {
const getDeleteRes: DeletedSecretResponse = await retryTransientEsErrors(
() =>
esClient.transport.request({
method: 'DELETE',
path: `${SECRETS_ENDPOINT_PATH}/${id}`,
}),
{ logger }
);
return { ...getDeleteRes, id };
} catch (err) {
const msg = `Error deleting secrets: ${err}`;
logger.error(msg);
throw new FleetError(msg);
}
})
);
deletedRes.forEach((item) => {
if (item.deleted === true) {
auditLoggingService.writeCustomAuditLog({
message: `secret deleted: ${item.id}`,
event: {
action: 'secret_delete',
category: ['database'],
type: ['access'],
outcome: 'success',
},
});
}
});
}
export async function extractAndWriteSecrets(opts: {
packagePolicy: NewPackagePolicy;
packageInfo: PackageInfo;
esClient: ElasticsearchClient;
}): Promise<{ packagePolicy: NewPackagePolicy; secretReferences: PolicySecretReference[] }> {
const { packagePolicy, packageInfo, esClient } = opts;
const secretPaths = getPolicySecretPaths(packagePolicy, packageInfo);
if (!secretPaths.length) {
return { packagePolicy, secretReferences: [] };
}
const secretsToCreate = secretPaths.filter((secretPath) => !!secretPath.value.value);
const secrets = await createSecrets({
esClient,
values: secretsToCreate.map((secretPath) => secretPath.value.value),
});
const policyWithSecretRefs = getPolicyWithSecretReferences(
secretsToCreate,
secrets,
packagePolicy
);
return {
packagePolicy: policyWithSecretRefs,
secretReferences: secrets.reduce((acc: PolicySecretReference[], secret) => {
if (Array.isArray(secret)) {
return [...acc, ...secret.map(({ id }) => ({ id }))];
}
return [...acc, { id: secret.id }];
}, []),
};
}
export async function extractAndUpdateSecrets(opts: {
oldPackagePolicy: PackagePolicy;
packagePolicyUpdate: UpdatePackagePolicy;
packageInfo: PackageInfo;
esClient: ElasticsearchClient;
}): Promise<{
packagePolicyUpdate: UpdatePackagePolicy;
secretReferences: PolicySecretReference[];
secretsToDelete: PolicySecretReference[];
}> {
const { oldPackagePolicy, packagePolicyUpdate, packageInfo, esClient } = opts;
const oldSecretPaths = getPolicySecretPaths(oldPackagePolicy, packageInfo);
const updatedSecretPaths = getPolicySecretPaths(packagePolicyUpdate, packageInfo);
if (!oldSecretPaths.length && !updatedSecretPaths.length) {
return { packagePolicyUpdate, secretReferences: [], secretsToDelete: [] };
}
const { toCreate, toDelete, noChange } = diffSecretPaths(oldSecretPaths, updatedSecretPaths);
const secretsToCreate = toCreate.filter((secretPath) => !!secretPath.value.value);
const createdSecrets = await createSecrets({
esClient,
values: secretsToCreate.map((secretPath) => secretPath.value.value),
});
const policyWithSecretRefs = getPolicyWithSecretReferences(
secretsToCreate,
createdSecrets,
packagePolicyUpdate
);
const secretReferences = [
...noChange.reduce((acc: PolicySecretReference[], secretPath) => {
if (secretPath.value.value.ids) {
return [...acc, ...secretPath.value.value.ids.map((id: string) => ({ id }))];
}
return [...acc, { id: secretPath.value.value.id }];
}, []),
...createdSecrets.reduce((acc: PolicySecretReference[], secret) => {
if (Array.isArray(secret)) {
return [...acc, ...secret.map(({ id }) => ({ id }))];
}
return [...acc, { id: secret.id }];
}, []),
];
const secretsToDelete: PolicySecretReference[] = [];
toDelete.forEach((secretPath) => {
// check if the previous secret is actually a secret refrerence
// it may be that secrets were not enabled at the time of creation
// in which case they are just stored as plain text
if (secretPath.value.value?.isSecretRef) {
if (secretPath.value.value.ids) {
secretPath.value.value.ids.forEach((id: string) => {
secretsToDelete.push({ id });
});
} else {
secretsToDelete.push({ id: secretPath.value.value.id });
}
}
});
return {
packagePolicyUpdate: policyWithSecretRefs,
secretReferences,
secretsToDelete,
};
}
function isSecretVar(varDef: RegistryVarsEntry) {
return varDef.secret === true;
}
function containsSecretVar(vars?: RegistryVarsEntry[]) {
return vars?.some(isSecretVar);
}
// this is how secrets are stored on the package policy
function toVarSecretRef(secret: Secret | Secret[]): VarSecretReference {
if (Array.isArray(secret)) {
return { ids: secret.map(({ id }) => id), isSecretRef: true };
}
return { id: secret.id, isSecretRef: true };
}
// this is how IDs are inserted into compiled templates
export function toCompiledSecretRef(id: string) {
return `$co.elastic.secret{${id}}`;
}
export function diffSecretPaths(
oldPaths: SecretPath[],
newPaths: SecretPath[]
): { toCreate: SecretPath[]; toDelete: SecretPath[]; noChange: SecretPath[] } {
const toCreate: SecretPath[] = [];
const toDelete: SecretPath[] = [];
const noChange: SecretPath[] = [];
const newPathsByPath = keyBy(newPaths, (x) => x.path.join('.'));
for (const oldPath of oldPaths) {
if (!newPathsByPath[oldPath.path.join('.')]) {
toDelete.push(oldPath);
}
const newPath = newPathsByPath[oldPath.path.join('.')];
if (newPath && newPath.value.value) {
const newValue = newPath.value?.value;
if (!newValue?.isSecretRef) {
toCreate.push(newPath);
toDelete.push(oldPath);
} else {
noChange.push(newPath);
}
delete newPathsByPath[oldPath.path.join('.')];
}
}
const remainingNewPaths = Object.values(newPathsByPath);
return { toCreate: [...toCreate, ...remainingNewPaths], toDelete, noChange };
}
// Given a package policy and a package,
// returns an array of lodash style paths to all secrets and their current values
export function getPolicySecretPaths(
packagePolicy: PackagePolicy | NewPackagePolicy | UpdatePackagePolicy,
packageInfo: PackageInfo
): SecretPath[] {
const packageLevelVarPaths = _getPackageLevelSecretPaths(packagePolicy, packageInfo);
if (!packageInfo?.policy_templates?.length || packageHasNoPolicyTemplates(packageInfo)) {
return packageLevelVarPaths;
}
const inputSecretPaths = _getInputSecretPaths(packagePolicy, packageInfo);
return [...packageLevelVarPaths, ...inputSecretPaths];
}
export async function isSecretStorageEnabled(
esClient: ElasticsearchClient,
soClient: SavedObjectsClientContract
): Promise<boolean> {
const logger = appContextService.getLogger();
// if serverless then secrets will always be supported
const isFleetServerStandalone =
appContextService.getConfig()?.internal?.fleetServerStandalone ?? false;
if (isFleetServerStandalone) {
logger.trace('Secrets storage is enabled as fleet server is standalone');
return true;
}
// now check the flag in settings to see if the fleet server requirement has already been met
// once the requirement has been met, secrets are always on
const settings = await settingsService.getSettingsOrUndefined(soClient);
if (settings && settings.secret_storage_requirements_met) {
logger.debug('Secrets storage requirements already met, turned on in settings');
return true;
}
const areAllFleetServersOnProperVersion = await checkFleetServerVersionsForSecretsStorage(
esClient,
soClient,
SECRETS_MINIMUM_FLEET_SERVER_VERSION
);
// otherwise check if we have the minimum fleet server version and enable secrets if so
if (areAllFleetServersOnProperVersion) {
logger.debug('Enabling secrets storage as minimum fleet server version has been met');
try {
await settingsService.saveSettings(soClient, {
secret_storage_requirements_met: true,
});
} catch (err) {
// we can suppress this error as it will be retried on the next function call
logger.warn(`Failed to save settings after enabling secrets storage: ${err.message}`);
}
return true;
}
logger.info('Secrets storage is disabled as minimum fleet server version has not been met');
return false;
}
export async function isOutputSecretStorageEnabled(
esClient: ElasticsearchClient,
soClient: SavedObjectsClientContract
): Promise<boolean> {
const logger = appContextService.getLogger();
// if serverless then output secrets will always be supported
const isFleetServerStandalone =
appContextService.getConfig()?.internal?.fleetServerStandalone ?? false;
if (isFleetServerStandalone) {
logger.trace('Output secrets storage is enabled as fleet server is standalone');
return true;
}
// now check the flag in settings to see if the fleet server requirement has already been met
// once the requirement has been met, output secrets are always on
const settings = await settingsService.getSettingsOrUndefined(soClient);
if (settings && settings.output_secret_storage_requirements_met) {
logger.debug('Output secrets storage requirements already met, turned on in settings');
return true;
}
// otherwise check if we have the minimum fleet server version and enable secrets if so
if (
await checkFleetServerVersionsForSecretsStorage(
esClient,
soClient,
OUTPUT_SECRETS_MINIMUM_FLEET_SERVER_VERSION
)
) {
logger.debug('Enabling output secrets storage as minimum fleet server version has been met');
try {
await settingsService.saveSettings(soClient, {
output_secret_storage_requirements_met: true,
});
} catch (err) {
// we can suppress this error as it will be retried on the next function call
logger.warn(`Failed to save settings after enabling output secrets storage: ${err.message}`);
}
return true;
}
logger.info('Secrets storage is disabled as minimum fleet server version has not been met');
return false;
}
function _getPackageLevelSecretPaths(
packagePolicy: NewPackagePolicy,
packageInfo: PackageInfo
): SecretPath[] {
const packageSecretVars = packageInfo.vars?.filter(isSecretVar) || [];
const packageSecretVarsByName = keyBy(packageSecretVars, 'name');
const packageVars = Object.entries(packagePolicy.vars || {});
return packageVars.reduce((vars, [name, configEntry], i) => {
if (packageSecretVarsByName[name]) {
vars.push({
value: configEntry,
path: ['vars', name],
});
}
return vars;
}, [] as SecretPath[]);
}
function _getInputSecretPaths(
packagePolicy: NewPackagePolicy,
packageInfo: PackageInfo
): SecretPath[] {
if (!packageInfo?.policy_templates?.length) return [];
const inputSecretVarDefsByPolicyTemplateAndType =
_getInputSecretVarDefsByPolicyTemplateAndType(packageInfo);
const streamSecretVarDefsByDatasetAndInput =
_getStreamSecretVarDefsByDatasetAndInput(packageInfo);
return packagePolicy.inputs.flatMap((input, inputIndex) => {
if (!input.vars && !input.streams) {
return [];
}
const currentInputVarPaths: SecretPath[] = [];
const inputKey = doesPackageHaveIntegrations(packageInfo)
? `${input.policy_template}-${input.type}`
: input.type;
const inputVars = Object.entries(input.vars || {});
if (inputVars.length) {
inputVars.forEach(([name, configEntry]) => {
if (inputSecretVarDefsByPolicyTemplateAndType[inputKey]?.[name]) {
currentInputVarPaths.push({
path: ['inputs', inputIndex.toString(), 'vars', name],
value: configEntry,
});
}
});
}
if (input.streams.length) {
input.streams.forEach((stream, streamIndex) => {
const streamVarDefs =
streamSecretVarDefsByDatasetAndInput[`${stream.data_stream.dataset}-${input.type}`];
if (streamVarDefs && Object.keys(streamVarDefs).length) {
Object.entries(stream.vars || {}).forEach(([name, configEntry]) => {
if (streamVarDefs[name]) {
currentInputVarPaths.push({
path: [
'inputs',
inputIndex.toString(),
'streams',
streamIndex.toString(),
'vars',
name,
],
value: configEntry,
});
}
});
}
});
}
return currentInputVarPaths;
});
}
// a map of all secret vars for each dataset and input combo
function _getStreamSecretVarDefsByDatasetAndInput(packageInfo: PackageInfo) {
const dataStreams = getNormalizedDataStreams(packageInfo);
const streamsByDatasetAndInput = dataStreams.reduce<Record<string, RegistryStream>>(
(streams, dataStream) => {
dataStream.streams?.forEach((stream) => {
streams[`${dataStream.dataset}-${stream.input}`] = stream;
});
return streams;
},
{}
);
return Object.entries(streamsByDatasetAndInput).reduce<
Record<string, Record<string, RegistryVarsEntry>>
>((varDefs, [path, stream]) => {
if (stream.vars && containsSecretVar(stream.vars)) {
const secretVars = stream.vars.filter(isSecretVar);
varDefs[path] = keyBy(secretVars, 'name');
}
return varDefs;
}, {});
}
// a map of all secret vars for each policyTemplate and input type combo
function _getInputSecretVarDefsByPolicyTemplateAndType(packageInfo: PackageInfo) {
if (!packageInfo?.policy_templates?.length) return {};
const hasIntegrations = doesPackageHaveIntegrations(packageInfo);
return packageInfo.policy_templates.reduce<Record<string, Record<string, RegistryVarsEntry>>>(
(varDefs, policyTemplate) => {
const inputs = getNormalizedInputs(policyTemplate);
inputs.forEach((input) => {
const varDefKey = hasIntegrations ? `${policyTemplate.name}-${input.type}` : input.type;
const secretVars = input?.vars?.filter(isSecretVar);
if (secretVars?.length) {
varDefs[varDefKey] = keyBy(secretVars, 'name');
}
});
return varDefs;
},
{}
);
}
/**
* Given an array of secret paths, existing secrets, and a package policy, generates a
* new package policy object that includes resolved secret reference values at each
* provided path.
*/
function getPolicyWithSecretReferences(
secretPaths: SecretPath[],
secrets: Array<Secret | Secret[]>,
packagePolicy: NewPackagePolicy
) {
const result = JSON.parse(JSON.stringify(packagePolicy));
secretPaths.forEach((secretPath, secretPathIndex) => {
secretPath.path.reduce((acc, val, secretPathComponentIndex) => {
if (!acc[val]) {
acc[val] = {};
}
const isLast = secretPathComponentIndex === secretPath.path.length - 1;
if (isLast) {
acc[val].value = toVarSecretRef(secrets[secretPathIndex]);
}
return acc[val];
}, result);
});
return result;
}
/**
* Common functions for SO objects
* Currently used for outputs and fleet server hosts
*/
/**
* diffSOSecretPaths
* Makes the diff betwwen old and new secrets paths
*/
export function diffSOSecretPaths(
oldPaths: SOSecretPath[],
newPaths: SOSecretPath[]
): { toCreate: SOSecretPath[]; toDelete: SOSecretPath[]; noChange: SOSecretPath[] } {
const toCreate: SOSecretPath[] = [];
const toDelete: SOSecretPath[] = [];
const noChange: SOSecretPath[] = [];
const newPathsByPath = keyBy(newPaths, 'path');
for (const oldPath of oldPaths) {
if (!newPathsByPath[oldPath.path]) {
toDelete.push(oldPath);
}
const newPath = newPathsByPath[oldPath.path];
if (newPath && newPath.value) {
const newValue = newPath.value;
if (typeof newValue === 'string') {
toCreate.push(newPath);
toDelete.push(oldPath);
} else {
noChange.push(newPath);
}
}
delete newPathsByPath[oldPath.path];
}
const remainingNewPaths = Object.values(newPathsByPath);
return { toCreate: [...toCreate, ...remainingNewPaths], toDelete, noChange };
}
/**
* deleteSOSecrets
* Given an array of secret paths, deletes the corresponding secrets
*/
export async function deleteSOSecrets(
esClient: ElasticsearchClient,
secretPaths: SOSecretPath[]
): Promise<void> {
if (secretPaths.length === 0) {
return Promise.resolve();
}
const secretIds = secretPaths.map(({ value }) => (value as { id: string }).id);
try {
return deleteSecrets({ esClient, ids: secretIds });
} catch (err) {
appContextService.getLogger().warn(`Error deleting secrets: ${err}`);
}
}
/**
* extractAndWriteSOSecrets
* Takes a generic object T and its secret paths
* Creates new secrets and returns the references
*/
async function extractAndWriteSOSecrets<T>(opts: {
soObject: T;
esClient: ElasticsearchClient;
secretPaths: SOSecretPath[];
secretHashes?: Record<string, any>;
}): Promise<{ soObjectWithSecrets: T; secretReferences: PolicySecretReference[] }> {
const { soObject, esClient, secretPaths, secretHashes = {} } = opts;
if (secretPaths.length === 0) {
return { soObjectWithSecrets: soObject, secretReferences: [] };
}
const secrets = await createSecrets({
esClient,
values: secretPaths.map(({ value }) => value as string | string[]),
});
const objectWithSecretRefs = JSON.parse(JSON.stringify(soObject));
secretPaths.forEach((secretPath, i) => {
const pathWithoutPrefix = secretPath.path.replace('secrets.', '');
const maybeHash = get(secretHashes, pathWithoutPrefix);
const currentSecret = secrets[i];
set(objectWithSecretRefs, secretPath.path, {
...(Array.isArray(currentSecret)
? { ids: currentSecret.map(({ id }) => id) }
: { id: currentSecret.id }),
...(typeof maybeHash === 'string' && { hash: maybeHash }),
});
});
return {
soObjectWithSecrets: objectWithSecretRefs,
secretReferences: secrets.reduce((acc: PolicySecretReference[], secret) => {
if (Array.isArray(secret)) {
return [...acc, ...secret.map(({ id }) => ({ id }))];
}
return [...acc, { id: secret.id }];
}, []),
};
}
/**
* extractAndUpdateSOSecrets
* Takes a generic object T to update and its old and new secret paths
* Updates secrets and returns the references
*/
async function extractAndUpdateSOSecrets<T>(opts: {
updatedSoObject: Partial<T>;
oldSecretPaths: SOSecretPath[];
updatedSecretPaths: SOSecretPath[];
esClient: ElasticsearchClient;
secretHashes?: Record<string, any>;
}): Promise<{
updatedSoObject: Partial<T>;
secretReferences: PolicySecretReference[];
secretsToDelete: PolicySecretReference[];
}> {
const { updatedSoObject, oldSecretPaths, updatedSecretPaths, esClient, secretHashes } = opts;
if (!oldSecretPaths.length && !updatedSecretPaths.length) {
return { updatedSoObject, secretReferences: [], secretsToDelete: [] };
}
const { toCreate, toDelete, noChange } = diffSOSecretPaths(oldSecretPaths, updatedSecretPaths);
const createdSecrets = await createSecrets({
esClient,
values: toCreate.map((secretPath) => secretPath.value as string),
});
const soObjectWithSecretRefs = JSON.parse(JSON.stringify(updatedSoObject));
toCreate.forEach((secretPath, i) => {
const pathWithoutPrefix = secretPath.path.replace('secrets.', '');
const maybeHash = get(secretHashes, pathWithoutPrefix);
const currentSecret = createdSecrets[i];
set(soObjectWithSecretRefs, secretPath.path, {
...(Array.isArray(currentSecret)
? { ids: currentSecret.map(({ id }) => id) }
: { id: currentSecret.id }),
...(typeof maybeHash === 'string' && { hash: maybeHash }),
});
});
const secretReferences = [
...noChange.reduce((acc: PolicySecretReference[], secretPath) => {
const currentValue = secretPath.value as { id: string } | { ids: string[] };
if ('ids' in currentValue) {
return [...acc, ...currentValue.ids.map((id: string) => ({ id }))];
} else {
return [...acc, { id: currentValue.id }];
}
}, []),
...createdSecrets.reduce((acc: PolicySecretReference[], secret) => {
if (Array.isArray(secret)) {
return [...acc, ...secret.map(({ id }) => ({ id }))];
}
return [...acc, { id: secret.id }];
}, []),
];
return {
updatedSoObject: soObjectWithSecretRefs,
secretReferences,
secretsToDelete: toDelete.map((secretPath) => ({
id: (secretPath.value as { id: string }).id,
})),
};
}
// Outputs functions
export async function extractAndWriteOutputSecrets(opts: {
output: NewOutput;
esClient: ElasticsearchClient;
secretHashes?: Record<string, any>;
}): Promise<{ output: NewOutput; secretReferences: PolicySecretReference[] }> {
const { output, esClient, secretHashes = {} } = opts;
const secretPaths = getOutputSecretPaths(output.type, output).filter(
(path) => typeof path.value === 'string'
);
const secretRes = await extractAndWriteSOSecrets<NewOutput>({
soObject: output,
secretPaths,
esClient,
secretHashes,
});
return { output: secretRes.soObjectWithSecrets, secretReferences: secretRes.secretReferences };
}
export async function extractAndUpdateOutputSecrets(opts: {
oldOutput: Output;
outputUpdate: Partial<Output>;
esClient: ElasticsearchClient;
secretHashes?: Record<string, any>;
}): Promise<{
outputUpdate: Partial<Output>;
secretReferences: PolicySecretReference[];
secretsToDelete: PolicySecretReference[];
}> {
const { oldOutput, outputUpdate, esClient, secretHashes } = opts;
const outputType = outputUpdate.type || oldOutput.type;
const oldSecretPaths = getOutputSecretPaths(oldOutput.type, oldOutput);
const updatedSecretPaths = getOutputSecretPaths(outputType, outputUpdate);
const secretRes = await extractAndUpdateSOSecrets<Output>({
updatedSoObject: outputUpdate,
oldSecretPaths,
updatedSecretPaths,
esClient,
secretHashes: outputUpdate.is_preconfigured ? secretHashes : undefined,
});
return {
outputUpdate: secretRes.updatedSoObject,
secretReferences: secretRes.secretReferences,
secretsToDelete: secretRes.secretsToDelete,
};
}
function getOutputSecretPaths(
outputType: NewOutput['type'],
output: NewOutput | Partial<Output>
): SOSecretPath[] {
const outputSecretPaths: SOSecretPath[] = [];
if (outputType === 'kafka') {
const kafkaOutput = output as KafkaOutput;
if (kafkaOutput?.secrets?.password) {
outputSecretPaths.push({
path: 'secrets.password',
value: kafkaOutput.secrets.password,
});
}
}
if (outputType === 'remote_elasticsearch') {
const remoteESOutput = output as NewRemoteElasticsearchOutput;
if (remoteESOutput.secrets?.service_token) {
outputSecretPaths.push({
path: 'secrets.service_token',
value: remoteESOutput.secrets.service_token,
});
}
}
// common to all outputs
if (output?.secrets?.ssl?.key) {
outputSecretPaths.push({
path: 'secrets.ssl.key',
value: output.secrets.ssl.key,
});
}
return outputSecretPaths;
}
export async function deleteOutputSecrets(opts: {
output: Output;
esClient: ElasticsearchClient;
}): Promise<void> {
const { output, esClient } = opts;
const outputType = output.type;
const outputSecretPaths = getOutputSecretPaths(outputType, output);
await deleteSOSecrets(esClient, outputSecretPaths);
}
export function getOutputSecretReferences(output: Output): PolicySecretReference[] {
const outputSecretPaths: PolicySecretReference[] = [];
if (typeof output.secrets?.ssl?.key === 'object') {
outputSecretPaths.push({
id: output.secrets.ssl.key.id,
});
}
if (output.type === 'kafka' && typeof output?.secrets?.password === 'object') {
outputSecretPaths.push({
id: output.secrets.password.id,
});
}
if (output.type === 'remote_elasticsearch') {
if (typeof output?.secrets?.service_token === 'object') {
outputSecretPaths.push({
id: output.secrets.service_token.id,
});
}
}
return outputSecretPaths;
}
// Fleet server hosts functions
function getFleetServerHostsSecretPaths(
fleetServerHost: NewFleetServerHost | Partial<FleetServerHost>
): SOSecretPath[] {
const secretPaths: SOSecretPath[] = [];
if (fleetServerHost?.secrets?.ssl?.key) {
secretPaths.push({
path: 'secrets.ssl.key',
value: fleetServerHost.secrets.ssl.key,
});
}
if (fleetServerHost?.secrets?.ssl?.es_key) {
secretPaths.push({
path: 'secrets.ssl.es_key',
value: fleetServerHost.secrets.ssl.es_key,
});
}
return secretPaths;
}
export async function extractAndWriteFleetServerHostsSecrets(opts: {
fleetServerHost: NewFleetServerHost;
esClient: ElasticsearchClient;
secretHashes?: Record<string, any>;
}): Promise<{ fleetServerHost: NewFleetServerHost; secretReferences: PolicySecretReference[] }> {
const { fleetServerHost, esClient, secretHashes = {} } = opts;
const secretPaths = getFleetServerHostsSecretPaths(fleetServerHost);
const secretRes = await extractAndWriteSOSecrets<NewFleetServerHost>({
soObject: fleetServerHost,
secretPaths,
esClient,
secretHashes,
});
return {
fleetServerHost: secretRes.soObjectWithSecrets,
secretReferences: secretRes.secretReferences,
};
}
export async function extractAndUpdateFleetServerHostsSecrets(opts: {
oldFleetServerHost: NewFleetServerHost;
fleetServerHostUpdate: Partial<NewFleetServerHost>;
esClient: ElasticsearchClient;
secretHashes?: Record<string, any>;
}): Promise<{
fleetServerHostUpdate: Partial<NewFleetServerHost>;
secretReferences: PolicySecretReference[];
secretsToDelete: PolicySecretReference[];
}> {
const { oldFleetServerHost, fleetServerHostUpdate, esClient, secretHashes } = opts;
const oldSecretPaths = getFleetServerHostsSecretPaths(oldFleetServerHost);
const updatedSecretPaths = getFleetServerHostsSecretPaths(fleetServerHostUpdate);
const secretsRes = await extractAndUpdateSOSecrets<FleetServerHost>({
updatedSoObject: fleetServerHostUpdate,
oldSecretPaths,
updatedSecretPaths,
esClient,
secretHashes,
});
return {
fleetServerHostUpdate: secretsRes.updatedSoObject,
secretReferences: secretsRes.secretReferences,
secretsToDelete: secretsRes.secretsToDelete,
};
}
export async function deleteFleetServerHostsSecrets(opts: {
fleetServerHost: NewFleetServerHost;
esClient: ElasticsearchClient;
}): Promise<void> {
const { fleetServerHost, esClient } = opts;
const secretPaths = getFleetServerHostsSecretPaths(fleetServerHost);
await deleteSOSecrets(esClient, secretPaths);
}
export function getFleetServerHostsSecretReferences(
fleetServerHost: FleetServerHost
): PolicySecretReference[] {
const secretPaths: PolicySecretReference[] = [];
if (typeof fleetServerHost.secrets?.ssl?.key === 'object') {
secretPaths.push({
id: fleetServerHost.secrets.ssl.key.id,
});
}
if (typeof fleetServerHost.secrets?.ssl?.es_key === 'object') {
secretPaths.push({
id: fleetServerHost.secrets.ssl.es_key.id,
});
}
return secretPaths;
}
// Download sources functions
function getDownloadSourcesSecretPaths(
downloadSource: DownloadSource | Partial<DownloadSource>
): SOSecretPath[] {
const secretPaths: SOSecretPath[] = [];
if (downloadSource?.secrets?.ssl?.key) {
secretPaths.push({
path: 'secrets.ssl.key',
value: downloadSource.secrets.ssl.key,
});
}
return secretPaths;
}
export async function extractAndWriteDownloadSourcesSecrets(opts: {
downloadSource: DownloadSourceBase;
esClient: ElasticsearchClient;
secretHashes?: Record<string, any>;
}): Promise<{ downloadSource: DownloadSourceBase; secretReferences: PolicySecretReference[] }> {
const { downloadSource, esClient, secretHashes = {} } = opts;
const secretPaths = getFleetServerHostsSecretPaths(downloadSource).filter(
(path) => typeof path.value === 'string'
);
const secretRes = await extractAndWriteSOSecrets<DownloadSourceBase>({
soObject: downloadSource,
secretPaths,
esClient,
secretHashes,
});
return {
downloadSource: secretRes.soObjectWithSecrets,
secretReferences: secretRes.secretReferences,
};
}
export async function extractAndUpdateDownloadSourceSecrets(opts: {
oldDownloadSource: DownloadSourceBase;
downloadSourceUpdate: Partial<DownloadSourceBase>;
esClient: ElasticsearchClient;
secretHashes?: Record<string, any>;
}): Promise<{
downloadSourceUpdate: Partial<DownloadSourceBase>;
secretReferences: PolicySecretReference[];
secretsToDelete: PolicySecretReference[];
}> {
const { oldDownloadSource, downloadSourceUpdate, esClient, secretHashes } = opts;
const oldSecretPaths = getDownloadSourcesSecretPaths(oldDownloadSource);
const updatedSecretPaths = getDownloadSourcesSecretPaths(downloadSourceUpdate);
const secretsRes = await extractAndUpdateSOSecrets<DownloadSourceBase>({
updatedSoObject: downloadSourceUpdate,
oldSecretPaths,
updatedSecretPaths,
esClient,
secretHashes,
});
return {
downloadSourceUpdate: secretsRes.updatedSoObject,
secretReferences: secretsRes.secretReferences,
secretsToDelete: secretsRes.secretsToDelete,
};
}
export async function deleteDownloadSourceSecrets(opts: {
downloadSource: DownloadSourceBase;
esClient: ElasticsearchClient;
}): Promise<void> {
const { downloadSource, esClient } = opts;
const secretPaths = getDownloadSourcesSecretPaths(downloadSource);
await deleteSOSecrets(esClient, secretPaths);
}
export function getDownloadSourceSecretReferences(
downloadSource: DownloadSource
): PolicySecretReference[] {
const secretPaths: PolicySecretReference[] = [];
if (typeof downloadSource.secrets?.ssl?.key === 'object') {
secretPaths.push({
id: downloadSource.secrets.ssl.key.id,
});
}
return secretPaths;
}