Merge pull request #3060 from Infisical/daniel/query-secrets-by-metadata

feat(api): list secrets filter by metadata
This commit is contained in:
Daniel Hougaard
2025-02-07 04:53:30 +01:00
committed by GitHub
6 changed files with 95 additions and 1 deletions

View File

@@ -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.",
metadataFilter:
"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 key=value1,value=value2|key=value3,value=value4."
},
CREATE: {
secretName: "The name of the secret to create.",

View File

@@ -181,6 +181,66 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}
],
querystring: z.object({
metadataFilter: z
.string()
.optional()
.transform((val) => {
if (!val) return undefined;
const result: { key?: string; value?: string }[] = [];
const pairs = val.split("|");
for (const pair of pairs) {
const keyValuePair: { key?: string; value?: string } = {};
const parts = pair.split(/[,=]/);
for (let i = 0; i < parts.length; i += 2) {
const identifier = parts[i].trim().toLowerCase();
const value = parts[i + 1]?.trim();
if (identifier === "key" && value) {
keyValuePair.key = value;
} else if (identifier === "value" && value) {
keyValuePair.value = value;
}
}
if (keyValuePair.key && keyValuePair.value) {
result.push(keyValuePair);
}
}
return result.length ? result : undefined;
})
.superRefine((metadata, ctx) => {
if (metadata && !Array.isArray(metadata)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message:
"Invalid secretMetadata format. Correct format is key=value1,value=value2|key=value3,value=value4."
});
}
if (metadata) {
if (metadata.length > 10) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "You can only filter by up to 10 metadata fields"
});
}
for (const item of metadata) {
if (!item.key && !item.value) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message:
"Invalid secretMetadata format, key or value must be provided. Correct format is key=value1,value=value2|key=value3,value=value4."
});
}
}
}
})
.describe(RAW_SECRETS.LIST.metadataFilter),
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 +341,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
actorAuthMethod: req.permission.authMethod,
projectId: workspaceId,
path: secretPath,
metadataFilter: req.query.metadataFilter,
includeImports: req.query.include_imports,
recursive: req.query.recursive,
tagSlugs: req.query.tagSlugs

View File

@@ -414,6 +414,20 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
`${TableName.SecretTag}.id`
)
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
.where((qb) => {
if (filters?.metadataFilter && filters.metadataFilter.length > 0) {
filters.metadataFilter.forEach((meta) => {
void qb.whereExists((subQuery) => {
void subQuery
.select("secretId")
.from(TableName.ResourceMetadata)
.whereRaw(`"${TableName.ResourceMetadata}"."secretId" = "${TableName.SecretV2}"."id"`)
.where(`${TableName.ResourceMetadata}.key`, meta.key)
.where(`${TableName.ResourceMetadata}.value`, meta.value);
});
});
}
})
.select(
selectAllTableCols(TableName.SecretV2),
db.raw(
@@ -481,6 +495,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
}
]
});
return data;
} catch (error) {
throw new DatabaseError({ error, name: "get all secret" });

View File

@@ -30,6 +30,10 @@ export type TGetSecretsDTO = {
includeImports?: boolean;
recursive?: boolean;
tagSlugs?: string[];
metadataFilter?: {
key?: string;
value?: string;
}[];
orderBy?: SecretsOrderBy;
orderDirection?: OrderByDirection;
offset?: number;
@@ -310,6 +314,7 @@ export type TFindSecretsByFolderIdsFilter = {
orderDirection?: OrderByDirection;
search?: string;
tagSlugs?: string[];
metadataFilter?: { key?: string; value?: string }[];
includeTagsInSearch?: boolean;
keys?: string[];
};

View File

@@ -1263,6 +1263,13 @@ export const secretServiceFactory = ({
name: "bot_not_found_error"
});
if (paramsV2.metadataFilter) {
throw new BadRequestError({
message: "Please upgrade your project to filter secrets by metadata",
name: "SecretMetadataNotSupported"
});
}
const { secrets, imports } = await getSecrets({
actorId,
projectId,

View File

@@ -182,6 +182,10 @@ export type TGetSecretsRawDTO = {
includeImports?: boolean;
recursive?: boolean;
tagSlugs?: string[];
metadataFilter?: {
key?: string;
value?: string;
}[];
orderBy?: SecretsOrderBy;
orderDirection?: OrderByDirection;
offset?: number;