diff --git a/backend/src/db/migrations/20250828110158_add_folderIds_to_secret_syncs.ts b/backend/src/db/migrations/20250828110158_add_folderIds_to_secret_syncs.ts new file mode 100644 index 0000000000..a129858e56 --- /dev/null +++ b/backend/src/db/migrations/20250828110158_add_folderIds_to_secret_syncs.ts @@ -0,0 +1,64 @@ +import { Knex } from "knex"; + +import { TableName } from "@app/db/schemas"; + +const TABLE = TableName.SecretSync; +const JOIN_TABLE = "secret_sync_folders"; + +export async function up(knex: Knex): Promise { + const hasFolderId = await knex.schema.hasColumn(TABLE, "folderId"); + + const hasJoinTable = await knex.schema.hasTable(JOIN_TABLE); + if (!hasJoinTable) { + await knex.schema.createTable(JOIN_TABLE, (t) => { + t.uuid("secretSyncId").notNullable().references("id").inTable(TABLE).onDelete("CASCADE"); + + t.uuid("folderId").notNullable().references("id").inTable("secret_folders").onDelete("CASCADE"); + + t.primary(["secretSyncId", "folderId"]); + t.timestamp("createdAt").defaultTo(knex.fn.now()); + t.timestamp("updatedAt").defaultTo(knex.fn.now()); + }); + } + + if (hasFolderId) { + await knex.raw(` + INSERT INTO "${JOIN_TABLE}" ("secretSyncId", "folderId", "createdAt", "updatedAt") + SELECT id, "folderId", NOW(), NOW() + FROM "${TABLE}" + WHERE "folderId" IS NOT NULL + `); + + await knex.schema.alterTable(TABLE, (t) => { + t.dropForeign(["folderId"]); + t.dropColumn("folderId"); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasCol = await knex.schema.hasColumn(TABLE, "folderId"); + + if (!hasCol) { + await knex.schema.alterTable(TABLE, (t) => { + t.uuid("folderId").nullable(); + t.foreign("folderId").references("id").inTable("secret_folders").onDelete("SET NULL"); + }); + + await knex.raw(` + UPDATE "${TABLE}" s + SET "folderId" = sub.folderId + FROM ( + SELECT DISTINCT ON ("secretSyncId") "secretSyncId", "folderId" + FROM "${JOIN_TABLE}" + ORDER BY "secretSyncId", "createdAt" ASC + ) sub + WHERE s.id = sub."secretSyncId" + `); + } + + const hasJoinTable = await knex.schema.hasTable(JOIN_TABLE); + if (hasJoinTable) { + await knex.schema.dropTable(JOIN_TABLE); + } +} diff --git a/backend/src/db/schemas/secret-sync-folders.ts b/backend/src/db/schemas/secret-sync-folders.ts new file mode 100644 index 0000000000..f521193e73 --- /dev/null +++ b/backend/src/db/schemas/secret-sync-folders.ts @@ -0,0 +1,19 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const SecretSyncFoldersSchema = z.object({ + secretSyncId: z.string().uuid(), + folderId: z.string().uuid(), + createdAt: z.date().nullable().optional(), + updatedAt: z.date().nullable().optional() +}); + +export type TSecretSyncFolders = z.infer; +export type TSecretSyncFoldersInsert = Omit, TImmutableDBKeys>; +export type TSecretSyncFoldersUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/server/routes/v1/secret-sync-routers/secret-sync-endpoints.ts b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-endpoints.ts index 9db3a2013c..8ccf8f32d8 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/secret-sync-endpoints.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-endpoints.ts @@ -29,6 +29,7 @@ export const registerSyncSecretsEndpoints = ; updateSchema: z.ZodType<{ connectionId?: string; @@ -39,6 +40,7 @@ export const registerSyncSecretsEndpoints = ; responseSchema: z.ZodTypeAny; }) => { diff --git a/backend/src/services/secret-folder/secret-folder-dal.ts b/backend/src/services/secret-folder/secret-folder-dal.ts index 7dfeaddcf7..6d306bbf99 100644 --- a/backend/src/services/secret-folder/secret-folder-dal.ts +++ b/backend/src/services/secret-folder/secret-folder-dal.ts @@ -88,6 +88,86 @@ const sqlFindMultipleFolderByEnvPathQuery = (db: Knex, query: Array<{ envId: str .from("parent"); }; +const sqlListDescendantsFromFolder = ( + db: Knex, + folder: { + id: string; + path: string; + projectId: string; + envSlug: string; + envName: string; + envId: string; + name: string; + version?: number | null; + createdAt: Date; + updatedAt: Date; + parentId?: string | null; + isReserved?: boolean | null; + description?: string | null; + lastSecretModified?: Date | null; + } +) => { + const parentId = folder.parentId ?? null; + const description = folder.description ?? null; + + return db + .with( + "seed", + (qb) => + void qb.select({ + depth: db.raw("1"), + path: db.raw("?::text", [folder.path]), + envSlug: db.raw("?::text", [folder.envSlug]), + envName: db.raw("?::text", [folder.envName]), + projectId: db.raw("?::uuid", [folder.projectId]), + + id: db.raw("?::uuid", [folder.id]), + name: db.raw("?::text", [folder.name]), + version: db.raw("?::int", [folder.version]), + createdAt: db.raw("?::timestamptz", [folder.createdAt]), + updatedAt: db.raw("?::timestamptz", [folder.updatedAt]), + parentId: db.raw("?::uuid", [parentId]), + isReserved: db.raw("?::boolean", [folder.isReserved]), + description: db.raw("?::text", [description]), + lastSecretModified: db.raw("?::timestamptz", [folder.lastSecretModified]), + envId: db.raw("?::uuid", [folder.envId]) + }) + ) + + .withRecursive("tree", (baseQb) => { + void baseQb + .select("*") + .from("seed") + .union( + (qb) => + void qb + .select({ + depth: db.raw("tree.depth + 1"), + path: db.raw("CONCAT((CASE WHEN tree.path = '/' THEN '' ELSE tree.path END),'/', sf.name)"), + envSlug: db.ref("envSlug").withSchema("tree"), + envName: db.ref("envName").withSchema("tree"), + projectId: db.ref("projectId").withSchema("tree"), + + id: db.ref("id").withSchema("sf"), + name: db.ref("name").withSchema("sf"), + version: db.ref("version").withSchema("sf"), + createdAt: db.ref("createdAt").withSchema("sf"), + updatedAt: db.ref("updatedAt").withSchema("sf"), + parentId: db.ref("parentId").withSchema("sf"), + isReserved: db.ref("isReserved").withSchema("sf"), + description: db.ref("description").withSchema("sf"), + lastSecretModified: db.ref("lastSecretModified").withSchema("sf"), + envId: db.ref("envId").withSchema("sf") + }) + .from({ sf: TableName.SecretFolder }) + .join("tree", "tree.id", "sf.parentId") + ); + }) + .from("tree") + .select("*") + .orderBy([{ column: "depth" }, { column: "path" }]); +}; + const sqlFindFolderByPathQuery = (db: Knex, projectId: string, environments: string[], secretPath: string) => { // this is removing an trailing slash like /folder1/folder2/ -> /folder1/folder2 const formatedPath = secretPath.at(-1) === "/" && secretPath.length > 1 ? secretPath.slice(0, -1) : secretPath; @@ -211,7 +291,13 @@ export const ROOT_FOLDER_NAME = "root"; export const secretFolderDALFactory = (db: TDbClient) => { const secretFolderOrm = ormify(db, TableName.SecretFolder); - const findBySecretPath = async (projectId: string, environment: string, path: string, tx?: Knex) => { + const findBySecretPath = async ( + projectId: string, + environment: string, + path: string, + tx?: Knex, + recursive: boolean = false + ) => { const isValidPath = isValidSecretPath(path); if (!isValidPath) throw new BadRequestError({ @@ -225,12 +311,14 @@ export const secretFolderDALFactory = (db: TDbClient) => { const folder = await query; if (!folder) return; const { envId: id, envName: name, envSlug: slug, ...el } = folder; - return { ...el, envId: id, environment: { id, name, slug } }; + if (!recursive) { + return { ...el, envId: id, environment: { id, name, slug } }; + } + return await sqlListDescendantsFromFolder(tx || db.replicaNode(), folder); } catch (error) { throw new DatabaseError({ error, name: "Find by secret path" }); } }; - // finds folders by path for multiple envs const findBySecretPathMultiEnv = async (projectId: string, environments: string[], path: string, tx?: Knex) => { const isValidPath = isValidSecretPath(path); diff --git a/backend/src/services/secret-sync/secret-sync-dal.ts b/backend/src/services/secret-sync/secret-sync-dal.ts index e50593f100..10665b69fe 100644 --- a/backend/src/services/secret-sync/secret-sync-dal.ts +++ b/backend/src/services/secret-sync/secret-sync-dal.ts @@ -13,12 +13,17 @@ type SecretSyncFindFilter = Parameters>[0]; const baseSecretSyncQuery = ({ filter, db, tx }: { db: TDbClient; filter?: SecretSyncFindFilter; tx?: Knex }) => { const query = (tx || db.replicaNode())(TableName.SecretSync) - .leftJoin(TableName.SecretFolder, `${TableName.SecretSync}.folderId`, `${TableName.SecretFolder}.id`) + .leftJoin(TableName.SecretSyncFolders, `${TableName.SecretSync}.id`, `${TableName.SecretSyncFolders}.secretSyncId`) + .leftJoin(TableName.SecretFolder, `${TableName.SecretSyncFolders}.folderId`, `${TableName.SecretFolder}.id`) .leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) .join(TableName.AppConnection, `${TableName.SecretSync}.connectionId`, `${TableName.AppConnection}.id`) .select(selectAllTableCols(TableName.SecretSync)) .select( // environment + db.raw(`coalesce(array_agg(distinct ??) filter (where ?? is not null), '{}') as "folderId"`, [ + `${TableName.SecretSyncFolders}.folderId`, + `${TableName.SecretSyncFolders}.folderId` + ]), db.ref("name").withSchema(TableName.Environment).as("envName"), db.ref("id").withSchema(TableName.Environment).as("envId"), db.ref("slug").withSchema(TableName.Environment).as("envSlug"), @@ -37,11 +42,24 @@ const baseSecretSyncQuery = ({ filter, db, tx }: { db: TDbClient; filter?: Secre .ref("isPlatformManagedCredentials") .withSchema(TableName.AppConnection) .as("connectionIsPlatformManagedCredentials") - ); + ) + .groupBy(`${TableName.SecretSync}.id`, `${TableName.Environment}.id`, `${TableName.AppConnection}.id`); if (filter) { /* eslint-disable @typescript-eslint/no-misused-promises */ - void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.SecretSync, filter))); + const { $in, folderId, ...rest } = filter; + + if ($in && $in.folderId) { + void query.whereIn(`${TableName.SecretSyncFolders}.folderId`, $in.folderId); + } + + if (folderId) { + void query.where(`${TableName.SecretSyncFolders}.folderId`, folderId); + } + + if (Object.keys(rest).length > 0) { + void query.where(prependTableNameToFindFilter(TableName.SecretSync, rest)); + } } return query; @@ -49,7 +67,7 @@ const baseSecretSyncQuery = ({ filter, db, tx }: { db: TDbClient; filter?: Secre const expandSecretSync = ( secretSync: Awaited>[number], - folder?: Awaited>[number] + folder?: Awaited> ) => { const { envId, @@ -88,12 +106,15 @@ const expandSecretSync = ( isPlatformManagedCredentials: connectionIsPlatformManagedCredentials, gatewayId: connectionGatewayId }, - folder: folder - ? { - id: folder.id, - path: folder.path - } - : null + folder: + folder && folder.length > 0 + ? folder + .filter((f): f is NonNullable => f !== undefined) + .map((f) => ({ + id: f.id, + path: f.path + })) + : [] }; }; @@ -102,6 +123,7 @@ export const secretSyncDALFactory = ( folderDAL: Pick ) => { const secretSyncOrm = ormify(db, TableName.SecretSync); + const secretSyncOrmWithFolder = ormify(db, TableName.SecretSyncFolders); const findById = async (id: string, tx?: Knex) => { try { @@ -113,8 +135,8 @@ export const secretSyncDALFactory = ( if (secretSync) { // TODO (scott): replace with cached folder path once implemented - const [folderWithPath] = secretSync.folderId - ? await folderDAL.findSecretPathByFolderIds(secretSync.projectId, [secretSync.folderId]) + const folderWithPath = secretSync.folderId + ? await folderDAL.findSecretPathByFolderIds(secretSync.projectId, secretSync.folderId) : []; return expandSecretSync(secretSync, folderWithPath); } @@ -123,20 +145,29 @@ export const secretSyncDALFactory = ( } }; - const create = async (data: Parameters<(typeof secretSyncOrm)["create"]>[0]) => { - const secretSync = (await secretSyncOrm.transaction(async (tx) => { + const create = async ( + data: Parameters<(typeof secretSyncOrm)["create"]>[0], + folderIds?: Parameters<(typeof secretSyncOrmWithFolder)["insertMany"]>[0] + ) => { + const secretSync = await secretSyncOrm.transaction(async (tx) => { const sync = await secretSyncOrm.create(data, tx); - return baseSecretSyncQuery({ - filter: { id: sync.id }, - db, - tx - }).first(); - }))!; + if (folderIds && folderIds.length > 0) { + const folderData = folderIds.map((folderId: string) => ({ + folderId, + secretSyncId: sync.id + })); + await secretSyncOrmWithFolder.insertMany(folderData, tx); + } + + return baseSecretSyncQuery({ filter: { id: sync.id }, db, tx }).first(); + }); + + const normalizedFolderIds = Array.isArray(folderIds) ? folderIds : folderIds ? [folderIds] : []; // TODO (scott): replace with cached folder path once implemented - const [folderWithPath] = secretSync.folderId - ? await folderDAL.findSecretPathByFolderIds(secretSync.projectId, [secretSync.folderId]) + const folderWithPath = normalizedFolderIds.length + ? await folderDAL.findSecretPathByFolderIds(secretSync.projectId, normalizedFolderIds) : []; return expandSecretSync(secretSync, folderWithPath); }; @@ -153,19 +184,26 @@ export const secretSyncDALFactory = ( }))!; // TODO (scott): replace with cached folder path once implemented - const [folderWithPath] = secretSync.folderId - ? await folderDAL.findSecretPathByFolderIds(secretSync.projectId, [secretSync.folderId]) + const folderWithPath = secretSync.folderId + ? await folderDAL.findSecretPathByFolderIds(secretSync.projectId, secretSync.folderId) : []; return expandSecretSync(secretSync, folderWithPath); }; + const deleteById = async (syncId: string) => { + return secretSyncOrm.transaction(async (tx) => { + await secretSyncOrmWithFolder.delete({ secretSyncId: syncId }, tx); + return secretSyncOrm.deleteById(syncId, tx); + }); + }; + const findOne = async (filter: Parameters<(typeof secretSyncOrm)["findOne"]>[0], tx?: Knex) => { try { const secretSync = await baseSecretSyncQuery({ filter, db, tx }).first(); if (secretSync) { // TODO (scott): replace with cached folder path once implemented - const [folderWithPath] = secretSync.folderId + const folderWithPath = secretSync.folderId ? await folderDAL.findSecretPathByFolderIds(secretSync.projectId, [secretSync.folderId]) : []; return expandSecretSync(secretSync, folderWithPath); @@ -181,10 +219,11 @@ export const secretSyncDALFactory = ( if (!secretSyncs.length) return []; - const foldersWithPath = await folderDAL.findSecretPathByFolderIds( - secretSyncs[0].projectId, - secretSyncs.filter((sync) => Boolean(sync.folderId)).map((sync) => sync.folderId!) - ); + const folderIds = secretSyncs + .filter((sync) => Array.isArray(sync.folderId) && sync.folderId.length > 0) + .flatMap((sync) => sync.folderId); + + const foldersWithPath = await folderDAL.findSecretPathByFolderIds(secretSyncs[0].projectId, folderIds); // TODO (scott): replace with cached folder path once implemented const folderRecord: Record = {}; @@ -193,13 +232,20 @@ export const secretSyncDALFactory = ( if (folder) folderRecord[folder.id] = folder; }); - return secretSyncs.map((secretSync) => - expandSecretSync(secretSync, secretSync.folderId ? folderRecord[secretSync.folderId] : undefined) - ); + return secretSyncs.map((secretSync) => { + return expandSecretSync( + secretSync, + secretSync.folderId + ? Array.isArray(secretSync.folderId) + ? secretSync.folderId.map((fid: string) => folderRecord[fid]) + : folderRecord[secretSync.folderId as string] + : undefined + ); + }); } catch (error) { throw new DatabaseError({ error, name: "Find - Secret Sync" }); } }; - return { ...secretSyncOrm, findById, findOne, find, create, updateById }; + return { ...secretSyncOrm, deleteById, findById, findOne, find, create, updateById }; }; diff --git a/backend/src/services/secret-sync/secret-sync-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts index 7bef7d8c78..c1354169f5 100644 --- a/backend/src/services/secret-sync/secret-sync-queue.ts +++ b/backend/src/services/secret-sync/secret-sync-queue.ts @@ -72,6 +72,7 @@ type TSecretSyncQueueFactoryDep = { secretV2BridgeDAL: Pick< TSecretV2BridgeDALFactory, | "findByFolderId" + | "findByFolderIds" | "find" | "insertMany" | "upsertSecretReferences" @@ -224,7 +225,7 @@ export const secretSyncQueueFactory = ({ canExpandValue: () => true }); - const secrets = await secretV2BridgeDAL.findByFolderId({ folderId }); + const secrets = await secretV2BridgeDAL.findByFolderIds({ folderIds: folderId }); await Promise.allSettled( secrets.map(async (secret) => { @@ -251,7 +252,7 @@ export const secretSyncQueueFactory = ({ if (!includeImports) return secretMap; - const secretImports = await secretImportDAL.find({ folderId, isReplication: false }); + const secretImports = await secretImportDAL.findByFolderIds(folderId); if (secretImports.length) { const importedSecrets = await fnSecretsV2FromImports({ @@ -437,7 +438,7 @@ export const secretSyncQueueFactory = ({ }); logger.info( - `SecretSync Sync [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]` + `SecretSync Sync [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [connectionId=${secretSync.connectionId}]` ); let isSynced = false; @@ -493,7 +494,7 @@ export const secretSyncQueueFactory = ({ } catch (err) { logger.error( err, - `SecretSync Sync Error [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]` + `SecretSync Sync Error [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [connectionId=${secretSync.connectionId}]` ); if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { @@ -580,7 +581,7 @@ export const secretSyncQueueFactory = ({ }); logger.info( - `SecretSync Import [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]` + `SecretSync Import [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [connectionId=${secretSync.connectionId}]` ); let isSuccess = false; @@ -613,7 +614,7 @@ export const secretSyncQueueFactory = ({ } catch (err) { logger.error( err, - `SecretSync Import Error [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]` + `SecretSync Import Error [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [connectionId=${secretSync.connectionId}]` ); if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { @@ -704,7 +705,7 @@ export const secretSyncQueueFactory = ({ }); logger.info( - `SecretSync Remove [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]` + `SecretSync Remove [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [connectionId=${secretSync.connectionId}]` ); let isSuccess = false; @@ -744,7 +745,7 @@ export const secretSyncQueueFactory = ({ } catch (err) { logger.error( err, - `SecretSync Remove Error [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]` + `SecretSync Remove Error [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [connectionId=${secretSync.connectionId}]` ); if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { diff --git a/backend/src/services/secret-sync/secret-sync-schemas.ts b/backend/src/services/secret-sync/secret-sync-schemas.ts index 3622ef3d04..c8afc46592 100644 --- a/backend/src/services/secret-sync/secret-sync-schemas.ts +++ b/backend/src/services/secret-sync/secret-sync-schemas.ts @@ -85,7 +85,10 @@ export const BaseSecretSyncSchema = ( @@ -111,7 +114,8 @@ export const GenericCreateSecretSyncFieldsSchema = ( @@ -139,5 +143,6 @@ export const GenericUpdateSecretSyncFieldsSchema = f.path) : [] }) : ProjectPermissionSub.SecretSyncs ) @@ -219,7 +219,7 @@ export const secretSyncServiceFactory = ({ }; const createSecretSync = async ( - { projectId, secretPath, environment, ...params }: TCreateSecretSyncDTO, + { projectId, secretPath, environment, recursive, ...params }: TCreateSecretSyncDTO, actor: OrgServiceActor ) => { await enterpriseSyncCheck( @@ -257,9 +257,17 @@ export const secretSyncServiceFactory = ({ } ); - const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); + const foldersRaw = await folderDAL.findBySecretPath(projectId, environment, secretPath, undefined, recursive); + const result = { + commonData: { + ...params, + ...(params.isAutoSyncEnabled && { syncStatus: SecretSyncStatus.Pending }), + projectId + }, + folderIds: (Array.isArray(foldersRaw) ? foldersRaw : [foldersRaw]).filter(Boolean).map((folder: any) => folder.id) + }; - if (!folder) + if (!result.folderIds.length) throw new BadRequestError({ message: `Could not find folder with path "${secretPath}" in environment "${environment}" for project with ID "${projectId}"` }); @@ -270,12 +278,7 @@ export const secretSyncServiceFactory = ({ await appConnectionService.connectAppConnectionById(destinationApp, params.connectionId, actor); try { - const secretSync = await secretSyncDAL.create({ - folderId: folder.id, - ...params, - ...(params.isAutoSyncEnabled && { syncStatus: SecretSyncStatus.Pending }), - projectId - }); + const secretSync = await secretSyncDAL.create(result.commonData, result.folderIds); if (secretSync.isAutoSyncEnabled) await secretSyncQueue.queueSecretSyncSyncSecretsById({ syncId: secretSync.id }); @@ -283,7 +286,7 @@ export const secretSyncServiceFactory = ({ } catch (err) { if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) { throw new BadRequestError({ - message: `A Secret Sync with the name "${params.name}" already exists for the project with ID "${folder.projectId}"` + message: `A Secret Sync with the name "${params.name}" already exists for the project with ID "${foldersRaw[0].projectId}"` }); } diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index 6435e19d39..4c8bed4b8f 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -311,6 +311,7 @@ export type TCreateSecretSyncDTO = Pick> & { diff --git a/frontend/src/components/secret-syncs/forms/CreateSecretSyncForm.tsx b/frontend/src/components/secret-syncs/forms/CreateSecretSyncForm.tsx index 8a1be69c49..712af1d34f 100644 --- a/frontend/src/components/secret-syncs/forms/CreateSecretSyncForm.tsx +++ b/frontend/src/components/secret-syncs/forms/CreateSecretSyncForm.tsx @@ -32,7 +32,7 @@ type Props = { }; const FORM_TABS: { name: string; key: string; fields: (keyof TSecretSyncForm)[] }[] = [ - { name: "Source", key: "source", fields: ["secretPath", "environment"] }, + { name: "Source", key: "source", fields: ["secretPath", "environment", "recursive"] }, { name: "Destination", key: "destination", fields: ["connection", "destinationConfig"] }, { name: "Sync Options", key: "options", fields: ["syncOptions"] }, { name: "Details", key: "details", fields: ["name", "description"] }, diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncSourceFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncSourceFields.tsx index 850ebbc801..5cb6e92572 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncSourceFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncSourceFields.tsx @@ -2,7 +2,7 @@ import { useEffect } from "react"; import { Controller, useFormContext } from "react-hook-form"; import { subject } from "@casl/ability"; -import { FilterableSelect, FormControl } from "@app/components/v2"; +import { Checkbox, FilterableSelect, FormControl } from "@app/components/v2"; import { SecretPathInput } from "@app/components/v2/SecretPathInput"; import { useProjectPermission, useWorkspace } from "@app/context"; import { @@ -20,6 +20,7 @@ export const SecretSyncSourceFields = () => { const selectedEnvironment = watch("environment"); const selectedSecretPath = watch("secretPath"); + const recursive = watch("recursive"); useEffect(() => { const hasAccessToSource = @@ -28,7 +29,8 @@ export const SecretSyncSourceFields = () => { ProjectPermissionSecretSyncActions.Create, subject(ProjectPermissionSub.SecretSyncs, { environment: selectedEnvironment.slug, - secretPath: selectedSecretPath + secretPath: selectedSecretPath, + recursive: recursive }) ); @@ -39,7 +41,7 @@ export const SecretSyncSourceFields = () => { } else { clearErrors("secretPath"); } - }, [selectedEnvironment, selectedSecretPath]); + }, [selectedEnvironment, selectedSecretPath, recursive]); return ( <> @@ -78,6 +80,21 @@ export const SecretSyncSourceFields = () => { control={control} name="secretPath" /> + ( +
+ Sync subfolders recursively + +
+ )} + /> ); }; diff --git a/frontend/src/components/secret-syncs/forms/schemas/base-secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/base-secret-sync-schema.ts index 1d925eb076..c50ef823a3 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/base-secret-sync-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/base-secret-sync-schema.ts @@ -50,6 +50,7 @@ export const BaseSecretSyncSchema =