feat: added caching for secret dal\

This commit is contained in:
=
2025-04-12 00:36:41 +05:30
parent 27fdf68e42
commit 5f6db870a6
18 changed files with 288 additions and 38 deletions
+1
View File
@@ -9,6 +9,7 @@ export const mockKeyStore = (): TKeyStoreFactory => {
store[key] = value; store[key] = value;
return "OK"; return "OK";
}, },
setExpiry: async () => 0,
setItemWithExpiry: async (key, value) => { setItemWithExpiry: async (key, value) => {
store[key] = value; store[key] = value;
return "OK"; return "OK";
@@ -113,7 +113,13 @@ type TSecretApprovalRequestServiceFactoryDep = {
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
secretV2BridgeDAL: Pick< secretV2BridgeDAL: Pick<
TSecretV2BridgeDALFactory, TSecretV2BridgeDALFactory,
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find" | "insertMany"
| "upsertSecretReferences"
| "findBySecretKeys"
| "bulkUpdate"
| "deleteMany"
| "find"
| "cacheInvalidateSecretByProjectId"
>; >;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">; secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">; secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
@@ -864,6 +870,7 @@ export const secretApprovalRequestServiceFactory = ({
}); });
} }
await secretV2BridgeDAL.cacheInvalidateSecretByProjectId(projectId);
await snapshotService.performSnapshot(folderId); await snapshotService.performSnapshot(folderId);
const [folder] = await folderDAL.findSecretPathByFolderIds(projectId, [folderId]); const [folder] = await folderDAL.findSecretPathByFolderIds(projectId, [folderId]);
if (!folder) { if (!folder) {
@@ -45,7 +45,14 @@ type TSecretReplicationServiceFactoryDep = {
secretVersionDAL: Pick<TSecretVersionDALFactory, "find" | "insertMany" | "update" | "findLatestVersionMany">; secretVersionDAL: Pick<TSecretVersionDALFactory, "find" | "insertMany" | "update" | "findLatestVersionMany">;
secretV2BridgeDAL: Pick< secretV2BridgeDAL: Pick<
TSecretV2BridgeDALFactory, TSecretV2BridgeDALFactory,
"find" | "findBySecretKeys" | "insertMany" | "bulkUpdate" | "delete" | "upsertSecretReferences" | "transaction" | "find"
| "findBySecretKeys"
| "insertMany"
| "bulkUpdate"
| "delete"
| "upsertSecretReferences"
| "transaction"
| "cacheInvalidateSecretByProjectId"
>; >;
secretVersionV2BridgeDAL: Pick< secretVersionV2BridgeDAL: Pick<
TSecretVersionV2DALFactory, TSecretVersionV2DALFactory,
@@ -260,6 +267,7 @@ export const secretReplicationServiceFactory = ({
const sourceLocalSecrets = await secretV2BridgeDAL.find({ folderId: folder.id, type: SecretType.Shared }); const sourceLocalSecrets = await secretV2BridgeDAL.find({ folderId: folder.id, type: SecretType.Shared });
const sourceSecretImports = await secretImportDAL.find({ folderId: folder.id }); const sourceSecretImports = await secretImportDAL.find({ folderId: folder.id });
const sourceImportedSecrets = await fnSecretsV2FromImports({ const sourceImportedSecrets = await fnSecretsV2FromImports({
projectId,
secretImports: sourceSecretImports, secretImports: sourceSecretImports,
secretDAL: secretV2BridgeDAL, secretDAL: secretV2BridgeDAL,
folderDAL, folderDAL,
@@ -783,6 +791,7 @@ export const secretReplicationServiceFactory = ({
} }
}); });
await secretV2BridgeDAL.cacheInvalidateSecretByProjectId(projectId);
await secretQueueService.syncSecrets({ await secretQueueService.syncSecrets({
projectId, projectId,
orgId, orgId,
@@ -88,7 +88,7 @@ export type TSecretRotationV2ServiceFactoryDep = {
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findBySecretPathMultiEnv">; folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findBySecretPathMultiEnv">;
secretV2BridgeDAL: Pick< secretV2BridgeDAL: Pick<
TSecretV2BridgeDALFactory, TSecretV2BridgeDALFactory,
"bulkUpdate" | "insertMany" | "deleteMany" | "upsertSecretReferences" | "find" "bulkUpdate" | "insertMany" | "deleteMany" | "upsertSecretReferences" | "find" | "cacheInvalidateSecretByProjectId"
>; >;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany">; secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">; secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
@@ -515,6 +515,7 @@ export const secretRotationV2ServiceFactory = ({
}); });
}); });
await secretV2BridgeDAL.cacheInvalidateSecretByProjectId(projectId);
await snapshotService.performSnapshot(folder.id); await snapshotService.performSnapshot(folder.id);
await secretQueueService.syncSecrets({ await secretQueueService.syncSecrets({
orgId: connection.orgId, orgId: connection.orgId,
@@ -651,6 +652,7 @@ export const secretRotationV2ServiceFactory = ({
}); });
if (secretsMappingUpdated) { if (secretsMappingUpdated) {
await secretV2BridgeDAL.cacheInvalidateSecretByProjectId(projectId);
await snapshotService.performSnapshot(folder.id); await snapshotService.performSnapshot(folder.id);
await secretQueueService.syncSecrets({ await secretQueueService.syncSecrets({
orgId: connection.orgId, orgId: connection.orgId,
@@ -777,6 +779,7 @@ export const secretRotationV2ServiceFactory = ({
} }
if (deleteSecrets) { if (deleteSecrets) {
await secretV2BridgeDAL.cacheInvalidateSecretByProjectId(projectId);
await snapshotService.performSnapshot(folder.id); await snapshotService.performSnapshot(folder.id);
await secretQueueService.syncSecrets({ await secretQueueService.syncSecrets({
orgId: connection.orgId, orgId: connection.orgId,
@@ -935,6 +938,7 @@ export const secretRotationV2ServiceFactory = ({
} }
}); });
await secretV2BridgeDAL.cacheInvalidateSecretByProjectId(projectId);
await snapshotService.performSnapshot(folder.id); await snapshotService.performSnapshot(folder.id);
await secretQueueService.syncSecrets({ await secretQueueService.syncSecrets({
orgId: connection.orgId, orgId: connection.orgId,
+3
View File
@@ -77,6 +77,8 @@ export const keyStoreFactory = (redisUrl: string) => {
const incrementBy = async (key: string, value: number) => redis.incrby(key, value); const incrementBy = async (key: string, value: number) => redis.incrby(key, value);
const setExpiry = async (key: string, expiryInSeconds: number) => redis.expire(key, expiryInSeconds);
const waitTillReady = async ({ const waitTillReady = async ({
key, key,
waitingCb, waitingCb,
@@ -103,6 +105,7 @@ export const keyStoreFactory = (redisUrl: string) => {
return { return {
setItem, setItem,
getItem, getItem,
setExpiry,
setItemWithExpiry, setItemWithExpiry,
deleteItem, deleteItem,
incrementBy, incrementBy,
+1
View File
@@ -10,6 +10,7 @@ export const inMemoryKeyStore = (): TKeyStoreFactory => {
store[key] = value; store[key] = value;
return "OK"; return "OK";
}, },
setExpiry: async () => 0,
setItemWithExpiry: async (key, value) => { setItemWithExpiry: async (key, value) => {
store[key] = value; store[key] = value;
return "OK"; return "OK";
+10
View File
@@ -0,0 +1,10 @@
import crypto from "node:crypto";
export const getCacheKey = (data: unknown) =>
crypto
.createHash("md5")
.update(JSON.stringify(data))
.digest("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
+12
View File
@@ -41,6 +41,18 @@ export type RequiredKeys<T> = {
[K in keyof T]-?: undefined extends T[K] ? never : K; [K in keyof T]-?: undefined extends T[K] ? never : K;
}[keyof T]; }[keyof T];
export type BufferKeysToString<T> = {
[K in keyof T]: T[K] extends Buffer
? string
: T[K] extends Buffer | null
? string | null
: T[K] extends Buffer | undefined
? string | undefined
: T[K] extends Buffer | null | undefined
? string | null | undefined
: T[K];
};
export type PickRequired<T> = Pick<T, RequiredKeys<T>>; export type PickRequired<T> = Pick<T, RequiredKeys<T>>;
export type DiscriminativePick<T, K extends keyof T> = T extends unknown ? Pick<T, K> : never; export type DiscriminativePick<T, K extends keyof T> = T extends unknown ? Pick<T, K> : never;
+1 -1
View File
@@ -315,7 +315,7 @@ export const registerRoutes = async (
const secretVersionTagDAL = secretVersionTagDALFactory(db); const secretVersionTagDAL = secretVersionTagDALFactory(db);
const secretBlindIndexDAL = secretBlindIndexDALFactory(db); const secretBlindIndexDAL = secretBlindIndexDALFactory(db);
const secretV2BridgeDAL = secretV2BridgeDALFactory(db); const secretV2BridgeDAL = secretV2BridgeDALFactory({ db, keyStore });
const secretVersionV2BridgeDAL = secretVersionV2BridgeDALFactory(db); const secretVersionV2BridgeDAL = secretVersionV2BridgeDALFactory(db);
const secretVersionTagV2BridgeDAL = secretVersionV2TagBridgeDALFactory(db); const secretVersionTagV2BridgeDAL = secretVersionV2TagBridgeDALFactory(db);
@@ -50,7 +50,7 @@ const getIntegrationSecretsV2 = async (
} }
// process secrets in current folder // process secrets in current folder
const secrets = await secretV2BridgeDAL.findByFolderId(dto.folderId); const secrets = await secretV2BridgeDAL.findByFolderId({ folderId: dto.folderId, projectId: dto.projectId });
secrets.forEach((secret) => { secrets.forEach((secret) => {
const secretKey = secret.key; const secretKey = secret.key;
@@ -63,6 +63,7 @@ const getIntegrationSecretsV2 = async (
// if no imports then return secrets in the current folder // if no imports then return secrets in the current folder
if (!secretImports.length) return content; if (!secretImports.length) return content;
const importedSecrets = await fnSecretsV2FromImports({ const importedSecrets = await fnSecretsV2FromImports({
projectId: dto.projectId,
decryptor: dto.decryptor, decryptor: dto.decryptor,
folderDAL, folderDAL,
secretDAL: secretV2BridgeDAL, secretDAL: secretV2BridgeDAL,
@@ -159,7 +159,8 @@ export const fnSecretsV2FromImports = async ({
decryptor, decryptor,
expandSecretReferences, expandSecretReferences,
hasSecretAccess, hasSecretAccess,
viewSecretValue viewSecretValue,
projectId
}: { }: {
secretImports: (Omit<TSecretImports, "importEnv"> & { secretImports: (Omit<TSecretImports, "importEnv"> & {
importEnv: { id: string; slug: string; name: string }; importEnv: { id: string; slug: string; name: string };
@@ -176,6 +177,7 @@ export const fnSecretsV2FromImports = async ({
environment: string; environment: string;
}) => Promise<string | undefined>; }) => Promise<string | undefined>;
hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean; hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean;
projectId: string;
}) => { }) => {
const cyclicDetector = new Set(); const cyclicDetector = new Set();
const stack: { const stack: {
@@ -216,7 +218,8 @@ export const fnSecretsV2FromImports = async ({
type: SecretType.Shared type: SecretType.Shared
}, },
{ {
sort: [["id", "asc"]] sort: [["id", "asc"]],
useCache: { projectId }
} }
); );
const importedSecretsGroupByFolderId = groupBy(importedSecrets, (i) => i.folderId); const importedSecretsGroupByFolderId = groupBy(importedSecrets, (i) => i.folderId);
@@ -43,7 +43,7 @@ type TSecretImportServiceFactoryDep = {
secretImportDAL: TSecretImportDALFactory; secretImportDAL: TSecretImportDALFactory;
folderDAL: TSecretFolderDALFactory; folderDAL: TSecretFolderDALFactory;
secretDAL: Pick<TSecretDALFactory, "find">; secretDAL: Pick<TSecretDALFactory, "find">;
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find">; secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find" | "cacheInvalidateSecretByProjectId">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">; projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">; projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
projectEnvDAL: TProjectEnvDALFactory; projectEnvDAL: TProjectEnvDALFactory;
@@ -184,6 +184,7 @@ export const secretImportServiceFactory = ({
}); });
} }
await secretV2BridgeDAL.cacheInvalidateSecretByProjectId(projectId);
return { ...secImport, importEnv }; return { ...secImport, importEnv };
}; };
@@ -281,6 +282,8 @@ export const secretImportServiceFactory = ({
); );
return doc; return doc;
}); });
await secretV2BridgeDAL.cacheInvalidateSecretByProjectId(projectId);
return { ...updatedSecImport, importEnv: importedEnv }; return { ...updatedSecImport, importEnv: importedEnv };
}; };
@@ -355,6 +358,7 @@ export const secretImportServiceFactory = ({
actorId actorId
}); });
await secretV2BridgeDAL.cacheInvalidateSecretByProjectId(projectId);
return secImport; return secImport;
}; };
@@ -693,6 +697,7 @@ export const secretImportServiceFactory = ({
projectId projectId
}); });
const importedSecrets = await fnSecretsV2FromImports({ const importedSecrets = await fnSecretsV2FromImports({
projectId,
secretImports, secretImports,
folderDAL, folderDAL,
viewSecretValue: true, viewSecretValue: true,
@@ -213,7 +213,7 @@ export const secretSyncQueueFactory = ({
canExpandValue: () => true canExpandValue: () => true
}); });
const secrets = await secretV2BridgeDAL.findByFolderId(folderId); const secrets = await secretV2BridgeDAL.findByFolderId({ folderId, projectId });
await Promise.allSettled( await Promise.allSettled(
secrets.map(async (secret) => { secrets.map(async (secret) => {
@@ -243,6 +243,7 @@ export const secretSyncQueueFactory = ({
if (secretImports.length) { if (secretImports.length) {
const importedSecrets = await fnSecretsV2FromImports({ const importedSecrets = await fnSecretsV2FromImports({
projectId,
decryptor: decryptSecretValue, decryptor: decryptSecretValue,
folderDAL, folderDAL,
secretDAL: secretV2BridgeDAL, secretDAL: secretV2BridgeDAL,
@@ -2,7 +2,10 @@ import { Knex } from "knex";
import { validate as uuidValidate } from "uuid"; import { validate as uuidValidate } from "uuid";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { SecretsV2Schema, SecretType, TableName, TSecretsV2, TSecretsV2Update } from "@app/db/schemas"; import { ProjectType, SecretsV2Schema, SecretType, TableName, TSecretsV2, TSecretsV2Update } from "@app/db/schemas";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { getCacheKey } from "@app/lib/crypto/cache";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import { import {
buildFindFilter, buildFindFilter,
@@ -12,15 +15,64 @@ import {
TFindFilter, TFindFilter,
TFindOpt TFindOpt
} from "@app/lib/knex"; } from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types"; import { BufferKeysToString, OrderByDirection } from "@app/lib/types";
import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { SecretsOrderBy } from "@app/services/secret/secret-types";
import { TFindSecretsByFolderIdsFilter } from "@app/services/secret-v2-bridge/secret-v2-bridge-types"; import type { TFindSecretsByFolderIdsFilter } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
export const SecretDalCacheKeys = {
get productKey() {
const { INFISICAL_PLATFORM_VERSION } = getConfig();
return `${ProjectType.SecretManager}:${INFISICAL_PLATFORM_VERSION || 0}`;
},
getSecretDalVersion: (projectId: string) => {
return `${SecretDalCacheKeys.productKey}:${projectId}:${TableName.SecretV2}-dal-version`;
},
findByFolderIds: (
projectId: string,
version: number,
{ useCache, tx, ...cacheKey }: Parameters<TSecretV2BridgeDALFactory["findByFolderIds"]>[0]
) => {
return `${SecretDalCacheKeys.productKey}:${projectId}:${
TableName.SecretV2
}-dal:v${version}:find-by-folder-ids:${getCacheKey(cacheKey)}`;
},
findByFolderId: (
projectId: string,
version: number,
{ useCache, tx, ...cacheKey }: Parameters<TSecretV2BridgeDALFactory["findByFolderId"]>[0]
) => {
return `${SecretDalCacheKeys.productKey}:${projectId}:${
TableName.SecretV2
}-dal:v${version}:find-by-folder-id:${getCacheKey(cacheKey)}`;
},
find: (projectId: string, version: number, ...args: Parameters<TSecretV2BridgeDALFactory["find"]>) => {
const [filter, opts] = args;
delete opts?.tx;
delete opts?.useCache;
return `${SecretDalCacheKeys.productKey}:${projectId}:${TableName.SecretV2}-dal:v${version}:find:${getCacheKey({
filter,
opts
})}`;
}
};
export type TSecretV2BridgeDALFactory = ReturnType<typeof secretV2BridgeDALFactory>; export type TSecretV2BridgeDALFactory = ReturnType<typeof secretV2BridgeDALFactory>;
interface TSecretV2DalArg {
db: TDbClient;
keyStore: TKeyStoreFactory;
}
export const secretV2BridgeDALFactory = (db: TDbClient) => { const SECRET_DAL_TTL = 5 * 60;
const SECRET_DAL_VERSION_TTL = 15 * 60;
export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
const secretOrm = ormify(db, TableName.SecretV2); const secretOrm = ormify(db, TableName.SecretV2);
const cacheInvalidateSecretByProjectId = async (projectId: string) => {
const secretDalVersionKey = SecretDalCacheKeys.getSecretDalVersion(projectId);
await keyStore.incrementBy(secretDalVersionKey, 1);
await keyStore.setExpiry(secretDalVersionKey, SECRET_DAL_VERSION_TTL);
};
const findOne = async (filter: Partial<TSecretsV2>, tx?: Knex) => { const findOne = async (filter: Partial<TSecretsV2>, tx?: Knex) => {
try { try {
const docs = await (tx || db)(TableName.SecretV2) const docs = await (tx || db)(TableName.SecretV2)
@@ -73,8 +125,35 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
} }
}; };
const find = async (filter: TFindFilter<TSecretsV2>, { offset, limit, sort, tx }: TFindOpt<TSecretsV2> = {}) => { const find = async (
filter: TFindFilter<TSecretsV2>,
opts: TFindOpt<TSecretsV2> & { useCache?: { projectId: string } } = {}
) => {
const { offset, limit, sort, tx, useCache } = opts;
try { try {
let secretDalVersion = 0;
if (useCache) {
const cachedSecretDalVersion = await keyStore.getItem(
SecretDalCacheKeys.getSecretDalVersion(useCache.projectId)
);
secretDalVersion = Number(cachedSecretDalVersion || 0);
const cacheKey = SecretDalCacheKeys.find(useCache.projectId, secretDalVersion, filter, opts);
const cachedSecrets = await keyStore.getItem(cacheKey);
if (cachedSecrets) {
await keyStore.setExpiry(cacheKey, SECRET_DAL_TTL);
const unsanitizedSecrets = JSON.parse(cachedSecrets) as BufferKeysToString<(typeof data)[number]>[];
const sanitizedSecrets = unsanitizedSecrets.map((el) => {
const encryptedValue = el.encryptedValue ? Buffer.from(el.encryptedValue, "base64") : null;
const encryptedComment = el.encryptedComment ? Buffer.from(el.encryptedComment, "base64") : null;
const createdAt = new Date(el.createdAt);
const updatedAt = new Date(el.updatedAt);
return { ...el, encryptedComment, encryptedValue, createdAt, updatedAt };
});
return sanitizedSecrets;
}
}
const query = (tx || db)(TableName.SecretV2) const query = (tx || db)(TableName.SecretV2)
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter(filter)) .where(buildFindFilter(filter))
@@ -142,6 +221,20 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
} }
] ]
}); });
if (useCache) {
const cachedSecrets = data.map((el) => {
const encryptedValue = el.encryptedValue ? el.encryptedValue.toString("base64") : null;
const encryptedComment = el.encryptedComment ? el.encryptedComment.toString("base64") : null;
return { ...el, encryptedValue, encryptedComment };
});
await keyStore.setItemWithExpiry(
SecretDalCacheKeys.find(useCache.projectId, secretDalVersion, filter, opts),
SECRET_DAL_TTL,
JSON.stringify(cachedSecrets)
);
}
return data; return data;
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: `${TableName.SecretV2}: Find` }); throw new DatabaseError({ error, name: `${TableName.SecretV2}: Find` });
@@ -246,14 +339,43 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
} }
}; };
const findByFolderId = async (folderId: string, userId?: string, tx?: Knex) => { const findByFolderId = async (dto: {
folderId: string;
userId?: string;
tx?: Knex;
projectId: string;
useCache?: boolean;
}) => {
try { try {
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo) const { folderId, tx, projectId } = dto;
let { userId } = dto;
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo
if (userId && !uuidValidate(userId)) { if (userId && !uuidValidate(userId)) {
// eslint-disable-next-line // eslint-disable-next-line
userId = undefined; userId = undefined;
} }
const cachedSecretDalVersion = await keyStore.getItem(SecretDalCacheKeys.getSecretDalVersion(projectId));
const secretDalVersion = Number(cachedSecretDalVersion || 0);
if (dto.useCache) {
const cacheKey = SecretDalCacheKeys.findByFolderId(projectId, secretDalVersion, dto);
const cachedSecrets = await keyStore.getItem(cacheKey);
if (cachedSecrets) {
await keyStore.setExpiry(cacheKey, SECRET_DAL_TTL);
const unsanitizedSecrets = JSON.parse(cachedSecrets) as BufferKeysToString<(typeof data)[number]>[];
const sanitizedSecrets = unsanitizedSecrets.map((el) => {
const encryptedValue = el.encryptedValue ? Buffer.from(el.encryptedValue, "base64") : null;
const encryptedComment = el.encryptedComment ? Buffer.from(el.encryptedComment, "base64") : null;
const createdAt = new Date(el.createdAt);
const updatedAt = new Date(el.updatedAt);
return { ...el, encryptedComment, encryptedValue, createdAt, updatedAt };
});
return sanitizedSecrets;
}
}
const secs = await (tx || db.replicaNode())(TableName.SecretV2) const secs = await (tx || db.replicaNode())(TableName.SecretV2)
.where({ folderId }) .where({ folderId })
.where((bd) => { .where((bd) => {
@@ -309,6 +431,18 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
} }
] ]
}); });
if (dto.useCache) {
const newCachedSecrets = data.map((el) => {
const encryptedValue = el.encryptedValue ? el.encryptedValue.toString("base64") : null;
const encryptedComment = el.encryptedComment ? el.encryptedComment.toString("base64") : null;
return { ...el, encryptedValue, encryptedComment };
});
await keyStore.setItemWithExpiry(
SecretDalCacheKeys.findByFolderId(projectId, secretDalVersion, dto),
SECRET_DAL_TTL,
JSON.stringify(newCachedSecrets)
);
}
return data; return data;
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "get all secret" }); throw new DatabaseError({ error, name: "get all secret" });
@@ -394,12 +528,16 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
} }
}; };
const findByFolderIds = async ( const findByFolderIds = async (dto: {
folderIds: string[], folderIds: string[];
userId?: string, userId?: string;
tx?: Knex, tx?: Knex;
filters?: TFindSecretsByFolderIdsFilter projectId: string;
) => { filters?: TFindSecretsByFolderIdsFilter;
useCache?: boolean;
}) => {
const { folderIds, tx, filters, useCache, projectId } = dto;
let { userId } = dto;
try { try {
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo) // check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo)
if (userId && !uuidValidate(userId)) { if (userId && !uuidValidate(userId)) {
@@ -407,6 +545,26 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
userId = undefined; userId = undefined;
} }
const cachedSecretDalVersion = await keyStore.getItem(SecretDalCacheKeys.getSecretDalVersion(projectId));
const secretDalVersion = Number(cachedSecretDalVersion || 0);
if (useCache) {
const cacheKey = SecretDalCacheKeys.findByFolderIds(projectId, secretDalVersion, dto);
const cachedSecrets = await keyStore.getItem(cacheKey);
if (cachedSecrets) {
await keyStore.setExpiry(cacheKey, SECRET_DAL_TTL);
const unsanitizedSecrets = JSON.parse(cachedSecrets) as BufferKeysToString<(typeof data)[number]>[];
const sanitizedSecrets = unsanitizedSecrets.map((el) => {
const encryptedValue = el.encryptedValue ? Buffer.from(el.encryptedValue, "base64") : null;
const encryptedComment = el.encryptedComment ? Buffer.from(el.encryptedComment, "base64") : null;
const createdAt = new Date(el.createdAt);
const updatedAt = new Date(el.updatedAt);
return { ...el, encryptedComment, encryptedValue, createdAt, updatedAt };
});
return sanitizedSecrets;
}
}
const query = (tx || db.replicaNode())(TableName.SecretV2) const query = (tx || db.replicaNode())(TableName.SecretV2)
.whereIn(`${TableName.SecretV2}.folderId`, folderIds) .whereIn(`${TableName.SecretV2}.folderId`, folderIds)
.where((bd) => { .where((bd) => {
@@ -532,6 +690,18 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
} }
] ]
}); });
if (useCache) {
const cachedSecrets = data.map((el) => {
const encryptedValue = el.encryptedValue ? el.encryptedValue.toString("base64") : null;
const encryptedComment = el.encryptedComment ? el.encryptedComment.toString("base64") : null;
return { ...el, encryptedValue, encryptedComment };
});
await keyStore.setItemWithExpiry(
SecretDalCacheKeys.findByFolderIds(projectId, secretDalVersion, dto),
SECRET_DAL_TTL,
JSON.stringify(cachedSecrets)
);
}
return data; return data;
} catch (error) { } catch (error) {
@@ -724,6 +894,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
findAllProjectSecretValues, findAllProjectSecretValues,
countByFolderIds, countByFolderIds,
findOne, findOne,
find find,
cacheInvalidateSecretByProjectId
}; };
}; };
@@ -501,7 +501,7 @@ export const expandSecretReferencesFactory = ({
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) return { value: "", tags: [] }; if (!folder) return { value: "", tags: [] };
const secrets = await secretDAL.findByFolderId(folder.id); const secrets = await secretDAL.findByFolderId({ folderId: folder.id, projectId, useCache: true });
const decryptedSecret = secrets.reduce<Record<string, { value: string; tags: string[] }>>((prev, secret) => { const decryptedSecret = secrets.reduce<Record<string, { value: string; tags: string[] }>>((prev, secret) => {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
@@ -331,6 +331,7 @@ export const secretV2BridgeServiceFactory = ({
return createdSecret; return createdSecret;
}); });
await secretDAL.cacheInvalidateSecretByProjectId(projectId);
if (inputSecret.type === SecretType.Shared) { if (inputSecret.type === SecretType.Shared) {
await snapshotService.performSnapshot(folderId); await snapshotService.performSnapshot(folderId);
await secretQueueService.syncSecrets({ await secretQueueService.syncSecrets({
@@ -539,6 +540,7 @@ export const secretV2BridgeServiceFactory = ({
projectId projectId
}); });
await secretDAL.cacheInvalidateSecretByProjectId(projectId);
if (inputSecret.type === SecretType.Shared) { if (inputSecret.type === SecretType.Shared) {
await snapshotService.performSnapshot(folderId); await snapshotService.performSnapshot(folderId);
await secretQueueService.syncSecrets({ await secretQueueService.syncSecrets({
@@ -647,6 +649,7 @@ export const secretV2BridgeServiceFactory = ({
}) })
); );
await secretDAL.cacheInvalidateSecretByProjectId(projectId);
if (inputSecret.type === SecretType.Shared) { if (inputSecret.type === SecretType.Shared) {
await snapshotService.performSnapshot(folderId); await snapshotService.performSnapshot(folderId);
await secretQueueService.syncSecrets({ await secretQueueService.syncSecrets({
@@ -796,12 +799,14 @@ export const secretV2BridgeServiceFactory = ({
) => { ) => {
const groupedFolderMappings = groupBy(folderMappings, (folderMapping) => folderMapping.folderId); const groupedFolderMappings = groupBy(folderMappings, (folderMapping) => folderMapping.folderId);
const secrets = await secretDAL.findByFolderIds( const secrets = await secretDAL.findByFolderIds({
folderMappings.map((folderMapping) => folderMapping.folderId), projectId,
folderIds: folderMappings.map((folderMapping) => folderMapping.folderId),
userId, userId,
undefined, tx: undefined,
filters filters,
); useCache: true
});
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager, type: KmsDataKey.SecretManager,
@@ -952,12 +957,14 @@ export const secretV2BridgeServiceFactory = ({
const groupedPaths = groupBy(paths, (p) => p.folderId); const groupedPaths = groupBy(paths, (p) => p.folderId);
const secrets = await secretDAL.findByFolderIds( const secrets = await secretDAL.findByFolderIds({
paths.map((p) => p.folderId), projectId,
actorId, folderIds: paths.map((p) => p.folderId),
undefined, userId: actorId,
params tx: undefined,
); filters: params,
useCache: true
});
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager, type: KmsDataKey.SecretManager,
@@ -1087,6 +1094,7 @@ export const secretV2BridgeServiceFactory = ({
const secretImports = await secretImportDAL.findByFolderIds(paths.map((p) => p.folderId)); const secretImports = await secretImportDAL.findByFolderIds(paths.map((p) => p.folderId));
const allowedImports = secretImports.filter(({ isReplication }) => !isReplication); const allowedImports = secretImports.filter(({ isReplication }) => !isReplication);
const importedSecrets = await fnSecretsV2FromImports({ const importedSecrets = await fnSecretsV2FromImports({
projectId,
viewSecretValue, viewSecretValue,
secretImports: allowedImports, secretImports: allowedImports,
secretDAL, secretDAL,
@@ -1304,6 +1312,7 @@ export const secretV2BridgeServiceFactory = ({
if (!secret && includeImports) { if (!secret && includeImports) {
const secretImports = await secretImportDAL.find({ folderId, isReplication: false }); const secretImports = await secretImportDAL.find({ folderId, isReplication: false });
const importedSecrets = await fnSecretsV2FromImports({ const importedSecrets = await fnSecretsV2FromImports({
projectId,
secretImports, secretImports,
viewSecretValue, viewSecretValue,
secretDAL, secretDAL,
@@ -1543,7 +1552,7 @@ export const secretV2BridgeServiceFactory = ({
tx tx
}) })
); );
await secretDAL.cacheInvalidateSecretByProjectId(projectId);
await snapshotService.performSnapshot(folderId); await snapshotService.performSnapshot(folderId);
await secretQueueService.syncSecrets({ await secretQueueService.syncSecrets({
actor, actor,
@@ -1883,6 +1892,7 @@ export const secretV2BridgeServiceFactory = ({
} }
}); });
await secretDAL.cacheInvalidateSecretByProjectId(projectId);
await Promise.allSettled(folders.map((el) => (el?.id ? snapshotService.performSnapshot(el.id) : undefined))); await Promise.allSettled(folders.map((el) => (el?.id ? snapshotService.performSnapshot(el.id) : undefined)));
await Promise.allSettled( await Promise.allSettled(
folders.map((el) => folders.map((el) =>
@@ -2014,6 +2024,7 @@ export const secretV2BridgeServiceFactory = ({
}) })
); );
await secretDAL.cacheInvalidateSecretByProjectId(projectId);
await snapshotService.performSnapshot(folderId); await snapshotService.performSnapshot(folderId);
await secretQueueService.syncSecrets({ await secretQueueService.syncSecrets({
actor, actor,
@@ -2537,6 +2548,9 @@ export const secretV2BridgeServiceFactory = ({
} }
}); });
if (isDestinationUpdated || isSourceUpdated) {
await secretDAL.cacheInvalidateSecretByProjectId(projectId);
}
if (isDestinationUpdated) { if (isDestinationUpdated) {
await snapshotService.performSnapshot(destinationFolder.id); await snapshotService.performSnapshot(destinationFolder.id);
await secretQueueService.syncSecrets({ await secretQueueService.syncSecrets({
@@ -2715,7 +2729,7 @@ export const secretV2BridgeServiceFactory = ({
generatePaths(folderMap).map(({ folderId, path }) => [folderId, path === "/" ? path : path.substring(1)]) generatePaths(folderMap).map(({ folderId, path }) => [folderId, path === "/" ? path : path.substring(1)])
); );
const secrets = await secretDAL.findByFolderIds(folders.map((f) => f.id)); const secrets = await secretDAL.findByFolderIds({ folderIds: folders.map((f) => f.id), projectId });
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager, type: KmsDataKey.SecretManager,
@@ -279,6 +279,13 @@ export type TUpdateManySecretsFnFactory = {
folderDAL: TSecretFolderDALFactory; folderDAL: TSecretFolderDALFactory;
}; };
export type TFindByFolderIdDALDTO = {
folderId: string;
userId?: string;
tx?: Knex;
projectId: string;
};
export type TUpdateManySecretsFn = { export type TUpdateManySecretsFn = {
projectId: string; projectId: string;
environment: string; environment: string;
+2 -1
View File
@@ -367,7 +367,7 @@ export const secretQueueFactory = ({
canExpandValue: () => true canExpandValue: () => true
}); });
// process secrets in current folder // process secrets in current folder
const secrets = await secretV2BridgeDAL.findByFolderId(dto.folderId); const secrets = await secretV2BridgeDAL.findByFolderId({ folderId: dto.folderId, projectId: dto.projectId });
await Promise.allSettled( await Promise.allSettled(
secrets.map(async (secret) => { secrets.map(async (secret) => {
@@ -397,6 +397,7 @@ export const secretQueueFactory = ({
// if no imports then return secrets in the current folder // if no imports then return secrets in the current folder
if (!secretImports.length) return content; if (!secretImports.length) return content;
const importedSecrets = await fnSecretsV2FromImports({ const importedSecrets = await fnSecretsV2FromImports({
projectId: dto.projectId,
decryptor: dto.decryptor, decryptor: dto.decryptor,
folderDAL, folderDAL,
secretDAL: secretV2BridgeDAL, secretDAL: secretV2BridgeDAL,