mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 17:27:16 +00:00
feat: added back secret referencing expansion support
This commit is contained in:
@@ -17,6 +17,23 @@ export const groupBy = <T, Key extends string | number | symbol>(
|
|||||||
{} as Record<Key, T[]>
|
{} as Record<Key, T[]>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts an array of items into groups. The return value is a map where the keys are
|
||||||
|
* the group ids the given getGroupId function produced and the value will be the last found one for the group key
|
||||||
|
*/
|
||||||
|
export const groupByUnique = <T, Key extends string | number | symbol>(
|
||||||
|
array: readonly T[],
|
||||||
|
getGroupId: (item: T) => Key
|
||||||
|
): Record<Key, T> =>
|
||||||
|
array.reduce(
|
||||||
|
(acc, item) => {
|
||||||
|
const groupId = getGroupId(item);
|
||||||
|
acc[groupId] = item;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<Key, T>
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a list of items returns a new list with only
|
* Given a list of items returns a new list with only
|
||||||
* unique items. Accepts an optional identity function
|
* unique items. Accepts an optional identity function
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { SecretType, TSecretImports, TSecrets, TSecretsV2 } from "@app/db/schemas";
|
import { SecretType, TSecretImports, TSecrets, TSecretsV2 } from "@app/db/schemas";
|
||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy, unique } from "@app/lib/fn";
|
||||||
|
|
||||||
import { TSecretDALFactory } from "../secret/secret-dal";
|
import { TSecretDALFactory } from "../secret/secret-dal";
|
||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
@@ -146,7 +146,8 @@ export const fnSecretsV2FromImports = async ({
|
|||||||
secretImportDAL,
|
secretImportDAL,
|
||||||
depth = 0,
|
depth = 0,
|
||||||
cyclicDetector = new Set(),
|
cyclicDetector = new Set(),
|
||||||
decryptor
|
decryptor,
|
||||||
|
expandSecretReferences
|
||||||
}: {
|
}: {
|
||||||
allowedImports: (Omit<TSecretImports, "importEnv"> & {
|
allowedImports: (Omit<TSecretImports, "importEnv"> & {
|
||||||
importEnv: { id: string; slug: string; name: string };
|
importEnv: { id: string; slug: string; name: string };
|
||||||
@@ -157,6 +158,9 @@ export const fnSecretsV2FromImports = async ({
|
|||||||
depth?: number;
|
depth?: number;
|
||||||
cyclicDetector?: Set<string>;
|
cyclicDetector?: Set<string>;
|
||||||
decryptor: (value?: Buffer | null) => string | undefined;
|
decryptor: (value?: Buffer | null) => string | undefined;
|
||||||
|
expandSecretReferences?: (
|
||||||
|
secrets: Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>
|
||||||
|
) => Promise<Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>>;
|
||||||
}) => {
|
}) => {
|
||||||
// avoid going more than a depth
|
// avoid going more than a depth
|
||||||
if (depth >= LEVEL_BREAK) return [];
|
if (depth >= LEVEL_BREAK) return [];
|
||||||
@@ -206,16 +210,27 @@ export const fnSecretsV2FromImports = async ({
|
|||||||
secretDAL,
|
secretDAL,
|
||||||
depth: depth + 1,
|
depth: depth + 1,
|
||||||
cyclicDetector,
|
cyclicDetector,
|
||||||
decryptor
|
decryptor,
|
||||||
|
expandSecretReferences
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const secretsFromdeeperImportGroupedByFolderId = groupBy(secretsFromDeeperImports, (i) => i.importFolderId);
|
const secretsFromdeeperImportGroupedByFolderId = groupBy(secretsFromDeeperImports, (i) => i.importFolderId);
|
||||||
|
|
||||||
const secrets = allowedImports.map(({ importPath, importEnv, id, folderId }, i) => {
|
const processedImports = allowedImports.map(({ importPath, importEnv, id, folderId }, i) => {
|
||||||
const sourceImportFolder = importedFolderGroupBySourceImport[`${importEnv.id}-${importPath}`][0];
|
const sourceImportFolder = importedFolderGroupBySourceImport[`${importEnv.id}-${importPath}`][0];
|
||||||
const folderDeeperImportSecrets =
|
const folderDeeperImportSecrets =
|
||||||
secretsFromdeeperImportGroupedByFolderId?.[sourceImportFolder?.id || ""]?.[0]?.secrets || [];
|
secretsFromdeeperImportGroupedByFolderId?.[sourceImportFolder?.id || ""]?.[0]?.secrets || [];
|
||||||
|
const secretsWithDuplicate = (importedSecretsGroupByFolderId?.[importedFolders?.[i]?.id as string] || [])
|
||||||
|
.map((item) => ({
|
||||||
|
...item,
|
||||||
|
secretKey: item.key,
|
||||||
|
secretValue: decryptor(item.encryptedValue),
|
||||||
|
secretComment: decryptor(item.encryptedComment),
|
||||||
|
environment: importEnv.slug,
|
||||||
|
workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend.
|
||||||
|
_id: item.id // The old Python SDK depends on the _id field being returned. We return this to keep the older Python SDK versions backwards compatible with the new Postgres backend.
|
||||||
|
}))
|
||||||
|
.concat(folderDeeperImportSecrets);
|
||||||
return {
|
return {
|
||||||
secretPath: importPath,
|
secretPath: importPath,
|
||||||
environment: importEnv.slug,
|
environment: importEnv.slug,
|
||||||
@@ -223,19 +238,33 @@ export const fnSecretsV2FromImports = async ({
|
|||||||
folderId: importedFolders?.[i]?.id,
|
folderId: importedFolders?.[i]?.id,
|
||||||
id,
|
id,
|
||||||
importFolderId: folderId,
|
importFolderId: folderId,
|
||||||
secrets: (importedSecretsGroupByFolderId?.[importedFolders?.[i]?.id as string] || [])
|
secrets: unique(secretsWithDuplicate, (el) => el.secretKey)
|
||||||
.map((item) => ({
|
|
||||||
...item,
|
|
||||||
secretKey: item.key,
|
|
||||||
secretValue: decryptor(item.encryptedValue),
|
|
||||||
secretComment: decryptor(item.encryptedComment),
|
|
||||||
environment: importEnv.slug,
|
|
||||||
workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend.
|
|
||||||
_id: item.id // The old Python SDK depends on the _id field being returned. We return this to keep the older Python SDK versions backwards compatible with the new Postgres backend.
|
|
||||||
}))
|
|
||||||
.concat(folderDeeperImportSecrets)
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return secrets;
|
if (expandSecretReferences) {
|
||||||
|
await Promise.all(
|
||||||
|
processedImports.map(async (processedImport) => {
|
||||||
|
const secretsGroupByKey = processedImport.secrets.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);
|
||||||
|
processedImport.secrets.forEach((decryptedSecret) => {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
decryptedSecret.secretValue = secretsGroupByKey[decryptedSecret.secretKey].value;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return processedImports;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -363,49 +363,60 @@ export const recursivelyGetSecretPaths = async ({
|
|||||||
|
|
||||||
return allowedPaths;
|
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 = {
|
type TInterpolateSecretArg = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
decryptSecret: (encryptedValue?: Buffer | null) => string | undefined;
|
decryptSecretValue: (encryptedValue?: Buffer | null) => string | undefined;
|
||||||
secretDAL: Pick<TSecretV2BridgeDALFactory, "findByFolderId">;
|
secretDAL: Pick<TSecretV2BridgeDALFactory, "findByFolderId">;
|
||||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const interpolateSecrets = ({ projectId, decryptSecret, secretDAL, folderDAL }: TInterpolateSecretArg) => {
|
export const expandSecretReferencesFactory = ({
|
||||||
const fetchSecretsCrossEnv = () => {
|
projectId,
|
||||||
const fetchCache: Record<string, Record<string, string>> = {};
|
decryptSecretValue: decryptSecret,
|
||||||
|
secretDAL,
|
||||||
|
folderDAL
|
||||||
|
}: TInterpolateSecretArg) => {
|
||||||
|
const fetchSecretFactory = () => {
|
||||||
|
const secretCache: Record<string, Record<string, string>> = {};
|
||||||
|
|
||||||
return async (secRefEnv: string, secRefPath: string[], secRefKey: string) => {
|
return async (secRefEnv: string, secRefPath: string[], secRefKey: string) => {
|
||||||
const secRefPathUrl = path.join("/", ...secRefPath);
|
const referredSecretPathURL = path.join("/", ...secRefPath);
|
||||||
const uniqKey = `${secRefEnv}-${secRefPathUrl}`;
|
const uniqueKey = `${secRefEnv}-${referredSecretPathURL}`;
|
||||||
|
|
||||||
if (fetchCache?.[uniqKey]) {
|
if (secretCache?.[uniqueKey]) {
|
||||||
return fetchCache[uniqKey][secRefKey];
|
return secretCache[uniqueKey][secRefKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, secRefEnv, secRefPathUrl);
|
const folder = await folderDAL.findBySecretPath(projectId, secRefEnv, referredSecretPathURL);
|
||||||
if (!folder) return "";
|
if (!folder) return "";
|
||||||
const secrets = await secretDAL.findByFolderId(folder.id);
|
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
|
// eslint-disable-next-line
|
||||||
prev[secret.key] = decryptSecret(secret.encryptedValue) || "";
|
prev[secret.key] = decryptSecret(secret.encryptedValue) || "";
|
||||||
return prev;
|
return prev;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
fetchCache[uniqKey] = decryptedSec;
|
secretCache[uniqueKey] = decryptedSecret;
|
||||||
|
|
||||||
return fetchCache[uniqKey][secRefKey];
|
return secretCache[uniqueKey][secRefKey];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const recursivelyExpandSecret = async (
|
const recursivelyExpandSecret = async (
|
||||||
expandedSec: Record<string, string>,
|
expandedSec: Record<string, string | undefined>,
|
||||||
interpolatedSec: Record<string, string>,
|
interpolatedSec: Record<string, string | undefined>,
|
||||||
fetchCrossEnv: (env: string, secPath: string[], secKey: string) => Promise<string>,
|
fetchSecret: (env: string, secPath: string[], secKey: string) => Promise<string>,
|
||||||
recursionChainBreaker: Record<string, boolean>,
|
recursionChainBreaker: Record<string, boolean>,
|
||||||
key: string
|
key: string
|
||||||
) => {
|
): Promise<string | undefined> => {
|
||||||
if (expandedSec?.[key] !== undefined) {
|
if (expandedSec?.[key] !== undefined) {
|
||||||
return expandedSec[key];
|
return expandedSec[key];
|
||||||
}
|
}
|
||||||
@@ -433,7 +444,7 @@ export const interpolateSecrets = ({ projectId, decryptSecret, secretDAL, folder
|
|||||||
const val = await recursivelyExpandSecret(
|
const val = await recursivelyExpandSecret(
|
||||||
expandedSec,
|
expandedSec,
|
||||||
interpolatedSec,
|
interpolatedSec,
|
||||||
fetchCrossEnv,
|
fetchSecret,
|
||||||
recursionChainBreaker,
|
recursionChainBreaker,
|
||||||
interpolationKey
|
interpolationKey
|
||||||
);
|
);
|
||||||
@@ -450,7 +461,7 @@ export const interpolateSecrets = ({ projectId, decryptSecret, secretDAL, folder
|
|||||||
const secRefKey = entities[entities.length - 1];
|
const secRefKey = entities[entities.length - 1];
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const val = await fetchCrossEnv(secRefEnv, secRefPath, secRefKey);
|
const val = await fetchSecret(secRefEnv, secRefPath, secRefKey);
|
||||||
if (val) {
|
if (val) {
|
||||||
interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val);
|
interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val);
|
||||||
}
|
}
|
||||||
@@ -463,36 +474,28 @@ export const interpolateSecrets = ({ projectId, decryptSecret, secretDAL, folder
|
|||||||
return interpolatedValue;
|
return interpolatedValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
// used to convert multi line ones to quotes ones with \n
|
const fetchSecret = fetchSecretFactory();
|
||||||
const formatMultiValueEnv = (val?: string) => {
|
|
||||||
if (!val) return "";
|
|
||||||
if (!val.match("\n")) return val;
|
|
||||||
return `"${val.replace(/\n/g, "\\n")}"`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const expandSecrets = async (
|
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 expandedSecrets: Record<string, string | undefined> = {};
|
||||||
const interpolatedSec: Record<string, string> = {};
|
const toBeExpandedSecrets: Record<string, string | undefined> = {};
|
||||||
|
|
||||||
const crossSecEnvFetch = fetchSecretsCrossEnv();
|
Object.keys(inputSecrets).forEach((key) => {
|
||||||
|
if (inputSecrets[key].value?.match(INTERPOLATION_SYNTAX_REG)) {
|
||||||
Object.keys(secrets).forEach((key) => {
|
toBeExpandedSecrets[key] = inputSecrets[key].value;
|
||||||
if (secrets[key].value.match(INTERPOLATION_SYNTAX_REG)) {
|
|
||||||
interpolatedSec[key] = secrets[key].value;
|
|
||||||
} else {
|
} else {
|
||||||
expandedSec[key] = secrets[key].value;
|
expandedSecrets[key] = inputSecrets[key].value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const key of Object.keys(secrets)) {
|
for (const key of Object.keys(inputSecrets)) {
|
||||||
if (expandedSec?.[key]) {
|
if (expandedSecrets?.[key]) {
|
||||||
// should not do multi line encoding if user has set it to skip
|
// should not do multi line encoding if user has set it to skip
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
secrets[key].value = secrets[key].skipMultilineEncoding
|
inputSecrets[key].value = inputSecrets[key].skipMultilineEncoding
|
||||||
? formatMultiValueEnv(expandedSec[key])
|
? formatMultiValueEnv(expandedSecrets[key])
|
||||||
: expandedSec[key];
|
: expandedSecrets[key];
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -502,18 +505,20 @@ export const interpolateSecrets = ({ projectId, decryptSecret, secretDAL, folder
|
|||||||
const recursionChainBreaker: Record<string, boolean> = {};
|
const recursionChainBreaker: Record<string, boolean> = {};
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const expandedVal = await recursivelyExpandSecret(
|
const expandedVal = await recursivelyExpandSecret(
|
||||||
expandedSec,
|
expandedSecrets,
|
||||||
interpolatedSec,
|
toBeExpandedSecrets,
|
||||||
crossSecEnvFetch,
|
fetchSecret,
|
||||||
recursionChainBreaker,
|
recursionChainBreaker,
|
||||||
key
|
key
|
||||||
);
|
);
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// 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;
|
return expandSecrets;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,11 +24,11 @@ import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
|
|||||||
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
|
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
|
||||||
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
|
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
|
||||||
import {
|
import {
|
||||||
|
expandSecretReferencesFactory,
|
||||||
fnSecretBulkDelete,
|
fnSecretBulkDelete,
|
||||||
fnSecretBulkInsert,
|
fnSecretBulkInsert,
|
||||||
fnSecretBulkUpdate,
|
fnSecretBulkUpdate,
|
||||||
getAllNestedSecretReferences,
|
getAllNestedSecretReferences,
|
||||||
interpolateSecrets,
|
|
||||||
recursivelyGetSecretPaths,
|
recursivelyGetSecretPaths,
|
||||||
reshapeBridgeSecret
|
reshapeBridgeSecret
|
||||||
} from "./secret-v2-bridge-fns";
|
} from "./secret-v2-bridge-fns";
|
||||||
@@ -428,7 +428,8 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
includeImports,
|
includeImports,
|
||||||
recursive // TODO(akhilmhdh-sev2): add logic for expandSecretReferences
|
recursive,
|
||||||
|
expandSecretReferences: shouldExpandSecretReferences
|
||||||
}: TGetSecretsDTO) => {
|
}: TGetSecretsDTO) => {
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
@@ -484,57 +485,81 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
projectId
|
projectId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (includeImports) {
|
const decryptedSecrets = secrets.map((secret) =>
|
||||||
const secretImports = await secretImportDAL.findByFolderIds(paths.map((p) => p.folderId));
|
reshapeBridgeSecret(projectId, environment, groupedPaths[secret.folderId][0].path, {
|
||||||
const allowedImports = secretImports.filter(({ importEnv, importPath, isReplication }) =>
|
...secret,
|
||||||
!isReplication &&
|
value: secret.encryptedValue
|
||||||
// if its service token allow full access over imported one
|
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
|
||||||
actor === ActorType.SERVICE
|
: undefined,
|
||||||
? true
|
comment: secret.encryptedComment
|
||||||
: permission.can(
|
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
|
||||||
ProjectPermissionActions.Read,
|
: undefined
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
})
|
||||||
environment: importEnv.slug,
|
);
|
||||||
secretPath: importPath
|
const expandSecretReferences = expandSecretReferencesFactory({
|
||||||
})
|
projectId,
|
||||||
)
|
folderDAL,
|
||||||
);
|
secretDAL,
|
||||||
const importedSecrets = await fnSecretsV2FromImports({
|
decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined)
|
||||||
allowedImports,
|
});
|
||||||
secretDAL,
|
|
||||||
folderDAL,
|
|
||||||
secretImportDAL,
|
|
||||||
decryptor: (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 {
|
return {
|
||||||
secrets: secrets.map((secret) =>
|
secrets: decryptedSecrets
|
||||||
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
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
return {
|
||||||
secrets: secrets.map((secret) =>
|
secrets: decryptedSecrets,
|
||||||
reshapeBridgeSecret(projectId, environment, groupedPaths[secret.folderId][0].path, {
|
imports: importedSecrets
|
||||||
...secret,
|
|
||||||
value: secret.encryptedValue
|
|
||||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
|
|
||||||
: undefined,
|
|
||||||
comment: secret.encryptedComment
|
|
||||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
|
|
||||||
: undefined
|
|
||||||
})
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -550,7 +575,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
secretName,
|
secretName,
|
||||||
version,
|
version,
|
||||||
includeImports,
|
includeImports,
|
||||||
expandSecretReferences
|
expandSecretReferences: shouldExpandSecretReferences
|
||||||
}: TGetASecretDTO) => {
|
}: TGetASecretDTO) => {
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
@@ -600,11 +625,11 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
})
|
})
|
||||||
.then((el) => SecretsV2Schema.parse({ ...el, id: el.secretId })));
|
.then((el) => SecretsV2Schema.parse({ ...el, id: el.secretId })));
|
||||||
|
|
||||||
const interpolateInlineSecretReference = interpolateSecrets({
|
const expandSecretReferences = expandSecretReferencesFactory({
|
||||||
projectId,
|
projectId,
|
||||||
decryptSecret: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined),
|
folderDAL,
|
||||||
secretDAL,
|
secretDAL,
|
||||||
folderDAL
|
decryptSecretValue: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined)
|
||||||
});
|
});
|
||||||
|
|
||||||
// now if secret is not found
|
// now if secret is not found
|
||||||
@@ -629,33 +654,20 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
secretDAL,
|
secretDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
secretImportDAL,
|
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 i = importedSecrets.length - 1; i >= 0; i -= 1) {
|
||||||
for (let j = 0; j < importedSecrets[i].secrets.length; j += 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];
|
||||||
const importedSecret = importedSecrets[i].secrets[j];
|
if (secretName === importedSecret.key) {
|
||||||
let secretValue = importedSecret.encryptedValue
|
return reshapeBridgeSecret(
|
||||||
? secretManagerDecryptor({ cipherTextBlob: importedSecret.encryptedValue }).toString()
|
projectId,
|
||||||
: undefined;
|
importedSecrets[i].environment,
|
||||||
|
importedSecrets[i].secretPath,
|
||||||
if (expandSecretReferences && secretValue) {
|
importedSecret
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -665,13 +677,13 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
let secretValue = secret.encryptedValue
|
let secretValue = secret.encryptedValue
|
||||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
|
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
|
||||||
: undefined;
|
: undefined;
|
||||||
if (expandSecretReferences && secretValue) {
|
if (shouldExpandSecretReferences && secretValue) {
|
||||||
const secretReferenceExpandedString = {
|
const secretReferenceExpandedRecord = {
|
||||||
[secret.key]: { value: secretValue }
|
[secret.key]: { value: secretValue }
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
await interpolateInlineSecretReference(secretReferenceExpandedString);
|
await expandSecretReferences(secretReferenceExpandedRecord);
|
||||||
secretValue = secretReferenceExpandedString[secret.key].value;
|
secretValue = secretReferenceExpandedRecord[secret.key].value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return reshapeBridgeSecret(projectId, environment, path, {
|
return reshapeBridgeSecret(projectId, environment, path, {
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
|||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
|
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
|
||||||
|
import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
|
||||||
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||||
import { getAllNestedSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns";
|
import { expandSecretReferencesFactory, getAllNestedSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns";
|
||||||
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
|
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
|
||||||
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
|
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
|
||||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
@@ -55,7 +56,7 @@ type TSecretQueueFactoryDep = {
|
|||||||
integrationAuthService: Pick<TIntegrationAuthServiceFactory, "getIntegrationAccessToken">;
|
integrationAuthService: Pick<TIntegrationAuthServiceFactory, "getIntegrationAccessToken">;
|
||||||
folderDAL: TSecretFolderDALFactory;
|
folderDAL: TSecretFolderDALFactory;
|
||||||
secretDAL: TSecretDALFactory;
|
secretDAL: TSecretDALFactory;
|
||||||
secretImportDAL: Pick<TSecretImportDALFactory, "find">;
|
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
|
||||||
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
|
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
|
||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "find">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "find">;
|
||||||
projectDAL: TProjectDALFactory;
|
projectDAL: TProjectDALFactory;
|
||||||
@@ -249,7 +250,7 @@ export const secretQueueFactory = ({
|
|||||||
depth: number;
|
depth: number;
|
||||||
decryptor: (value: Buffer | null | undefined) => string;
|
decryptor: (value: Buffer | null | undefined) => string;
|
||||||
}) => {
|
}) => {
|
||||||
let content: TIntegrationSecret = {};
|
const content: TIntegrationSecret = {};
|
||||||
if (dto.depth > MAX_SYNC_SECRET_DEPTH) {
|
if (dto.depth > MAX_SYNC_SECRET_DEPTH) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`getIntegrationSecrets: secret depth exceeded for [projectId=${dto.projectId}] [folderId=${dto.folderId}] [depth=${dto.depth}]`
|
`getIntegrationSecrets: secret depth exceeded for [projectId=${dto.projectId}] [folderId=${dto.folderId}] [depth=${dto.depth}]`
|
||||||
@@ -272,38 +273,40 @@ export const secretQueueFactory = ({
|
|||||||
content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
|
content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO(akhilmhdh-sev2): change this to v2 expand secrets
|
const expandSecretReferences = expandSecretReferencesFactory({
|
||||||
|
decryptSecretValue: dto.decryptor,
|
||||||
|
secretDAL: secretV2BridgeDAL,
|
||||||
|
folderDAL,
|
||||||
|
projectId: dto.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
await expandSecretReferences(content);
|
||||||
// check if current folder has any imports from other folders
|
// check if current folder has any imports from other folders
|
||||||
const secretImport = await secretImportDAL.find({ folderId: dto.folderId, isReplication: false });
|
const secretImports = await secretImportDAL.find({ folderId: dto.folderId, isReplication: false });
|
||||||
|
|
||||||
// if no imports then return secrets in the current folder
|
// if no imports then return secrets in the current folder
|
||||||
if (!secretImport) return content;
|
if (!secretImports.length) return content;
|
||||||
|
const importedSecrets = await fnSecretsV2FromImports({
|
||||||
|
decryptor: dto.decryptor,
|
||||||
|
folderDAL,
|
||||||
|
secretDAL: secretV2BridgeDAL,
|
||||||
|
expandSecretReferences,
|
||||||
|
secretImportDAL,
|
||||||
|
allowedImports: secretImports
|
||||||
|
});
|
||||||
|
|
||||||
const importedFolders = await folderDAL.findByManySecretPath(
|
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
|
||||||
secretImport.map(({ importEnv, importPath }) => ({
|
for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) {
|
||||||
envId: importEnv.id,
|
const importedSecret = importedSecrets[i].secrets[j];
|
||||||
secretPath: importPath
|
if (!content[importedSecret.key]) {
|
||||||
}))
|
content[importedSecret.key] = {
|
||||||
);
|
skipMultilineEncoding: importedSecret.skipMultilineEncoding,
|
||||||
|
comment: importedSecret.secretComment,
|
||||||
for await (const folder of importedFolders) {
|
value: importedSecret.secretValue || ""
|
||||||
if (folder) {
|
};
|
||||||
// get secrets contained in each imported folder by recursively calling
|
}
|
||||||
// this function against the imported folder
|
|
||||||
const importedSecrets = await getIntegrationSecretsV2({
|
|
||||||
environment: dto.environment,
|
|
||||||
projectId: dto.projectId,
|
|
||||||
folderId: folder.id,
|
|
||||||
depth: dto.depth + 1,
|
|
||||||
decryptor: dto.decryptor
|
|
||||||
});
|
|
||||||
|
|
||||||
// add the imported secrets to the current folder secrets
|
|
||||||
content = { ...importedSecrets, ...content };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -628,6 +631,23 @@ export const secretQueueFactory = ({
|
|||||||
logger.info(
|
logger.info(
|
||||||
`getIntegrationSecrets: secret integration sync started [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${job.data.depth}]`
|
`getIntegrationSecrets: secret integration sync started [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${job.data.depth}]`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const secrets = shouldUseSecretV2Bridge
|
||||||
|
? await getIntegrationSecretsV2({
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
folderId: folder.id,
|
||||||
|
depth: 1,
|
||||||
|
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
|
||||||
|
})
|
||||||
|
: await getIntegrationSecrets({
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
folderId: folder.id,
|
||||||
|
key: botKey as string,
|
||||||
|
depth: 1
|
||||||
|
});
|
||||||
|
|
||||||
for (const integration of toBeSyncedIntegrations) {
|
for (const integration of toBeSyncedIntegrations) {
|
||||||
const integrationAuth = {
|
const integrationAuth = {
|
||||||
...integration.integrationAuth,
|
...integration.integrationAuth,
|
||||||
@@ -661,21 +681,6 @@ export const secretQueueFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const secrets = shouldUseSecretV2Bridge
|
|
||||||
? await getIntegrationSecretsV2({
|
|
||||||
environment,
|
|
||||||
projectId,
|
|
||||||
folderId: folder.id,
|
|
||||||
depth: 1,
|
|
||||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
|
|
||||||
})
|
|
||||||
: await getIntegrationSecrets({
|
|
||||||
environment,
|
|
||||||
projectId,
|
|
||||||
folderId: folder.id,
|
|
||||||
key: botKey as string,
|
|
||||||
depth: 1
|
|
||||||
});
|
|
||||||
const suffixedSecrets: typeof secrets = {};
|
const suffixedSecrets: typeof secrets = {};
|
||||||
const metadata = integration.metadata as Record<string, string>;
|
const metadata = integration.metadata as Record<string, string>;
|
||||||
if (metadata) {
|
if (metadata) {
|
||||||
|
|||||||
Reference in New Issue
Block a user