mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 09:27:50 +00:00
Merge pull request #1643 from akhilmhdh/feat/import-sync-secret
fix(server): added sync secret for imports and added check for avoid cyclic import
This commit is contained in:
@@ -46,7 +46,7 @@ const deleteSecretImport = async (id: string) => {
|
|||||||
|
|
||||||
describe("Secret Import Router", async () => {
|
describe("Secret Import Router", async () => {
|
||||||
test.each([
|
test.each([
|
||||||
{ importEnv: "dev", importPath: "/" }, // one in root
|
{ importEnv: "prod", importPath: "/" }, // one in root
|
||||||
{ importEnv: "staging", importPath: "/" } // then create a deep one creating intermediate ones
|
{ importEnv: "staging", importPath: "/" } // then create a deep one creating intermediate ones
|
||||||
])("Create secret import $importEnv with path $importPath", async ({ importPath, importEnv }) => {
|
])("Create secret import $importEnv with path $importPath", async ({ importPath, importEnv }) => {
|
||||||
// check for default environments
|
// check for default environments
|
||||||
@@ -66,7 +66,7 @@ describe("Secret Import Router", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Get secret imports", async () => {
|
test("Get secret imports", async () => {
|
||||||
const createdImport1 = await createSecretImport("/", "dev");
|
const createdImport1 = await createSecretImport("/", "prod");
|
||||||
const createdImport2 = await createSecretImport("/", "staging");
|
const createdImport2 = await createSecretImport("/", "staging");
|
||||||
const res = await testServer.inject({
|
const res = await testServer.inject({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -103,10 +103,10 @@ describe("Secret Import Router", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Update secret import position", async () => {
|
test("Update secret import position", async () => {
|
||||||
const devImportDetails = { path: "/", envSlug: "dev" };
|
const prodImportDetails = { path: "/", envSlug: "prod" };
|
||||||
const stagingImportDetails = { path: "/", envSlug: "staging" };
|
const stagingImportDetails = { path: "/", envSlug: "staging" };
|
||||||
|
|
||||||
const createdImport1 = await createSecretImport(devImportDetails.path, devImportDetails.envSlug);
|
const createdImport1 = await createSecretImport(prodImportDetails.path, prodImportDetails.envSlug);
|
||||||
const createdImport2 = await createSecretImport(stagingImportDetails.path, stagingImportDetails.envSlug);
|
const createdImport2 = await createSecretImport(stagingImportDetails.path, stagingImportDetails.envSlug);
|
||||||
|
|
||||||
const updateImportRes = await testServer.inject({
|
const updateImportRes = await testServer.inject({
|
||||||
@@ -136,7 +136,7 @@ describe("Secret Import Router", async () => {
|
|||||||
position: 2,
|
position: 2,
|
||||||
importEnv: expect.objectContaining({
|
importEnv: expect.objectContaining({
|
||||||
name: expect.any(String),
|
name: expect.any(String),
|
||||||
slug: expect.stringMatching(devImportDetails.envSlug),
|
slug: expect.stringMatching(prodImportDetails.envSlug),
|
||||||
id: expect.any(String)
|
id: expect.any(String)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -166,7 +166,7 @@ describe("Secret Import Router", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Delete secret import position", async () => {
|
test("Delete secret import position", async () => {
|
||||||
const createdImport1 = await createSecretImport("/", "dev");
|
const createdImport1 = await createSecretImport("/", "prod");
|
||||||
const createdImport2 = await createSecretImport("/", "staging");
|
const createdImport2 = await createSecretImport("/", "staging");
|
||||||
const deletedImport = await deleteSecretImport(createdImport1.id);
|
const deletedImport = await deleteSecretImport(createdImport1.id);
|
||||||
// check for default environments
|
// check for default environments
|
||||||
|
|||||||
@@ -61,11 +61,11 @@ export type TQueueJobTypes = {
|
|||||||
};
|
};
|
||||||
[QueueName.SecretWebhook]: {
|
[QueueName.SecretWebhook]: {
|
||||||
name: QueueJobs.SecWebhook;
|
name: QueueJobs.SecWebhook;
|
||||||
payload: { projectId: string; environment: string; secretPath: string };
|
payload: { projectId: string; environment: string; secretPath: string; depth?: number };
|
||||||
};
|
};
|
||||||
[QueueName.IntegrationSync]: {
|
[QueueName.IntegrationSync]: {
|
||||||
name: QueueJobs.IntegrationSync;
|
name: QueueJobs.IntegrationSync;
|
||||||
payload: { projectId: string; environment: string; secretPath: string };
|
payload: { projectId: string; environment: string; secretPath: string; depth?: number };
|
||||||
};
|
};
|
||||||
[QueueName.SecretFullRepoScan]: {
|
[QueueName.SecretFullRepoScan]: {
|
||||||
name: QueueJobs.SecretScan;
|
name: QueueJobs.SecretScan;
|
||||||
|
|||||||
@@ -445,14 +445,6 @@ export const registerRoutes = async (
|
|||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
snapshotService
|
snapshotService
|
||||||
});
|
});
|
||||||
const secretImportService = secretImportServiceFactory({
|
|
||||||
projectEnvDAL,
|
|
||||||
folderDAL,
|
|
||||||
permissionService,
|
|
||||||
secretImportDAL,
|
|
||||||
projectDAL,
|
|
||||||
secretDAL
|
|
||||||
});
|
|
||||||
const integrationAuthService = integrationAuthServiceFactory({
|
const integrationAuthService = integrationAuthServiceFactory({
|
||||||
integrationAuthDAL,
|
integrationAuthDAL,
|
||||||
integrationDAL,
|
integrationDAL,
|
||||||
@@ -480,6 +472,15 @@ export const registerRoutes = async (
|
|||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL
|
secretVersionTagDAL
|
||||||
});
|
});
|
||||||
|
const secretImportService = secretImportServiceFactory({
|
||||||
|
projectEnvDAL,
|
||||||
|
folderDAL,
|
||||||
|
permissionService,
|
||||||
|
secretImportDAL,
|
||||||
|
projectDAL,
|
||||||
|
secretDAL,
|
||||||
|
secretQueueService
|
||||||
|
});
|
||||||
const secretBlindIndexService = secretBlindIndexServiceFactory({
|
const secretBlindIndexService = secretBlindIndexServiceFactory({
|
||||||
permissionService,
|
permissionService,
|
||||||
secretDAL,
|
secretDAL,
|
||||||
|
|||||||
@@ -170,7 +170,8 @@ const sqlFindSecretPathByFolderId = (db: Knex, projectId: string, folderIds: str
|
|||||||
// if the given folder id is root folder id then intial path is set as / instead of /root
|
// if the given folder id is root folder id then intial path is set as / instead of /root
|
||||||
// if not root folder the path here will be /<folder name>
|
// if not root folder the path here will be /<folder name>
|
||||||
path: db.raw(`CONCAT('/', (CASE WHEN "parentId" is NULL THEN '' ELSE ${TableName.SecretFolder}.name END))`),
|
path: db.raw(`CONCAT('/', (CASE WHEN "parentId" is NULL THEN '' ELSE ${TableName.SecretFolder}.name END))`),
|
||||||
child: db.raw("NULL::uuid")
|
child: db.raw("NULL::uuid"),
|
||||||
|
environmentSlug: `${TableName.Environment}.slug`
|
||||||
})
|
})
|
||||||
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||||
.where({ projectId })
|
.where({ projectId })
|
||||||
@@ -190,14 +191,15 @@ const sqlFindSecretPathByFolderId = (db: Knex, projectId: string, folderIds: str
|
|||||||
ELSE CONCAT('/', secret_folders.name)
|
ELSE CONCAT('/', secret_folders.name)
|
||||||
END, parent.path )`
|
END, parent.path )`
|
||||||
),
|
),
|
||||||
child: db.raw("COALESCE(parent.child, parent.id)")
|
child: db.raw("COALESCE(parent.child, parent.id)"),
|
||||||
|
environmentSlug: "parent.environmentSlug"
|
||||||
})
|
})
|
||||||
.from(TableName.SecretFolder)
|
.from(TableName.SecretFolder)
|
||||||
.join("parent", "parent.parentId", `${TableName.SecretFolder}.id`)
|
.join("parent", "parent.parentId", `${TableName.SecretFolder}.id`)
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.select("*")
|
.select("*")
|
||||||
.from<TSecretFolders & { child: string | null; path: string }>("parent");
|
.from<TSecretFolders & { child: string | null; path: string; environmentSlug: string }>("parent");
|
||||||
|
|
||||||
export type TSecretFolderDALFactory = ReturnType<typeof secretFolderDALFactory>;
|
export type TSecretFolderDALFactory = ReturnType<typeof secretFolderDALFactory>;
|
||||||
// never change this. If u do write a migration for it
|
// never change this. If u do write a migration for it
|
||||||
@@ -257,10 +259,12 @@ export const secretFolderDALFactory = (db: TDbClient) => {
|
|||||||
const findSecretPathByFolderIds = async (projectId: string, folderIds: string[], tx?: Knex) => {
|
const findSecretPathByFolderIds = async (projectId: string, folderIds: string[], tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const folders = await sqlFindSecretPathByFolderId(tx || db, projectId, folderIds);
|
const folders = await sqlFindSecretPathByFolderId(tx || db, projectId, folderIds);
|
||||||
|
|
||||||
const rootFolders = groupBy(
|
const rootFolders = groupBy(
|
||||||
folders.filter(({ parentId }) => parentId === null),
|
folders.filter(({ parentId }) => parentId === null),
|
||||||
(i) => i.child || i.id // root condition then child and parent will null
|
(i) => i.child || i.id // root condition then child and parent will null
|
||||||
);
|
);
|
||||||
|
|
||||||
return folderIds.map((folderId) => rootFolders[folderId]?.[0]);
|
return folderIds.map((folderId) => rootFolders[folderId]?.[0]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Find by secret path" });
|
throw new DatabaseError({ error, name: "Find by secret path" });
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const find = async (filter: Partial<TSecretImports>, tx?: Knex) => {
|
const find = async (filter: Partial<TSecretImports & { projectId: string }>, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const docs = await (tx || db)(TableName.SecretImport)
|
const docs = await (tx || db)(TableName.SecretImport)
|
||||||
.where(filter)
|
.where(filter)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { BadRequestError } from "@app/lib/errors";
|
|||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { TSecretDALFactory } from "../secret/secret-dal";
|
import { TSecretDALFactory } from "../secret/secret-dal";
|
||||||
|
import { TSecretQueueFactory } from "../secret/secret-queue";
|
||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
import { TSecretImportDALFactory } from "./secret-import-dal";
|
import { TSecretImportDALFactory } from "./secret-import-dal";
|
||||||
import { fnSecretsFromImports } from "./secret-import-fns";
|
import { fnSecretsFromImports } from "./secret-import-fns";
|
||||||
@@ -25,6 +26,7 @@ type TSecretImportServiceFactoryDep = {
|
|||||||
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
|
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
|
||||||
projectEnvDAL: TProjectEnvDALFactory;
|
projectEnvDAL: TProjectEnvDALFactory;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets">;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ERR_SEC_IMP_NOT_FOUND = new BadRequestError({ message: "Secret import not found" });
|
const ERR_SEC_IMP_NOT_FOUND = new BadRequestError({ message: "Secret import not found" });
|
||||||
@@ -37,7 +39,8 @@ export const secretImportServiceFactory = ({
|
|||||||
permissionService,
|
permissionService,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
secretDAL
|
secretDAL,
|
||||||
|
secretQueueService
|
||||||
}: TSecretImportServiceFactoryDep) => {
|
}: TSecretImportServiceFactoryDep) => {
|
||||||
const createImport = async ({
|
const createImport = async ({
|
||||||
environment,
|
environment,
|
||||||
@@ -77,10 +80,19 @@ export const secretImportServiceFactory = ({
|
|||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||||
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create import" });
|
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create import" });
|
||||||
|
|
||||||
// TODO(akhilmhdh-pg): updated permission check add here
|
|
||||||
const [importEnv] = await projectEnvDAL.findBySlugs(projectId, [data.environment]);
|
const [importEnv] = await projectEnvDAL.findBySlugs(projectId, [data.environment]);
|
||||||
if (!importEnv) throw new BadRequestError({ error: "Imported env not found", name: "Create import" });
|
if (!importEnv) throw new BadRequestError({ error: "Imported env not found", name: "Create import" });
|
||||||
|
|
||||||
|
const sourceFolder = await folderDAL.findBySecretPath(projectId, data.environment, data.path);
|
||||||
|
if (sourceFolder) {
|
||||||
|
const existingImport = await secretImportDAL.findOne({
|
||||||
|
folderId: sourceFolder.id,
|
||||||
|
importEnv: folder.environment.id,
|
||||||
|
importPath: path
|
||||||
|
});
|
||||||
|
if (existingImport) throw new BadRequestError({ message: "Cyclic import not allowed" });
|
||||||
|
}
|
||||||
|
|
||||||
const secImport = await secretImportDAL.transaction(async (tx) => {
|
const secImport = await secretImportDAL.transaction(async (tx) => {
|
||||||
const lastPos = await secretImportDAL.findLastImportPosition(folder.id, tx);
|
const lastPos = await secretImportDAL.findLastImportPosition(folder.id, tx);
|
||||||
return secretImportDAL.create(
|
return secretImportDAL.create(
|
||||||
@@ -94,6 +106,12 @@ export const secretImportServiceFactory = ({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await secretQueueService.syncSecrets({
|
||||||
|
secretPath: secImport.importPath,
|
||||||
|
projectId,
|
||||||
|
environment: importEnv.slug
|
||||||
|
});
|
||||||
|
|
||||||
return { ...secImport, importEnv };
|
return { ...secImport, importEnv };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -131,6 +149,20 @@ export const secretImportServiceFactory = ({
|
|||||||
: await projectEnvDAL.findById(secImpDoc.importEnv);
|
: await projectEnvDAL.findById(secImpDoc.importEnv);
|
||||||
if (!importedEnv) throw new BadRequestError({ error: "Imported env not found", name: "Create import" });
|
if (!importedEnv) throw new BadRequestError({ error: "Imported env not found", name: "Create import" });
|
||||||
|
|
||||||
|
const sourceFolder = await folderDAL.findBySecretPath(
|
||||||
|
projectId,
|
||||||
|
importedEnv.slug,
|
||||||
|
data.path || secImpDoc.importPath
|
||||||
|
);
|
||||||
|
if (sourceFolder) {
|
||||||
|
const existingImport = await secretImportDAL.findOne({
|
||||||
|
folderId: sourceFolder.id,
|
||||||
|
importEnv: folder.environment.id,
|
||||||
|
importPath: path
|
||||||
|
});
|
||||||
|
if (existingImport) throw new BadRequestError({ message: "Cyclic import not allowed" });
|
||||||
|
}
|
||||||
|
|
||||||
const updatedSecImport = await secretImportDAL.transaction(async (tx) => {
|
const updatedSecImport = await secretImportDAL.transaction(async (tx) => {
|
||||||
const secImp = await secretImportDAL.findOne({ folderId: folder.id, id });
|
const secImp = await secretImportDAL.findOne({ folderId: folder.id, id });
|
||||||
if (!secImp) throw ERR_SEC_IMP_NOT_FOUND;
|
if (!secImp) throw ERR_SEC_IMP_NOT_FOUND;
|
||||||
@@ -185,6 +217,13 @@ export const secretImportServiceFactory = ({
|
|||||||
if (!importEnv) throw new BadRequestError({ error: "Imported env not found", name: "Create import" });
|
if (!importEnv) throw new BadRequestError({ error: "Imported env not found", name: "Create import" });
|
||||||
return { ...doc, importEnv };
|
return { ...doc, importEnv };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await secretQueueService.syncSecrets({
|
||||||
|
secretPath: path,
|
||||||
|
projectId,
|
||||||
|
environment
|
||||||
|
});
|
||||||
|
|
||||||
return secImport;
|
return secImport;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { getConfig } from "@app/lib/config/env";
|
|||||||
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||||
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
|
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { isSamePath } from "@app/lib/fn";
|
import { groupBy, isSamePath, unique } from "@app/lib/fn";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||||
@@ -23,7 +23,6 @@ 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 { fnSecretsFromImports } from "../secret-import/secret-import-fns";
|
|
||||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
import { TWebhookDALFactory } from "../webhook/webhook-dal";
|
import { TWebhookDALFactory } from "../webhook/webhook-dal";
|
||||||
import { fnTriggerWebhook } from "../webhook/webhook-fns";
|
import { fnTriggerWebhook } from "../webhook/webhook-fns";
|
||||||
@@ -32,7 +31,6 @@ import { interpolateSecrets } from "./secret-fns";
|
|||||||
import { TCreateSecretReminderDTO, THandleReminderDTO, TRemoveSecretReminderDTO } from "./secret-types";
|
import { TCreateSecretReminderDTO, THandleReminderDTO, TRemoveSecretReminderDTO } from "./secret-types";
|
||||||
|
|
||||||
export type TSecretQueueFactory = ReturnType<typeof secretQueueFactory>;
|
export type TSecretQueueFactory = ReturnType<typeof secretQueueFactory>;
|
||||||
|
|
||||||
type TSecretQueueFactoryDep = {
|
type TSecretQueueFactoryDep = {
|
||||||
queueService: TQueueServiceFactory;
|
queueService: TQueueServiceFactory;
|
||||||
integrationDAL: Pick<TIntegrationDALFactory, "findByProjectIdV2" | "updateById">;
|
integrationDAL: Pick<TIntegrationDALFactory, "findByProjectIdV2" | "updateById">;
|
||||||
@@ -60,6 +58,8 @@ export type TGetSecrets = {
|
|||||||
environment: string;
|
environment: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MAX_SYNC_SECRET_DEPTH = 5;
|
||||||
|
|
||||||
export const secretQueueFactory = ({
|
export const secretQueueFactory = ({
|
||||||
queueService,
|
queueService,
|
||||||
integrationDAL,
|
integrationDAL,
|
||||||
@@ -117,7 +117,10 @@ export const secretQueueFactory = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const syncSecrets = async (dto: TGetSecrets) => {
|
const syncSecrets = async (dto: TGetSecrets & { depth?: number }) => {
|
||||||
|
logger.info(
|
||||||
|
`syncSecrets: syncing project secrets where [projectId=${dto.projectId}] [environment=${dto.environment}] [path=${dto.secretPath}]`
|
||||||
|
);
|
||||||
await queueService.queue(QueueName.SecretWebhook, QueueJobs.SecWebhook, dto, {
|
await queueService.queue(QueueName.SecretWebhook, QueueJobs.SecWebhook, dto, {
|
||||||
jobId: `secret-webhook-${dto.environment}-${dto.projectId}-${dto.secretPath}`,
|
jobId: `secret-webhook-${dto.environment}-${dto.projectId}-${dto.secretPath}`,
|
||||||
removeOnFail: { count: 5 },
|
removeOnFail: { count: 5 },
|
||||||
@@ -227,62 +230,42 @@ export const secretQueueFactory = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getIntegrationSecrets = async (dto: TGetSecrets & { folderId: string }, key: string) => {
|
type Content = Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean }>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the secrets in a given [folderId] including secrets from
|
||||||
|
* nested imported folders recursively.
|
||||||
|
*/
|
||||||
|
const getIntegrationSecrets = async (dto: {
|
||||||
|
projectId: string;
|
||||||
|
environment: string;
|
||||||
|
folderId: string;
|
||||||
|
key: string;
|
||||||
|
depth: number;
|
||||||
|
}) => {
|
||||||
|
let content: Content = {};
|
||||||
|
if (dto.depth > MAX_SYNC_SECRET_DEPTH) {
|
||||||
|
logger.info(
|
||||||
|
`getIntegrationSecrets: secret depth exceeded for [projectId=${dto.projectId}] [folderId=${dto.folderId}] [depth=${dto.depth}]`
|
||||||
|
);
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// process secrets in current folder
|
||||||
const secrets = await secretDAL.findByFolderId(dto.folderId);
|
const secrets = await secretDAL.findByFolderId(dto.folderId);
|
||||||
|
|
||||||
// get imported secrets
|
|
||||||
const secretImport = await secretImportDAL.find({ folderId: dto.folderId });
|
|
||||||
const importedSecrets = await fnSecretsFromImports({
|
|
||||||
allowedImports: secretImport,
|
|
||||||
secretDAL,
|
|
||||||
folderDAL
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!secrets.length && !importedSecrets.length) return {};
|
|
||||||
|
|
||||||
const content: Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean }> = {};
|
|
||||||
|
|
||||||
importedSecrets.forEach(({ secrets: secs }) => {
|
|
||||||
secs.forEach((secret) => {
|
|
||||||
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
|
||||||
ciphertext: secret.secretKeyCiphertext,
|
|
||||||
iv: secret.secretKeyIV,
|
|
||||||
tag: secret.secretKeyTag,
|
|
||||||
key
|
|
||||||
});
|
|
||||||
const secretValue = decryptSymmetric128BitHexKeyUTF8({
|
|
||||||
ciphertext: secret.secretValueCiphertext,
|
|
||||||
iv: secret.secretValueIV,
|
|
||||||
tag: secret.secretValueTag,
|
|
||||||
key
|
|
||||||
});
|
|
||||||
content[secretKey] = { value: secretValue };
|
|
||||||
content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
|
|
||||||
|
|
||||||
if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
|
|
||||||
const commentValue = decryptSymmetric128BitHexKeyUTF8({
|
|
||||||
ciphertext: secret.secretCommentCiphertext,
|
|
||||||
iv: secret.secretCommentIV,
|
|
||||||
tag: secret.secretCommentTag,
|
|
||||||
key
|
|
||||||
});
|
|
||||||
content[secretKey].comment = commentValue;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
secrets.forEach((secret) => {
|
secrets.forEach((secret) => {
|
||||||
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
||||||
ciphertext: secret.secretKeyCiphertext,
|
ciphertext: secret.secretKeyCiphertext,
|
||||||
iv: secret.secretKeyIV,
|
iv: secret.secretKeyIV,
|
||||||
tag: secret.secretKeyTag,
|
tag: secret.secretKeyTag,
|
||||||
key
|
key: dto.key
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretValue = decryptSymmetric128BitHexKeyUTF8({
|
const secretValue = decryptSymmetric128BitHexKeyUTF8({
|
||||||
ciphertext: secret.secretValueCiphertext,
|
ciphertext: secret.secretValueCiphertext,
|
||||||
iv: secret.secretValueIV,
|
iv: secret.secretValueIV,
|
||||||
tag: secret.secretValueTag,
|
tag: secret.secretValueTag,
|
||||||
key
|
key: dto.key
|
||||||
});
|
});
|
||||||
|
|
||||||
content[secretKey] = { value: secretValue };
|
content[secretKey] = { value: secretValue };
|
||||||
@@ -292,38 +275,111 @@ export const secretQueueFactory = ({
|
|||||||
ciphertext: secret.secretCommentCiphertext,
|
ciphertext: secret.secretCommentCiphertext,
|
||||||
iv: secret.secretCommentIV,
|
iv: secret.secretCommentIV,
|
||||||
tag: secret.secretCommentTag,
|
tag: secret.secretCommentTag,
|
||||||
key
|
key: dto.key
|
||||||
});
|
});
|
||||||
content[secretKey].comment = commentValue;
|
content[secretKey].comment = commentValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
|
content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
|
||||||
});
|
});
|
||||||
|
|
||||||
const expandSecrets = interpolateSecrets({
|
const expandSecrets = interpolateSecrets({
|
||||||
projectId: dto.projectId,
|
projectId: dto.projectId,
|
||||||
secretEncKey: key,
|
secretEncKey: dto.key,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
secretDAL
|
secretDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
await expandSecrets(content);
|
await expandSecrets(content);
|
||||||
|
|
||||||
|
// check if current folder has any imports from other folders
|
||||||
|
const secretImport = await secretImportDAL.find({ folderId: dto.folderId });
|
||||||
|
|
||||||
|
// if no imports then return secrets in the current folder
|
||||||
|
if (!secretImport) return content;
|
||||||
|
|
||||||
|
const importedFolders = await folderDAL.findByManySecretPath(
|
||||||
|
secretImport.map(({ importEnv, importPath }) => ({
|
||||||
|
envId: importEnv.id,
|
||||||
|
secretPath: importPath
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
for await (const folder of importedFolders) {
|
||||||
|
if (folder) {
|
||||||
|
// get secrets contained in each imported folder by recursively calling
|
||||||
|
// this function against the imported folder
|
||||||
|
const importedSecrets = await getIntegrationSecrets({
|
||||||
|
environment: dto.environment,
|
||||||
|
projectId: dto.projectId,
|
||||||
|
folderId: folder.id,
|
||||||
|
key: dto.key,
|
||||||
|
depth: dto.depth + 1
|
||||||
|
});
|
||||||
|
|
||||||
|
// add the imported secrets to the current folder secrets
|
||||||
|
content = { ...content, ...importedSecrets };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
};
|
};
|
||||||
|
|
||||||
queueService.start(QueueName.IntegrationSync, async (job) => {
|
queueService.start(QueueName.IntegrationSync, async (job) => {
|
||||||
const { environment, projectId, secretPath } = job.data;
|
const { environment, projectId, secretPath, depth = 1 } = job.data;
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||||
if (!folder) {
|
if (!folder) {
|
||||||
logger.error("Secret path not found");
|
logger.error(new Error("Secret path not found"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const integrations = await integrationDAL.findByProjectIdV2(projectId, environment);
|
// start syncing all linked imports also
|
||||||
|
if (depth < MAX_SYNC_SECRET_DEPTH) {
|
||||||
|
// find all imports made with the given environment and secret path
|
||||||
|
const linkSourceDto = {
|
||||||
|
projectId,
|
||||||
|
importEnv: folder.environment.id,
|
||||||
|
importPath: secretPath
|
||||||
|
};
|
||||||
|
const imports = await secretImportDAL.find(linkSourceDto);
|
||||||
|
|
||||||
|
if (imports.length) {
|
||||||
|
// keep calling sync secret for all the imports made
|
||||||
|
const importedFolderIds = unique(imports, (i) => i.folderId).map(({ folderId }) => folderId);
|
||||||
|
const importedFolders = await folderDAL.findSecretPathByFolderIds(projectId, importedFolderIds);
|
||||||
|
const foldersGroupedById = groupBy(importedFolders, (i) => i.child || i.id);
|
||||||
|
await Promise.all(
|
||||||
|
imports
|
||||||
|
.filter(({ folderId }) => Boolean(foldersGroupedById[folderId][0].path))
|
||||||
|
.map(({ folderId }) => {
|
||||||
|
const syncDto = {
|
||||||
|
depth: depth + 1,
|
||||||
|
projectId,
|
||||||
|
secretPath: foldersGroupedById[folderId][0].path,
|
||||||
|
environment: foldersGroupedById[folderId][0].environmentSlug
|
||||||
|
};
|
||||||
|
logger.info(
|
||||||
|
`getIntegrationSecrets: Syncing secret due to link change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
|
||||||
|
);
|
||||||
|
return syncSecrets(syncDto);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info(`getIntegrationSecrets: Secret depth exceeded for [projectId=${projectId}] [folderId=${folder.id}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const integrations = await integrationDAL.findByProjectIdV2(projectId, environment); // note: returns array of integrations + integration auths in this environment
|
||||||
const toBeSyncedIntegrations = integrations.filter(
|
const toBeSyncedIntegrations = integrations.filter(
|
||||||
|
// note: sync only the integrations sourced from secretPath
|
||||||
({ secretPath: integrationSecPath, isActive }) => isActive && isSamePath(secretPath, integrationSecPath)
|
({ secretPath: integrationSecPath, isActive }) => isActive && isSamePath(secretPath, integrationSecPath)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!integrations.length) return;
|
if (!integrations.length) return;
|
||||||
logger.info("Secret integration sync started", job.data, job.id);
|
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}]`
|
||||||
|
);
|
||||||
for (const integration of toBeSyncedIntegrations) {
|
for (const integration of toBeSyncedIntegrations) {
|
||||||
const integrationAuth = {
|
const integrationAuth = {
|
||||||
...integration.integrationAuth,
|
...integration.integrationAuth,
|
||||||
@@ -334,7 +390,13 @@ export const secretQueueFactory = ({
|
|||||||
|
|
||||||
const botKey = await projectBotService.getBotKey(projectId);
|
const botKey = await projectBotService.getBotKey(projectId);
|
||||||
const { accessToken, accessId } = await integrationAuthService.getIntegrationAccessToken(integrationAuth, botKey);
|
const { accessToken, accessId } = await integrationAuthService.getIntegrationAccessToken(integrationAuth, botKey);
|
||||||
const secrets = await getIntegrationSecrets({ environment, projectId, secretPath, folderId: folder.id }, botKey);
|
const secrets = await getIntegrationSecrets({
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
folderId: folder.id,
|
||||||
|
key: botKey,
|
||||||
|
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) {
|
||||||
@@ -362,7 +424,7 @@ export const secretQueueFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Secret integration sync ended", job.id);
|
logger.info("Secret integration sync ended: %s", job.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
queueService.start(QueueName.SecretReminder, async ({ data }) => {
|
queueService.start(QueueName.SecretReminder, async ({ data }) => {
|
||||||
@@ -403,7 +465,7 @@ export const secretQueueFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
queueService.listen(QueueName.IntegrationSync, "failed", (job, err) => {
|
queueService.listen(QueueName.IntegrationSync, "failed", (job, err) => {
|
||||||
logger.error("Failed to sync integration", job?.data, err);
|
logger.error(err, "Failed to sync integration %s", job?.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
queueService.start(QueueName.SecretWebhook, async (job) => {
|
queueService.start(QueueName.SecretWebhook, async (job) => {
|
||||||
@@ -411,7 +473,8 @@ export const secretQueueFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
syncSecrets,
|
// depth is internal only field thus no need to make it available outside
|
||||||
|
syncSecrets: (dto: TGetSecrets) => syncSecrets(dto),
|
||||||
syncIntegrations,
|
syncIntegrations,
|
||||||
addSecretReminder,
|
addSecretReminder,
|
||||||
removeSecretReminder,
|
removeSecretReminder,
|
||||||
|
|||||||
Reference in New Issue
Block a user