mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 21:51:56 +00:00
feat: added back secret referencing expansion support
This commit is contained in:
@@ -363,49 +363,60 @@ export const recursivelyGetSecretPaths = async ({
|
||||
|
||||
return allowedPaths;
|
||||
};
|
||||
// used to convert multi line ones to quotes ones with \n
|
||||
const formatMultiValueEnv = (val?: string) => {
|
||||
if (!val) return "";
|
||||
if (!val.match("\n")) return val;
|
||||
return `"${val.replace(/\n/g, "\\n")}"`;
|
||||
};
|
||||
|
||||
type TInterpolateSecretArg = {
|
||||
projectId: string;
|
||||
decryptSecret: (encryptedValue?: Buffer | null) => string | undefined;
|
||||
decryptSecretValue: (encryptedValue?: Buffer | null) => string | undefined;
|
||||
secretDAL: Pick<TSecretV2BridgeDALFactory, "findByFolderId">;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
||||
};
|
||||
|
||||
export const interpolateSecrets = ({ projectId, decryptSecret, secretDAL, folderDAL }: TInterpolateSecretArg) => {
|
||||
const fetchSecretsCrossEnv = () => {
|
||||
const fetchCache: Record<string, Record<string, string>> = {};
|
||||
export const expandSecretReferencesFactory = ({
|
||||
projectId,
|
||||
decryptSecretValue: decryptSecret,
|
||||
secretDAL,
|
||||
folderDAL
|
||||
}: TInterpolateSecretArg) => {
|
||||
const fetchSecretFactory = () => {
|
||||
const secretCache: Record<string, Record<string, string>> = {};
|
||||
|
||||
return async (secRefEnv: string, secRefPath: string[], secRefKey: string) => {
|
||||
const secRefPathUrl = path.join("/", ...secRefPath);
|
||||
const uniqKey = `${secRefEnv}-${secRefPathUrl}`;
|
||||
const referredSecretPathURL = path.join("/", ...secRefPath);
|
||||
const uniqueKey = `${secRefEnv}-${referredSecretPathURL}`;
|
||||
|
||||
if (fetchCache?.[uniqKey]) {
|
||||
return fetchCache[uniqKey][secRefKey];
|
||||
if (secretCache?.[uniqueKey]) {
|
||||
return secretCache[uniqueKey][secRefKey];
|
||||
}
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, secRefEnv, secRefPathUrl);
|
||||
const folder = await folderDAL.findBySecretPath(projectId, secRefEnv, referredSecretPathURL);
|
||||
if (!folder) return "";
|
||||
const secrets = await secretDAL.findByFolderId(folder.id);
|
||||
|
||||
const decryptedSec = secrets.reduce<Record<string, string>>((prev, secret) => {
|
||||
const decryptedSecret = secrets.reduce<Record<string, string>>((prev, secret) => {
|
||||
// eslint-disable-next-line
|
||||
prev[secret.key] = decryptSecret(secret.encryptedValue) || "";
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
fetchCache[uniqKey] = decryptedSec;
|
||||
secretCache[uniqueKey] = decryptedSecret;
|
||||
|
||||
return fetchCache[uniqKey][secRefKey];
|
||||
return secretCache[uniqueKey][secRefKey];
|
||||
};
|
||||
};
|
||||
|
||||
const recursivelyExpandSecret = async (
|
||||
expandedSec: Record<string, string>,
|
||||
interpolatedSec: Record<string, string>,
|
||||
fetchCrossEnv: (env: string, secPath: string[], secKey: string) => Promise<string>,
|
||||
expandedSec: Record<string, string | undefined>,
|
||||
interpolatedSec: Record<string, string | undefined>,
|
||||
fetchSecret: (env: string, secPath: string[], secKey: string) => Promise<string>,
|
||||
recursionChainBreaker: Record<string, boolean>,
|
||||
key: string
|
||||
) => {
|
||||
): Promise<string | undefined> => {
|
||||
if (expandedSec?.[key] !== undefined) {
|
||||
return expandedSec[key];
|
||||
}
|
||||
@@ -433,7 +444,7 @@ export const interpolateSecrets = ({ projectId, decryptSecret, secretDAL, folder
|
||||
const val = await recursivelyExpandSecret(
|
||||
expandedSec,
|
||||
interpolatedSec,
|
||||
fetchCrossEnv,
|
||||
fetchSecret,
|
||||
recursionChainBreaker,
|
||||
interpolationKey
|
||||
);
|
||||
@@ -450,7 +461,7 @@ export const interpolateSecrets = ({ projectId, decryptSecret, secretDAL, folder
|
||||
const secRefKey = entities[entities.length - 1];
|
||||
|
||||
// eslint-disable-next-line
|
||||
const val = await fetchCrossEnv(secRefEnv, secRefPath, secRefKey);
|
||||
const val = await fetchSecret(secRefEnv, secRefPath, secRefKey);
|
||||
if (val) {
|
||||
interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val);
|
||||
}
|
||||
@@ -463,36 +474,28 @@ export const interpolateSecrets = ({ projectId, decryptSecret, secretDAL, folder
|
||||
return interpolatedValue;
|
||||
};
|
||||
|
||||
// used to convert multi line ones to quotes ones with \n
|
||||
const formatMultiValueEnv = (val?: string) => {
|
||||
if (!val) return "";
|
||||
if (!val.match("\n")) return val;
|
||||
return `"${val.replace(/\n/g, "\\n")}"`;
|
||||
};
|
||||
|
||||
const fetchSecret = fetchSecretFactory();
|
||||
const expandSecrets = async (
|
||||
secrets: Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean | null }>
|
||||
inputSecrets: Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>
|
||||
) => {
|
||||
const expandedSec: Record<string, string> = {};
|
||||
const interpolatedSec: Record<string, string> = {};
|
||||
const expandedSecrets: Record<string, string | undefined> = {};
|
||||
const toBeExpandedSecrets: Record<string, string | undefined> = {};
|
||||
|
||||
const crossSecEnvFetch = fetchSecretsCrossEnv();
|
||||
|
||||
Object.keys(secrets).forEach((key) => {
|
||||
if (secrets[key].value.match(INTERPOLATION_SYNTAX_REG)) {
|
||||
interpolatedSec[key] = secrets[key].value;
|
||||
Object.keys(inputSecrets).forEach((key) => {
|
||||
if (inputSecrets[key].value?.match(INTERPOLATION_SYNTAX_REG)) {
|
||||
toBeExpandedSecrets[key] = inputSecrets[key].value;
|
||||
} else {
|
||||
expandedSec[key] = secrets[key].value;
|
||||
expandedSecrets[key] = inputSecrets[key].value;
|
||||
}
|
||||
});
|
||||
|
||||
for (const key of Object.keys(secrets)) {
|
||||
if (expandedSec?.[key]) {
|
||||
for (const key of Object.keys(inputSecrets)) {
|
||||
if (expandedSecrets?.[key]) {
|
||||
// should not do multi line encoding if user has set it to skip
|
||||
// eslint-disable-next-line
|
||||
secrets[key].value = secrets[key].skipMultilineEncoding
|
||||
? formatMultiValueEnv(expandedSec[key])
|
||||
: expandedSec[key];
|
||||
inputSecrets[key].value = inputSecrets[key].skipMultilineEncoding
|
||||
? formatMultiValueEnv(expandedSecrets[key])
|
||||
: expandedSecrets[key];
|
||||
// eslint-disable-next-line
|
||||
continue;
|
||||
}
|
||||
@@ -502,18 +505,20 @@ export const interpolateSecrets = ({ projectId, decryptSecret, secretDAL, folder
|
||||
const recursionChainBreaker: Record<string, boolean> = {};
|
||||
// eslint-disable-next-line
|
||||
const expandedVal = await recursivelyExpandSecret(
|
||||
expandedSec,
|
||||
interpolatedSec,
|
||||
crossSecEnvFetch,
|
||||
expandedSecrets,
|
||||
toBeExpandedSecrets,
|
||||
fetchSecret,
|
||||
recursionChainBreaker,
|
||||
key
|
||||
);
|
||||
|
||||
// eslint-disable-next-line
|
||||
secrets[key].value = secrets[key].skipMultilineEncoding ? formatMultiValueEnv(expandedVal) : expandedVal;
|
||||
inputSecrets[key].value = inputSecrets[key].skipMultilineEncoding
|
||||
? formatMultiValueEnv(expandedVal)
|
||||
: expandedVal;
|
||||
}
|
||||
|
||||
return secrets;
|
||||
return inputSecrets;
|
||||
};
|
||||
return expandSecrets;
|
||||
};
|
||||
|
||||
@@ -24,11 +24,11 @@ import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
|
||||
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
|
||||
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
|
||||
import {
|
||||
expandSecretReferencesFactory,
|
||||
fnSecretBulkDelete,
|
||||
fnSecretBulkInsert,
|
||||
fnSecretBulkUpdate,
|
||||
getAllNestedSecretReferences,
|
||||
interpolateSecrets,
|
||||
recursivelyGetSecretPaths,
|
||||
reshapeBridgeSecret
|
||||
} from "./secret-v2-bridge-fns";
|
||||
@@ -428,7 +428,8 @@ export const secretV2BridgeServiceFactory = ({
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
includeImports,
|
||||
recursive // TODO(akhilmhdh-sev2): add logic for expandSecretReferences
|
||||
recursive,
|
||||
expandSecretReferences: shouldExpandSecretReferences
|
||||
}: TGetSecretsDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
@@ -484,57 +485,81 @@ export const secretV2BridgeServiceFactory = ({
|
||||
projectId
|
||||
});
|
||||
|
||||
if (includeImports) {
|
||||
const secretImports = await secretImportDAL.findByFolderIds(paths.map((p) => p.folderId));
|
||||
const allowedImports = secretImports.filter(({ importEnv, importPath, isReplication }) =>
|
||||
!isReplication &&
|
||||
// if its service token allow full access over imported one
|
||||
actor === ActorType.SERVICE
|
||||
? true
|
||||
: permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: importEnv.slug,
|
||||
secretPath: importPath
|
||||
})
|
||||
)
|
||||
);
|
||||
const importedSecrets = await fnSecretsV2FromImports({
|
||||
allowedImports,
|
||||
secretDAL,
|
||||
folderDAL,
|
||||
secretImportDAL,
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined)
|
||||
});
|
||||
const decryptedSecrets = secrets.map((secret) =>
|
||||
reshapeBridgeSecret(projectId, environment, groupedPaths[secret.folderId][0].path, {
|
||||
...secret,
|
||||
value: secret.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
|
||||
: undefined,
|
||||
comment: secret.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
|
||||
: undefined
|
||||
})
|
||||
);
|
||||
const expandSecretReferences = expandSecretReferencesFactory({
|
||||
projectId,
|
||||
folderDAL,
|
||||
secretDAL,
|
||||
decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined)
|
||||
});
|
||||
|
||||
if (shouldExpandSecretReferences) {
|
||||
const secretsGroupByPath = groupBy(decryptedSecrets, (i) => i.secretPath);
|
||||
for (const secretPathKey in secretsGroupByPath) {
|
||||
if (Object.hasOwn(secretsGroupByPath, secretPathKey)) {
|
||||
const secretsGroupByKey = secretsGroupByPath[secretPathKey].reduce(
|
||||
(acc, item) => {
|
||||
acc[item.secretKey] = {
|
||||
value: item.secretValue,
|
||||
comment: item.secretComment,
|
||||
skipMultilineEncoding: item.skipMultilineEncoding
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>
|
||||
);
|
||||
// eslint-disable-next-line
|
||||
await expandSecretReferences(secretsGroupByKey);
|
||||
secretsGroupByPath[secretPathKey].forEach((decryptedSecret) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
decryptedSecret.secretValue = secretsGroupByKey[decryptedSecret.secretKey].value;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!includeImports) {
|
||||
return {
|
||||
secrets: secrets.map((secret) =>
|
||||
reshapeBridgeSecret(projectId, environment, groupedPaths[secret.folderId][0].path, {
|
||||
...secret,
|
||||
value: secret.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
|
||||
: undefined,
|
||||
comment: secret.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
|
||||
: undefined
|
||||
})
|
||||
),
|
||||
imports: importedSecrets
|
||||
secrets: decryptedSecrets
|
||||
};
|
||||
}
|
||||
|
||||
const secretImports = await secretImportDAL.findByFolderIds(paths.map((p) => p.folderId));
|
||||
const allowedImports = secretImports.filter(({ importEnv, importPath, isReplication }) =>
|
||||
!isReplication &&
|
||||
// if its service token allow full access over imported one
|
||||
actor === ActorType.SERVICE
|
||||
? true
|
||||
: permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: importEnv.slug,
|
||||
secretPath: importPath
|
||||
})
|
||||
)
|
||||
);
|
||||
const importedSecrets = await fnSecretsV2FromImports({
|
||||
allowedImports,
|
||||
secretDAL,
|
||||
folderDAL,
|
||||
secretImportDAL,
|
||||
expandSecretReferences,
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined)
|
||||
});
|
||||
|
||||
return {
|
||||
secrets: secrets.map((secret) =>
|
||||
reshapeBridgeSecret(projectId, environment, groupedPaths[secret.folderId][0].path, {
|
||||
...secret,
|
||||
value: secret.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
|
||||
: undefined,
|
||||
comment: secret.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
|
||||
: undefined
|
||||
})
|
||||
)
|
||||
secrets: decryptedSecrets,
|
||||
imports: importedSecrets
|
||||
};
|
||||
};
|
||||
|
||||
@@ -550,7 +575,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretName,
|
||||
version,
|
||||
includeImports,
|
||||
expandSecretReferences
|
||||
expandSecretReferences: shouldExpandSecretReferences
|
||||
}: TGetASecretDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
@@ -600,11 +625,11 @@ export const secretV2BridgeServiceFactory = ({
|
||||
})
|
||||
.then((el) => SecretsV2Schema.parse({ ...el, id: el.secretId })));
|
||||
|
||||
const interpolateInlineSecretReference = interpolateSecrets({
|
||||
const expandSecretReferences = expandSecretReferencesFactory({
|
||||
projectId,
|
||||
decryptSecret: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined),
|
||||
folderDAL,
|
||||
secretDAL,
|
||||
folderDAL
|
||||
decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined)
|
||||
});
|
||||
|
||||
// now if secret is not found
|
||||
@@ -629,33 +654,20 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretDAL,
|
||||
folderDAL,
|
||||
secretImportDAL,
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined)
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined),
|
||||
expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined
|
||||
});
|
||||
|
||||
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
|
||||
for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) {
|
||||
if (secretName === importedSecrets[i].secrets[j].key) {
|
||||
const importedSecret = importedSecrets[i].secrets[j];
|
||||
let secretValue = importedSecret.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: importedSecret.encryptedValue }).toString()
|
||||
: undefined;
|
||||
|
||||
if (expandSecretReferences && secretValue) {
|
||||
const secretReferenceExpandedString = {
|
||||
[importedSecret.key]: { value: secretValue }
|
||||
};
|
||||
// eslint-disable-next-line
|
||||
await interpolateInlineSecretReference(secretReferenceExpandedString);
|
||||
secretValue = secretReferenceExpandedString[importedSecret.key].value;
|
||||
}
|
||||
|
||||
return reshapeBridgeSecret(projectId, importedSecrets[i].environment, importedSecrets[i].secretPath, {
|
||||
...importedSecret,
|
||||
value: secretValue,
|
||||
comment: importedSecret.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: importedSecret.encryptedComment }).toString()
|
||||
: undefined
|
||||
});
|
||||
const importedSecret = importedSecrets[i].secrets[j];
|
||||
if (secretName === importedSecret.key) {
|
||||
return reshapeBridgeSecret(
|
||||
projectId,
|
||||
importedSecrets[i].environment,
|
||||
importedSecrets[i].secretPath,
|
||||
importedSecret
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -665,13 +677,13 @@ export const secretV2BridgeServiceFactory = ({
|
||||
let secretValue = secret.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
|
||||
: undefined;
|
||||
if (expandSecretReferences && secretValue) {
|
||||
const secretReferenceExpandedString = {
|
||||
if (shouldExpandSecretReferences && secretValue) {
|
||||
const secretReferenceExpandedRecord = {
|
||||
[secret.key]: { value: secretValue }
|
||||
};
|
||||
// eslint-disable-next-line
|
||||
await interpolateInlineSecretReference(secretReferenceExpandedString);
|
||||
secretValue = secretReferenceExpandedString[secret.key].value;
|
||||
await expandSecretReferences(secretReferenceExpandedRecord);
|
||||
secretValue = secretReferenceExpandedRecord[secret.key].value;
|
||||
}
|
||||
|
||||
return reshapeBridgeSecret(projectId, environment, path, {
|
||||
|
||||
Reference in New Issue
Block a user