diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index eca11e983..d38f9e59a 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -688,7 +688,9 @@ export const RAW_SECRETS = { environment: "The slug of the environment to list secrets from.", secretPath: "The secret path to list secrets from.", includeImports: "Weather to include imported secrets or not.", - tagSlugs: "The comma separated tag slugs to filter secrets." + tagSlugs: "The comma separated tag slugs to filter secrets.", + secretMetadata: + "The secret metadata key-value pairs to filter secrets by. When querying for multiple metadata pairs, the query is treated as an AND operation. Secret metadata format is key1:value1,key2:value2." }, CREATE: { secretName: "The name of the secret to create.", diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index 67fd09ac8..0053a9ed2 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -181,6 +181,40 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } ], querystring: z.object({ + secretMetadata: z + .string() + .optional() + .transform((value) => { + if (!value) return undefined; + + const metadata = value.split(",").map((el) => { + const [key, val] = el.split(":"); + return { key, value: val }; + }); + + return metadata; + }) + .superRefine((el, ctx) => { + if (el && !Array.isArray(el)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Invalid secretMetadata format. Correct format is key1:value1,key2:value2" + }); + } + + if (el) { + for (const item of el) { + if (!item.key || !item.value) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "Invalid secretMetadata format, key or value is missing. Correct format is key1:value1,key2:value2" + }); + } + } + } + }) + .describe(RAW_SECRETS.LIST.secretMetadata), workspaceId: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceId), workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug), environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment), @@ -281,6 +315,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { actorAuthMethod: req.permission.authMethod, projectId: workspaceId, path: secretPath, + secretMetadata: req.query.secretMetadata, includeImports: req.query.include_imports, recursive: req.query.recursive, tagSlugs: req.query.tagSlugs diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts index 64ad77deb..77bf8c140 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts @@ -414,6 +414,22 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { `${TableName.SecretTag}.id` ) .leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`) + .where((bd) => { + if (filters?.secretMetadata && filters.secretMetadata?.length > 0) { + filters.secretMetadata.forEach((meta) => { + void bd.whereExists((qb) => { + void qb + .select("secretId") + .from(TableName.ResourceMetadata) + .whereRaw(`"${TableName.ResourceMetadata}"."secretId" = "${TableName.SecretV2}"."id"`) + .where({ + [`${TableName.ResourceMetadata}.key` as string]: meta.key, + [`${TableName.ResourceMetadata}.value` as string]: meta.value + }); + }); + }); + } + }) .select( selectAllTableCols(TableName.SecretV2), db.raw( @@ -481,6 +497,19 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { } ] }); + + // if (secretMetadata) { + // return data.filter((s) => { + // if (!s.secretMetadata.length) return false; + + // return secretMetadata.every((m) => { + // const secretMeta = s.secretMetadata.find((sm) => sm.key === m.key); + // if (!secretMeta) return false; + // return secretMeta.value === m.value; + // }); + // }); + // } + return data; } catch (error) { throw new DatabaseError({ error, name: "get all secret" }); diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts index 5e5cc26cd..ee997c970 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts @@ -30,6 +30,10 @@ export type TGetSecretsDTO = { includeImports?: boolean; recursive?: boolean; tagSlugs?: string[]; + secretMetadata?: { + key: string; + value: string; + }[]; orderBy?: SecretsOrderBy; orderDirection?: OrderByDirection; offset?: number; @@ -310,6 +314,7 @@ export type TFindSecretsByFolderIdsFilter = { orderDirection?: OrderByDirection; search?: string; tagSlugs?: string[]; + secretMetadata?: { key: string; value: string }[]; includeTagsInSearch?: boolean; keys?: string[]; }; diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index a9bba106c..eb488c6a6 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -1263,6 +1263,13 @@ export const secretServiceFactory = ({ name: "bot_not_found_error" }); + if (paramsV2.secretMetadata) { + throw new BadRequestError({ + message: "Please upgrade your project to filter secrets by metadata", + name: "SecretMetadataNotSupported" + }); + } + const { secrets, imports } = await getSecrets({ actorId, projectId, diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index d1011bf8c..a857c0d30 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -182,6 +182,10 @@ export type TGetSecretsRawDTO = { includeImports?: boolean; recursive?: boolean; tagSlugs?: string[]; + secretMetadata?: { + key: string; + value: string; + }[]; orderBy?: SecretsOrderBy; orderDirection?: OrderByDirection; offset?: number;