From 5f902229f6e0a1976d9690ed86a745fcc15fda5d Mon Sep 17 00:00:00 2001 From: = Date: Sat, 13 Sep 2025 15:58:51 +0530 Subject: [PATCH] feat: migrated secret router --- .../secret-rotation-queue.ts | 2 +- backend/src/lib/api-docs/constants.ts | 8 +- backend/src/server/routes/index.ts | 2 + .../src/server/routes/v1/dashboard-router.ts | 8 +- backend/src/server/routes/v3/secret-router.ts | 137 +- backend/src/server/routes/v4/index.ts | 5 + backend/src/server/routes/v4/secret-router.ts | 1317 +++++++++++++++++ backend/src/services/secret/secret-types.ts | 3 +- .../src/services/telemetry/telemetry-types.ts | 2 +- frontend/src/hooks/api/secrets/mutations.tsx | 175 ++- frontend/src/hooks/api/secrets/queries.tsx | 36 +- frontend/src/hooks/api/secrets/types.ts | 46 +- 12 files changed, 1472 insertions(+), 269 deletions(-) create mode 100644 backend/src/server/routes/v4/index.ts create mode 100644 backend/src/server/routes/v4/secret-router.ts diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts index 557e71e6c..eefe6b63a 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts @@ -431,7 +431,7 @@ export const secretRotationQueueFactory = ({ numberOfSecrets: numberOfSecretsRotated, environment: secretRotation.environment.slug, secretPath: secretRotation.secretPath, - workspaceId: secretRotation.projectId + projectId: secretRotation.projectId } }); diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index c35dc2f33..ff56e5e01 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -984,7 +984,7 @@ export const RAW_SECRETS = { secretValue: "The value of the secret to create.", skipMultilineEncoding: "Skip multiline encoding for the secret value.", type: "The type of the secret to create.", - workspaceId: "The ID of the project to create the secret in.", + projectId: "The ID of the project to create the secret in.", tagIds: "The ID of the tags to be attached to the created secret.", secretReminderRepeatDays: "Interval for secret rotation notifications, measured in days.", secretReminderNote: "Note to be attached in notification email." @@ -992,7 +992,7 @@ export const RAW_SECRETS = { GET: { expand: "Whether or not to expand secret references.", secretName: "The name of the secret to get.", - workspaceId: "The ID of the project to get the secret from.", + projectId: "The ID of the project to get the secret from.", workspaceSlug: "The slug of the project to get the secret from.", environment: "The slug of the environment to get the secret from.", secretPath: "The path of the secret to get.", @@ -1011,7 +1011,7 @@ export const RAW_SECRETS = { skipMultilineEncoding: "Skip multiline encoding for the secret value.", type: "The type of the secret to update.", projectSlug: "The slug of the project to update the secret in.", - workspaceId: "The ID of the project to update the secret in.", + projectId: "The ID of the project to update the secret in.", tagIds: "The ID of the tags to be attached to the updated secret.", secretReminderRepeatDays: "Interval for secret rotation notifications, measured in days.", secretReminderNote: "Note to be attached in notification email.", @@ -1025,7 +1025,7 @@ export const RAW_SECRETS = { secretPath: "The path of the secret.", type: "The type of the secret to delete.", projectSlug: "The slug of the project to delete the secret in.", - workspaceId: "The ID of the project where the secret is located." + projectId: "The ID of the project where the secret is located." }, GET_REFERENCE_TREE: { secretName: "The name of the secret to get the reference tree for.", diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index eccad2956..14ad4e17c 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -328,6 +328,7 @@ import { registerV1Routes } from "./v1"; import { initializeOauthConfigSync } from "./v1/sso-router"; import { registerV2Routes } from "./v2"; import { registerV3Routes } from "./v3"; +import { registerV4Routes } from "./v4"; const histogram = monitorEventLoopDelay({ resolution: 20 }); histogram.enable(); @@ -2294,6 +2295,7 @@ export const registerRoutes = async ( { prefix: "/api/v2" } ); await server.register(registerV3Routes, { prefix: "/api/v3" }); + await server.register(registerV4Routes, { prefix: "/api/v4" }); server.addHook("onClose", async () => { cronJobs.forEach((job) => job.stop()); diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index a54bd5ccf..a8132a3fa 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -474,7 +474,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: secretCountFromEnv, - workspaceId: projectId, + projectId: projectId, environment, secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -1001,7 +1001,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: secretCount, - workspaceId: projectId, + projectId: projectId, environment, secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -1168,7 +1168,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: secretCountForEnv, - workspaceId: projectId, + projectId: projectId, environment, secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -1361,7 +1361,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: secrets.length, - workspaceId: projectId, + projectId: projectId, environment, secretPath, channel: getUserAgentType(req.headers["user-agent"]), diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index ce0d4f188..eaeeb8bb8 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -336,7 +336,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: secrets.length, - workspaceId, + projectId: workspaceId, environment, secretPath: req.query.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -405,7 +405,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretName: z.string().trim().describe(RAW_SECRETS.GET.secretName) }), querystring: z.object({ - workspaceId: z.string().trim().optional().describe(RAW_SECRETS.GET.workspaceId), + workspaceId: z.string().trim().optional().describe(RAW_SECRETS.GET.projectId), workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.GET.workspaceSlug), environment: z.string().trim().optional().describe(RAW_SECRETS.GET.environment), secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.GET.secretPath), @@ -495,7 +495,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { distinctId: getTelemetryDistinctId(req), properties: { numberOfSecrets: 1, - workspaceId: secret.workspace, + projectId: secret.workspace, environment, secretPath: req.query.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -526,7 +526,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretName: SecretNameSchema.describe(RAW_SECRETS.CREATE.secretName) }), body: z.object({ - workspaceId: z.string().trim().optional().describe(RAW_SECRETS.CREATE.workspaceId), + workspaceId: z.string().trim().optional().describe(RAW_SECRETS.CREATE.projectId), projectSlug: z.string().trim().optional().describe(RAW_SECRETS.CREATE.projectSlug), environment: z.string().trim().describe(RAW_SECRETS.CREATE.environment), secretPath: z @@ -638,7 +638,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: 1, - workspaceId: projectId, + projectId, environment: req.body.environment, secretPath: req.body.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -669,7 +669,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretName: BaseSecretNameSchema.describe(RAW_SECRETS.UPDATE.secretName) }), body: z.object({ - workspaceId: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.workspaceId), + workspaceId: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.projectId), projectSlug: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.projectSlug), environment: z.string().trim().describe(RAW_SECRETS.UPDATE.environment), secretValue: z @@ -791,7 +791,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: 1, - workspaceId: projectId, + projectId, environment: req.body.environment, secretPath: req.body.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -821,7 +821,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretName: z.string().min(1).describe(RAW_SECRETS.DELETE.secretName) }), body: z.object({ - workspaceId: z.string().trim().optional().describe(RAW_SECRETS.DELETE.workspaceId), + workspaceId: z.string().trim().optional().describe(RAW_SECRETS.DELETE.projectId), projectSlug: z.string().trim().optional().describe(RAW_SECRETS.DELETE.projectSlug), environment: z.string().trim().describe(RAW_SECRETS.DELETE.environment), secretPath: z @@ -909,7 +909,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: 1, - workspaceId: projectId, + projectId, environment: req.body.environment, secretPath: req.body.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -1017,7 +1017,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: secrets.length, - workspaceId: req.query.workspaceId, + projectId: req.query.workspaceId, environment: req.query.environment, secretPath: req.query.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -1097,7 +1097,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: 1, - workspaceId: req.query.workspaceId, + projectId: req.query.workspaceId, environment: req.query.environment, secretPath: req.query.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -1272,7 +1272,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: 1, - workspaceId: req.body.workspaceId, + projectId: req.body.workspaceId, environment: req.body.environment, secretPath: req.body.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -1466,7 +1466,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: 1, - workspaceId: req.body.workspaceId, + projectId: req.body.workspaceId, environment: req.body.environment, secretPath: req.body.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -1594,7 +1594,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: 1, - workspaceId: req.body.workspaceId, + projectId: req.body.workspaceId, environment: req.body.environment, secretPath: req.body.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -1781,7 +1781,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: secrets.length, - workspaceId: req.body.workspaceId, + projectId: req.body.workspaceId, environment: req.body.environment, secretPath: req.body.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -1914,7 +1914,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: secrets.length, - workspaceId: req.body.workspaceId, + projectId: req.body.workspaceId, environment: req.body.environment, secretPath: req.body.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -2039,7 +2039,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: secrets.length, - workspaceId: req.body.workspaceId, + projectId: req.body.workspaceId, environment: req.body.environment, secretPath: req.body.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -2067,7 +2067,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { ], body: z.object({ projectSlug: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.projectSlug), - workspaceId: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.workspaceId), + workspaceId: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.projectId), environment: z.string().trim().describe(RAW_SECRETS.CREATE.environment), secretPath: z .string() @@ -2166,7 +2166,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: secrets.length, - workspaceId: secrets[0].workspace, + projectId: secrets[0].workspace, environment: req.body.environment, secretPath: req.body.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -2194,7 +2194,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { ], body: z.object({ projectSlug: z.string().trim().optional().describe(RAW_SECRETS.DELETE.projectSlug), - workspaceId: z.string().trim().optional().describe(RAW_SECRETS.DELETE.workspaceId), + workspaceId: z.string().trim().optional().describe(RAW_SECRETS.DELETE.projectId), environment: z.string().trim().describe(RAW_SECRETS.UPDATE.environment), secretPath: z .string() @@ -2341,7 +2341,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: secrets.length, - workspaceId: secrets[0].workspace, + projectId: secrets[0].workspace, environment: req.body.environment, secretPath: req.body.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -2369,7 +2369,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { ], body: z.object({ projectSlug: z.string().trim().optional().describe(RAW_SECRETS.DELETE.projectSlug), - workspaceId: z.string().trim().optional().describe(RAW_SECRETS.DELETE.workspaceId), + workspaceId: z.string().trim().optional().describe(RAW_SECRETS.DELETE.projectId), environment: z.string().trim().describe(RAW_SECRETS.DELETE.environment), secretPath: z .string() @@ -2459,7 +2459,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { organizationId: req.permission.orgId, properties: { numberOfSecrets: secrets.length, - workspaceId: secrets[0].workspace, + projectId: secrets[0].workspace, environment: req.body.environment, secretPath: req.body.secretPath, channel: getUserAgentType(req.headers["user-agent"]), @@ -2469,95 +2469,4 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { return { secrets }; } }); - - server.route({ - method: "GET", - url: "/raw/:secretName/secret-reference-tree", - config: { - rateLimit: secretsLimit - }, - schema: { - hide: false, - tags: [ApiDocsTags.Secrets], - description: "Get secret reference tree", - security: [ - { - bearerAuth: [] - } - ], - params: z.object({ - secretName: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.secretName) - }), - querystring: z.object({ - workspaceId: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.workspaceId), - environment: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.environment), - secretPath: z - .string() - .trim() - .default("/") - .transform(removeTrailingSlash) - .describe(RAW_SECRETS.GET_REFERENCE_TREE.secretPath) - }), - response: { - 200: z.object({ - tree: SecretReferenceNodeTree, - value: z.string().optional() - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT]), - handler: async (req) => { - const { secretName } = req.params; - const { secretPath, environment, workspaceId } = req.query; - const { tree, value } = await server.services.secret.getSecretReferenceTree({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: workspaceId, - secretName, - secretPath, - environment - }); - - return { tree, value }; - } - }); - - server.route({ - method: "POST", - url: "/backfill-secret-references", - config: { - rateLimit: secretsLimit - }, - schema: { - description: "Backfill secret references", - security: [ - { - bearerAuth: [] - } - ], - body: z.object({ - projectId: z.string().trim().min(1) - }), - response: { - 200: z.object({ - message: z.string() - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT]), - handler: async (req) => { - const { projectId } = req.body; - const message = await server.services.secret.backfillSecretReferences({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId - }); - - return message; - } - }); }; diff --git a/backend/src/server/routes/v4/index.ts b/backend/src/server/routes/v4/index.ts new file mode 100644 index 000000000..db2e69222 --- /dev/null +++ b/backend/src/server/routes/v4/index.ts @@ -0,0 +1,5 @@ +import { registerSecretRouter } from "./secret-router"; + +export const registerV4Routes = async (server: FastifyZodProvider) => { + await server.register(registerSecretRouter, { prefix: "/secrets" }); +}; diff --git a/backend/src/server/routes/v4/secret-router.ts b/backend/src/server/routes/v4/secret-router.ts new file mode 100644 index 000000000..8776beb8d --- /dev/null +++ b/backend/src/server/routes/v4/secret-router.ts @@ -0,0 +1,1317 @@ +import picomatch from "picomatch"; +import { z } from "zod"; + +import { SecretApprovalRequestsSchema, SecretType, ServiceTokenScopes } from "@app/db/schemas"; +import { EventType, SecretApprovalEvent, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags, RAW_SECRETS } from "@app/lib/api-docs"; +import { BadRequestError } from "@app/lib/errors"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { secretsLimit } from "@app/server/config/rateLimiter"; +import { BaseSecretNameSchema, SecretNameSchema } from "@app/server/lib/schemas"; +import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; +import { getUserAgentType } from "@app/server/plugins/audit-log"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { ActorType, AuthMode } from "@app/services/auth/auth-type"; +import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema"; +import { SecretProtectionType } from "@app/services/secret/secret-types"; +import { SecretUpdateMode } from "@app/services/secret-v2-bridge/secret-v2-bridge-types"; +import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; + +import { SanitizedTagSchema, secretRawSchema } from "../sanitizedSchemas"; + +const SecretReferenceNode = z.object({ + key: z.string(), + value: z.string().optional(), + environment: z.string(), + secretPath: z.string() +}); + +const convertStringBoolean = (defaultValue: boolean = false) => { + return z + .enum(["true", "false"]) + .default(defaultValue ? "true" : "false") + .transform((value) => value === "true"); +}; + +type TSecretReferenceNode = z.infer & { children: TSecretReferenceNode[] }; + +const SecretReferenceNodeTree: z.ZodType = SecretReferenceNode.extend({ + children: z.lazy(() => SecretReferenceNodeTree.array()) +}); + +// TODO(depri): is service token supported in secrets router +export const registerSecretRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Secrets], + description: "List secrets", + security: [ + { + bearerAuth: [] + } + ], + 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), + projectId: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceId), + environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment), + secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath), + viewSecretValue: convertStringBoolean(true).describe(RAW_SECRETS.LIST.viewSecretValue), + expandSecretReferences: convertStringBoolean().describe(RAW_SECRETS.LIST.expand), + recursive: convertStringBoolean().describe(RAW_SECRETS.LIST.recursive), + include_imports: convertStringBoolean().describe(RAW_SECRETS.LIST.includeImports), + tagSlugs: z + .string() + .describe(RAW_SECRETS.LIST.tagSlugs) + .optional() + // split by comma and trim the strings + .transform((el) => (el ? el.split(",").map((i) => i.trim()) : [])) + }), + response: { + 200: z.object({ + secrets: secretRawSchema + .extend({ + secretPath: z.string().optional(), + secretValueHidden: z.boolean(), + secretMetadata: ResourceMetadataSchema.optional(), + tags: SanitizedTagSchema.array().optional() + }) + .array(), + imports: z + .object({ + secretPath: z.string(), + environment: z.string(), + folderId: z.string().optional(), + secrets: secretRawSchema + .omit({ createdAt: true, updatedAt: true }) + .extend({ + secretValueHidden: z.boolean(), + secretMetadata: ResourceMetadataSchema.optional() + }) + .array() + }) + .array() + .optional() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + // just for delivery hero usecase + let { secretPath, environment, projectId } = req.query; + if (req.auth.actor === ActorType.SERVICE) { + const scope = ServiceTokenScopes.parse(req.auth.serviceToken.scopes); + const isSingleScope = scope.length === 1; + if (isSingleScope && !picomatch.scan(scope[0].secretPath).isGlob) { + secretPath = scope[0].secretPath; + environment = scope[0].environment; + projectId = req.auth.serviceToken.projectId; + } + } + + if (!projectId || !environment) throw new BadRequestError({ message: "Missing workspace id or environment" }); + + const { secrets, imports } = await server.services.secret.getSecretsRaw({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + environment, + expandSecretReferences: req.query.expandSecretReferences, + actorAuthMethod: req.permission.authMethod, + projectId, + viewSecretValue: req.query.viewSecretValue, + path: secretPath, + metadataFilter: req.query.metadataFilter, + includeImports: req.query.include_imports, + recursive: req.query.recursive, + tagSlugs: req.query.tagSlugs + }); + + await server.services.auditLog.createAuditLog({ + projectId, + ...req.auditLogInfo, + event: { + type: EventType.GET_SECRETS, + metadata: { + environment, + secretPath: req.query.secretPath, + numberOfSecrets: secrets.length + } + } + }); + + if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) { + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SecretPulled, + distinctId: getTelemetryDistinctId(req), + organizationId: req.permission.orgId, + properties: { + numberOfSecrets: secrets.length, + projectId, + environment, + secretPath: req.query.secretPath, + channel: getUserAgentType(req.headers["user-agent"]), + ...req.auditLogInfo + } + }); + } + + return { secrets, imports }; + } + }); + + server.route({ + method: "GET", + url: "/raw/id/:secretId", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Secrets], + params: z.object({ + secretId: z.string() + }), + response: { + 200: z.object({ + secret: secretRawSchema.extend({ + secretPath: z.string(), + tags: SanitizedTagSchema.array().optional(), + secretMetadata: ResourceMetadataSchema.optional() + }) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { secretId } = req.params; + const secret = await server.services.secret.getSecretByIdRaw({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + secretId + }); + + return { secret }; + } + }); + + server.route({ + method: "GET", + url: "/:secretName", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Secrets], + description: "Get a secret by name", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + secretName: z.string().trim().describe(RAW_SECRETS.GET.secretName) + }), + querystring: z.object({ + projectId: z.string().trim().describe(RAW_SECRETS.GET.projectId), + environment: z.string().trim().optional().describe(RAW_SECRETS.GET.environment), + secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.GET.secretPath), + version: z.coerce.number().optional().describe(RAW_SECRETS.GET.version), + type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.GET.type), + viewSecretValue: convertStringBoolean(true).describe(RAW_SECRETS.GET.viewSecretValue), + expandSecretReferences: convertStringBoolean().describe(RAW_SECRETS.GET.expand), + include_imports: convertStringBoolean().describe(RAW_SECRETS.GET.includeImports) + }), + response: { + 200: z.object({ + secret: secretRawSchema.extend({ + secretValueHidden: z.boolean(), + secretPath: z.string(), + tags: SanitizedTagSchema.array().optional(), + secretMetadata: ResourceMetadataSchema.optional() + }) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + let { secretPath, environment, projectId } = req.query; + if (req.auth.actor === ActorType.SERVICE) { + const scope = ServiceTokenScopes.parse(req.auth.serviceToken.scopes); + const isSingleScope = scope.length === 1; + if (isSingleScope && !picomatch.scan(scope[0].secretPath).isGlob) { + secretPath = scope[0].secretPath; + environment = scope[0].environment; + projectId = req.auth.serviceToken.projectId; + } + } + + if (!environment) throw new BadRequestError({ message: "Missing environment" }); + if (!projectId) { + throw new BadRequestError({ message: "You must provide workspaceId" }); + } + + const secret = await server.services.secret.getSecretByNameRaw({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + expandSecretReferences: req.query.expandSecretReferences, + environment, + projectId, + viewSecretValue: req.query.viewSecretValue, + path: secretPath, + secretName: req.params.secretName, + type: req.query.type, + includeImports: req.query.include_imports, + version: req.query.version + }); + + await server.services.auditLog.createAuditLog({ + projectId, + ...req.auditLogInfo, + event: { + type: EventType.GET_SECRET, + metadata: { + environment, + secretPath: req.query.secretPath, + secretId: secret.id, + secretKey: req.params.secretName, + secretVersion: secret.version, + secretMetadata: secret.secretMetadata + } + } + }); + + if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) { + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SecretPulled, + organizationId: req.permission.orgId, + distinctId: getTelemetryDistinctId(req), + properties: { + numberOfSecrets: 1, + projectId, + environment, + secretPath: req.query.secretPath, + channel: getUserAgentType(req.headers["user-agent"]), + ...req.auditLogInfo + } + }); + } + return { secret }; + } + }); + + server.route({ + method: "POST", + url: "/:secretName", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Secrets], + description: "Create secret", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + secretName: SecretNameSchema.describe(RAW_SECRETS.CREATE.secretName) + }), + body: z.object({ + projectId: z.string().trim().describe(RAW_SECRETS.CREATE.projectId), + environment: z.string().trim().describe(RAW_SECRETS.CREATE.environment), + secretPath: z + .string() + .trim() + .default("/") + .transform(removeTrailingSlash) + .describe(RAW_SECRETS.CREATE.secretPath), + secretValue: z + .string() + .transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())) + .describe(RAW_SECRETS.CREATE.secretValue), + secretComment: z.string().trim().optional().default("").describe(RAW_SECRETS.CREATE.secretComment), + secretMetadata: ResourceMetadataSchema.optional(), + tagIds: z.string().array().optional().describe(RAW_SECRETS.CREATE.tagIds), + skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding), + type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.CREATE.type), + secretReminderRepeatDays: z + .number() + .optional() + .nullable() + .describe(RAW_SECRETS.CREATE.secretReminderRepeatDays), + secretReminderNote: z + .string() + .max(1024, "Secret reminder note cannot exceed 1024 characters") + .optional() + .nullable() + .describe(RAW_SECRETS.CREATE.secretReminderNote) + }), + response: { + 200: z.union([ + z.object({ + secret: secretRawSchema + }), + z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") + ]) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const secretOperation = await server.services.secret.createSecretRaw({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + environment: req.body.environment, + actorAuthMethod: req.permission.authMethod, + projectId: req.body.projectId, + secretPath: req.body.secretPath, + secretName: req.params.secretName, + type: req.body.type, + secretValue: req.body.secretValue, + skipMultilineEncoding: req.body.skipMultilineEncoding, + secretComment: req.body.secretComment, + secretMetadata: req.body.secretMetadata, + tagIds: req.body.tagIds, + secretReminderNote: req.body.secretReminderNote, + secretReminderRepeatDays: req.body.secretReminderRepeatDays + }); + if (secretOperation.type === SecretProtectionType.Approval) { + await server.services.auditLog.createAuditLog({ + projectId: req.body.projectId, + ...req.auditLogInfo, + event: { + type: EventType.SECRET_APPROVAL_REQUEST, + metadata: { + committedBy: secretOperation.approval.committerUserId, + secretApprovalRequestId: secretOperation.approval.id, + secretApprovalRequestSlug: secretOperation.approval.slug, + secretPath: req.body.secretPath, + environment: req.body.environment, + secretKey: req.params.secretName, + eventType: SecretApprovalEvent.Create + } + } + }); + + return { approval: secretOperation.approval }; + } + + const { secret } = secretOperation; + await server.services.auditLog.createAuditLog({ + projectId: req.body.projectId, + ...req.auditLogInfo, + event: { + type: EventType.CREATE_SECRET, + metadata: { + environment: req.body.environment, + secretPath: req.body.secretPath, + secretId: secret.id, + secretKey: req.params.secretName, + secretVersion: secret.version, + secretMetadata: req.body.secretMetadata + } + } + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SecretCreated, + distinctId: getTelemetryDistinctId(req), + organizationId: req.permission.orgId, + properties: { + numberOfSecrets: 1, + projectId: req.body.projectId, + environment: req.body.environment, + secretPath: req.body.secretPath, + channel: getUserAgentType(req.headers["user-agent"]), + ...req.auditLogInfo + } + }); + + return { secret }; + } + }); + + server.route({ + method: "PATCH", + url: "/:secretName", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Secrets], + description: "Update secret", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + secretName: BaseSecretNameSchema.describe(RAW_SECRETS.UPDATE.secretName) + }), + body: z.object({ + projectId: z.string().trim().describe(RAW_SECRETS.UPDATE.projectId), + environment: z.string().trim().describe(RAW_SECRETS.UPDATE.environment), + secretValue: z + .string() + .transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())) + .optional() + .describe(RAW_SECRETS.UPDATE.secretValue), + secretPath: z + .string() + .trim() + .default("/") + .transform(removeTrailingSlash) + .describe(RAW_SECRETS.UPDATE.secretPath), + skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.UPDATE.skipMultilineEncoding), + type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.UPDATE.type), + tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds), + metadata: z.record(z.string()).optional(), + secretMetadata: ResourceMetadataSchema.optional(), + secretReminderNote: z + .string() + .max(1024, "Secret reminder note cannot exceed 1024 characters") + .optional() + .nullable() + .describe(RAW_SECRETS.UPDATE.secretReminderNote), + secretReminderRepeatDays: z + .number() + .optional() + .nullable() + .describe(RAW_SECRETS.UPDATE.secretReminderRepeatDays), + secretReminderRecipients: z.string().array().optional().describe(RAW_SECRETS.UPDATE.secretReminderRecipients), + newSecretName: SecretNameSchema.optional().describe(RAW_SECRETS.UPDATE.newSecretName), + secretComment: z.string().optional().describe(RAW_SECRETS.UPDATE.secretComment) + }), + response: { + 200: z.union([ + z.object({ + secret: secretRawSchema.extend({ + secretValueHidden: z.boolean() + }) + }), + z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") + ]) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const secretOperation = await server.services.secret.updateSecretRaw({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + environment: req.body.environment, + projectId: req.body.projectId, + secretPath: req.body.secretPath, + secretName: req.params.secretName, + type: req.body.type, + secretValue: req.body.secretValue, + skipMultilineEncoding: req.body.skipMultilineEncoding, + tagIds: req.body.tagIds, + secretReminderRepeatDays: req.body.secretReminderRepeatDays, + secretReminderRecipients: req.body.secretReminderRecipients, + secretReminderNote: req.body.secretReminderNote, + metadata: req.body.metadata, + newSecretName: req.body.newSecretName, + secretComment: req.body.secretComment, + secretMetadata: req.body.secretMetadata + }); + + if (secretOperation.type === SecretProtectionType.Approval) { + await server.services.auditLog.createAuditLog({ + projectId: req.body.projectId, + ...req.auditLogInfo, + event: { + type: EventType.SECRET_APPROVAL_REQUEST, + metadata: { + committedBy: secretOperation.approval.committerUserId, + secretApprovalRequestId: secretOperation.approval.id, + secretApprovalRequestSlug: secretOperation.approval.slug, + secretPath: req.body.secretPath, + environment: req.body.environment, + secretKey: req.params.secretName, + eventType: SecretApprovalEvent.Update + } + } + }); + + return { approval: secretOperation.approval }; + } + const { secret } = secretOperation; + + await server.services.auditLog.createAuditLog({ + projectId: req.body.projectId, + ...req.auditLogInfo, + event: { + type: EventType.UPDATE_SECRET, + metadata: { + environment: req.body.environment, + secretPath: req.body.secretPath, + secretId: secret.id, + secretKey: req.params.secretName, + secretVersion: secret.version, + secretMetadata: req.body.secretMetadata + } + } + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SecretUpdated, + distinctId: getTelemetryDistinctId(req), + organizationId: req.permission.orgId, + properties: { + numberOfSecrets: 1, + projectId: req.body.projectId, + environment: req.body.environment, + secretPath: req.body.secretPath, + channel: getUserAgentType(req.headers["user-agent"]), + ...req.auditLogInfo + } + }); + return { secret }; + } + }); + + server.route({ + method: "DELETE", + url: "/:secretName", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Secrets], + description: "Delete secret", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + secretName: z.string().min(1).describe(RAW_SECRETS.DELETE.secretName) + }), + body: z.object({ + projectId: z.string().trim().describe(RAW_SECRETS.DELETE.projectId), + environment: z.string().trim().describe(RAW_SECRETS.DELETE.environment), + secretPath: z + .string() + .trim() + .default("/") + .transform(removeTrailingSlash) + .describe(RAW_SECRETS.DELETE.secretPath), + type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.DELETE.type) + }), + response: { + 200: z.union([ + z.object({ + secret: secretRawSchema.extend({ + secretValueHidden: z.boolean() + }) + }), + z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") + ]) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const secretOperation = await server.services.secret.deleteSecretRaw({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + environment: req.body.environment, + projectId: req.body.projectId, + secretPath: req.body.secretPath, + secretName: req.params.secretName, + type: req.body.type + }); + if (secretOperation.type === SecretProtectionType.Approval) { + await server.services.auditLog.createAuditLog({ + projectId: req.body.projectId, + ...req.auditLogInfo, + event: { + type: EventType.SECRET_APPROVAL_REQUEST, + metadata: { + committedBy: secretOperation.approval.committerUserId, + secretApprovalRequestId: secretOperation.approval.id, + secretApprovalRequestSlug: secretOperation.approval.slug, + secretPath: req.body.secretPath, + environment: req.body.environment, + secretKey: req.params.secretName, + eventType: SecretApprovalEvent.Delete + } + } + }); + + return { approval: secretOperation.approval }; + } + + const { secret } = secretOperation; + + await server.services.auditLog.createAuditLog({ + projectId: req.body.projectId, + ...req.auditLogInfo, + event: { + type: EventType.DELETE_SECRET, + metadata: { + environment: req.body.environment, + secretPath: req.body.secretPath, + secretId: secret.id, + secretKey: req.params.secretName, + secretVersion: secret.version + } + } + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SecretDeleted, + distinctId: getTelemetryDistinctId(req), + organizationId: req.permission.orgId, + properties: { + numberOfSecrets: 1, + projectId: req.body.projectId, + environment: req.body.environment, + secretPath: req.body.secretPath, + channel: getUserAgentType(req.headers["user-agent"]), + ...req.auditLogInfo + } + }); + + return { secret }; + } + }); + + server.route({ + method: "POST", + url: "/move", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Secrets], + body: z.object({ + projectSlug: z.string().trim(), + sourceEnvironment: z.string().trim(), + sourceSecretPath: z.string().trim().default("/").transform(removeTrailingSlash), + destinationEnvironment: z.string().trim(), + destinationSecretPath: z.string().trim().default("/").transform(removeTrailingSlash), + secretIds: z.string().array(), + shouldOverwrite: z.boolean().default(false) + }), + response: { + 200: z.object({ + isSourceUpdated: z.boolean(), + isDestinationUpdated: z.boolean() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { projectId, isSourceUpdated, isDestinationUpdated } = await server.services.secret.moveSecrets({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + projectId, + ...req.auditLogInfo, + event: { + type: EventType.MOVE_SECRETS, + metadata: { + sourceEnvironment: req.body.sourceEnvironment, + sourceSecretPath: req.body.sourceSecretPath, + destinationEnvironment: req.body.destinationEnvironment, + destinationSecretPath: req.body.destinationSecretPath, + secretIds: req.body.secretIds + } + } + }); + + return { + isSourceUpdated, + isDestinationUpdated + }; + } + }); + + server.route({ + method: "POST", + url: "/batch", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Secrets], + description: "Create many secrets", + security: [ + { + bearerAuth: [] + } + ], + body: z.object({ + projectId: z.string().trim().describe(RAW_SECRETS.UPDATE.projectId), + environment: z.string().trim().describe(RAW_SECRETS.CREATE.environment), + secretPath: z + .string() + .trim() + .default("/") + .transform(removeTrailingSlash) + .describe(RAW_SECRETS.CREATE.secretPath), + secrets: z + .object({ + secretKey: SecretNameSchema.describe(RAW_SECRETS.CREATE.secretName), + secretValue: z + .string() + .transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())) + .describe(RAW_SECRETS.CREATE.secretValue), + secretComment: z.string().trim().optional().default("").describe(RAW_SECRETS.CREATE.secretComment), + skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding), + metadata: z.record(z.string()).optional(), + secretMetadata: ResourceMetadataSchema.optional(), + tagIds: z.string().array().optional().describe(RAW_SECRETS.CREATE.tagIds) + }) + .array() + .min(1) + }), + response: { + 200: z.union([ + z.object({ + secrets: secretRawSchema.array() + }), + z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") + ]) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { environment, secretPath, secrets: inputSecrets } = req.body; + + const secretOperation = await server.services.secret.createManySecretsRaw({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + secretPath, + environment, + projectId: req.body.projectId, + secrets: inputSecrets + }); + if (secretOperation.type === SecretProtectionType.Approval) { + await server.services.auditLog.createAuditLog({ + projectId: req.body.projectId, + ...req.auditLogInfo, + event: { + type: EventType.SECRET_APPROVAL_REQUEST, + metadata: { + committedBy: secretOperation.approval.committerUserId, + secretApprovalRequestId: secretOperation.approval.id, + secretApprovalRequestSlug: secretOperation.approval.slug, + secretPath, + environment, + secrets: inputSecrets.map((secret) => ({ + secretKey: secret.secretKey + })), + eventType: SecretApprovalEvent.CreateMany + } + } + }); + return { approval: secretOperation.approval }; + } + const { secrets } = secretOperation; + + const secretMetadataMap = new Map( + inputSecrets.map(({ secretKey, secretMetadata }) => [secretKey, secretMetadata]) + ); + + await server.services.auditLog.createAuditLog({ + projectId: secrets[0].workspace, + ...req.auditLogInfo, + event: { + type: EventType.CREATE_SECRETS, + metadata: { + environment: req.body.environment, + secretPath: req.body.secretPath, + secrets: secrets.map((secret) => ({ + secretId: secret.id, + secretKey: secret.secretKey, + secretVersion: secret.version, + secretMetadata: secretMetadataMap.get(secret.secretKey) + })) + } + } + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SecretCreated, + distinctId: getTelemetryDistinctId(req), + organizationId: req.permission.orgId, + properties: { + numberOfSecrets: secrets.length, + projectId: secrets[0].workspace, + environment: req.body.environment, + secretPath: req.body.secretPath, + channel: getUserAgentType(req.headers["user-agent"]), + ...req.auditLogInfo + } + }); + return { secrets }; + } + }); + + server.route({ + method: "PATCH", + url: "/batch", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Secrets], + description: "Update many secrets", + security: [ + { + bearerAuth: [] + } + ], + body: z.object({ + projectId: z.string().trim().describe(RAW_SECRETS.DELETE.projectId), + environment: z.string().trim().describe(RAW_SECRETS.UPDATE.environment), + secretPath: z + .string() + .trim() + .default("/") + .transform(removeTrailingSlash) + .describe(RAW_SECRETS.UPDATE.secretPath), + mode: z + .nativeEnum(SecretUpdateMode) + .optional() + .default(SecretUpdateMode.FailOnNotFound) + .describe(RAW_SECRETS.UPDATE.mode), + secrets: z + .object({ + secretKey: SecretNameSchema.describe(RAW_SECRETS.UPDATE.secretName), + secretValue: z + .string() + .transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())) + .optional() + .describe(RAW_SECRETS.UPDATE.secretValue), + secretPath: z + .string() + .trim() + .transform(removeTrailingSlash) + .optional() + .describe(RAW_SECRETS.UPDATE.secretPath), + secretComment: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.secretComment), + skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.UPDATE.skipMultilineEncoding), + newSecretName: SecretNameSchema.optional().describe(RAW_SECRETS.UPDATE.newSecretName), + tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds), + secretReminderNote: z + .string() + .max(1024, "Secret reminder note cannot exceed 1024 characters") + .optional() + .nullable() + .describe(RAW_SECRETS.UPDATE.secretReminderNote), + secretMetadata: ResourceMetadataSchema.optional(), + secretReminderRepeatDays: z + .number() + .optional() + .nullable() + .describe(RAW_SECRETS.UPDATE.secretReminderRepeatDays) + }) + .array() + .min(1) + }), + response: { + 200: z.union([ + z.object({ + secrets: secretRawSchema.extend({ secretValueHidden: z.boolean() }).array() + }), + z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") + ]) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { environment, secretPath, secrets: inputSecrets } = req.body; + const secretOperation = await server.services.secret.updateManySecretsRaw({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + secretPath, + environment, + projectId: req.body.projectId, + secrets: inputSecrets, + mode: req.body.mode + }); + if (secretOperation.type === SecretProtectionType.Approval) { + await server.services.auditLog.createAuditLog({ + projectId: req.body.projectId, + ...req.auditLogInfo, + event: { + type: EventType.SECRET_APPROVAL_REQUEST, + metadata: { + committedBy: secretOperation.approval.committerUserId, + secretApprovalRequestId: secretOperation.approval.id, + secretApprovalRequestSlug: secretOperation.approval.slug, + secretPath, + environment, + secrets: inputSecrets.map((secret) => ({ + secretKey: secret.secretKey, + secretPath: secret.secretPath + })), + eventType: SecretApprovalEvent.UpdateMany + } + } + }); + return { approval: secretOperation.approval }; + } + const { secrets } = secretOperation; + + const secretMetadataMap = new Map( + inputSecrets.map(({ secretKey, secretMetadata }) => [secretKey, secretMetadata]) + ); + + await server.services.auditLog.createAuditLog({ + projectId: secrets[0].workspace, + ...req.auditLogInfo, + event: { + type: EventType.UPDATE_SECRETS, + metadata: { + environment: req.body.environment, + secretPath: req.body.secretPath, + secrets: secrets + .filter((el) => el.version > 1) + .map((secret) => ({ + secretId: secret.id, + secretPath: secret.secretPath, + secretKey: secret.secretKey, + secretVersion: secret.version, + secretMetadata: secretMetadataMap.get(secret.secretKey) + })) + } + } + }); + const createdSecrets = secrets.filter((el) => el.version === 1); + if (createdSecrets.length) { + await server.services.auditLog.createAuditLog({ + projectId: secrets[0].workspace, + ...req.auditLogInfo, + event: { + type: EventType.CREATE_SECRETS, + metadata: { + environment: req.body.environment, + secretPath: req.body.secretPath, + secrets: createdSecrets.map((secret) => ({ + secretId: secret.id, + secretPath: secret.secretPath, + secretKey: secret.secretKey, + secretVersion: secret.version, + secretMetadata: secretMetadataMap.get(secret.secretKey) + })) + } + } + }); + } + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SecretUpdated, + distinctId: getTelemetryDistinctId(req), + organizationId: req.permission.orgId, + properties: { + numberOfSecrets: secrets.length, + projectId: secrets[0].workspace, + environment: req.body.environment, + secretPath: req.body.secretPath, + channel: getUserAgentType(req.headers["user-agent"]), + ...req.auditLogInfo + } + }); + return { secrets }; + } + }); + + server.route({ + method: "DELETE", + url: "/batch", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Secrets], + description: "Delete many secrets", + security: [ + { + bearerAuth: [] + } + ], + body: z.object({ + projectId: z.string().trim().describe(RAW_SECRETS.DELETE.projectId), + environment: z.string().trim().describe(RAW_SECRETS.DELETE.environment), + secretPath: z + .string() + .trim() + .default("/") + .transform(removeTrailingSlash) + .describe(RAW_SECRETS.DELETE.secretPath), + secrets: z + .object({ + secretKey: z.string().describe(RAW_SECRETS.DELETE.secretName), + type: z.nativeEnum(SecretType).default(SecretType.Shared) + }) + .array() + .min(1) + }), + response: { + 200: z.union([ + z.object({ + secrets: secretRawSchema + .extend({ + secretValueHidden: z.boolean() + }) + .array() + }), + z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") + ]) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { environment, secretPath, secrets: inputSecrets } = req.body; + const secretOperation = await server.services.secret.deleteManySecretsRaw({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + environment, + secretPath, + projectId: req.body.projectId, + secrets: inputSecrets + }); + if (secretOperation.type === SecretProtectionType.Approval) { + await server.services.auditLog.createAuditLog({ + projectId: req.body.projectId, + ...req.auditLogInfo, + event: { + type: EventType.SECRET_APPROVAL_REQUEST, + metadata: { + committedBy: secretOperation.approval.committerUserId, + secretApprovalRequestId: secretOperation.approval.id, + secretApprovalRequestSlug: secretOperation.approval.slug, + secretPath, + environment, + secrets: inputSecrets.map((secret) => ({ + secretKey: secret.secretKey + })), + eventType: SecretApprovalEvent.DeleteMany + } + } + }); + + return { approval: secretOperation.approval }; + } + const { secrets } = secretOperation; + + await server.services.auditLog.createAuditLog({ + projectId: secrets[0].workspace, + ...req.auditLogInfo, + event: { + type: EventType.DELETE_SECRETS, + metadata: { + environment: req.body.environment, + secretPath: req.body.secretPath, + secrets: secrets.map((secret) => ({ + secretId: secret.id, + secretKey: secret.secretKey, + secretVersion: secret.version + })) + } + } + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SecretDeleted, + distinctId: getTelemetryDistinctId(req), + organizationId: req.permission.orgId, + properties: { + numberOfSecrets: secrets.length, + projectId: secrets[0].workspace, + environment: req.body.environment, + secretPath: req.body.secretPath, + channel: getUserAgentType(req.headers["user-agent"]), + ...req.auditLogInfo + } + }); + return { secrets }; + } + }); + + server.route({ + method: "GET", + url: "/:secretName/secret-reference-tree", + config: { + rateLimit: secretsLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.Secrets], + description: "Get secret reference tree", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + secretName: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.secretName) + }), + querystring: z.object({ + projectId: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.workspaceId), + environment: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.environment), + secretPath: z + .string() + .trim() + .default("/") + .transform(removeTrailingSlash) + .describe(RAW_SECRETS.GET_REFERENCE_TREE.secretPath) + }), + response: { + 200: z.object({ + tree: SecretReferenceNodeTree, + value: z.string().optional() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { secretName } = req.params; + const { secretPath, environment, projectId: workspaceId } = req.query; + const { tree, value } = await server.services.secret.getSecretReferenceTree({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: workspaceId, + secretName, + secretPath, + environment + }); + + return { tree, value }; + } + }); + + server.route({ + method: "POST", + url: "/backfill-secret-references", + config: { + rateLimit: secretsLimit + }, + schema: { + description: "Backfill secret references", + security: [ + { + bearerAuth: [] + } + ], + body: z.object({ + projectId: z.string().trim().min(1) + }), + response: { + 200: z.object({ + message: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { projectId } = req.body; + const message = await server.services.secret.backfillSecretReferences({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId + }); + + return message; + } + }); +}; diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index cd341a8b5..07f46bf1e 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -534,7 +534,8 @@ export type TSyncSecretsDTO = { }); export type TMoveSecretsDTO = { - projectSlug: string; + projectId?: string; + projectSlug?: string; sourceEnvironment: string; sourceSecretPath: string; destinationEnvironment: string; diff --git a/backend/src/services/telemetry/telemetry-types.ts b/backend/src/services/telemetry/telemetry-types.ts index 7e2027f25..a43a2f746 100644 --- a/backend/src/services/telemetry/telemetry-types.ts +++ b/backend/src/services/telemetry/telemetry-types.ts @@ -46,7 +46,7 @@ export type TSecretModifiedEvent = { properties: { numberOfSecrets: number; environment: string; - workspaceId: string; + projectId: string; secretPath: string; channel?: string; userAgent?: string; diff --git a/frontend/src/hooks/api/secrets/mutations.tsx b/frontend/src/hooks/api/secrets/mutations.tsx index 715aa108a..0dfc5db25 100644 --- a/frontend/src/hooks/api/secrets/mutations.tsx +++ b/frontend/src/hooks/api/secrets/mutations.tsx @@ -33,18 +33,18 @@ export const useCreateSecretV3 = ({ secretPath = "/", type, environment, - workspaceId, + projectId, secretKey, secretValue, secretComment, skipMultilineEncoding, tagIds }) => { - const { data } = await apiRequest.post(`/api/v3/secrets/raw/${secretKey}`, { + const { data } = await apiRequest.post(`/api/v4/secrets/${secretKey}`, { secretPath, type, environment, - workspaceId, + projectId, secretValue, secretComment, skipMultilineEncoding, @@ -52,26 +52,26 @@ export const useCreateSecretV3 = ({ }); return data; }, - onSuccess: (_, { workspaceId, environment, secretPath }) => { + onSuccess: (_, { projectId, environment, secretPath }) => { queryClient.invalidateQueries({ - queryKey: dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath }) + queryKey: dashboardKeys.getDashboardSecrets({ projectId, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }) + queryKey: secretKeys.getProjectSecret({ projectId, environment, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.list({ environment, workspaceId, directory: secretPath }) + queryKey: secretSnapshotKeys.list({ environment, projectId, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath }) + queryKey: secretSnapshotKeys.count({ environment, projectId, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.count({ projectId: workspaceId, environment, directory: secretPath }) + queryKey: commitKeys.count({ projectId, environment, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath }) + queryKey: commitKeys.history({ projectId, environment, directory: secretPath }) }); - queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) }); + queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ projectId }) }); }, ...options }); @@ -88,7 +88,7 @@ export const useUpdateSecretV3 = ({ secretPath = "/", type, environment, - workspaceId, + projectId, secretKey, secretValue, tagIds, @@ -100,8 +100,8 @@ export const useUpdateSecretV3 = ({ skipMultilineEncoding, secretMetadata }) => { - const { data } = await apiRequest.patch(`/api/v3/secrets/raw/${secretKey}`, { - workspaceId, + const { data } = await apiRequest.patch(`/api/v4/secrets/${secretKey}`, { + projectId, environment, type, secretReminderNote, @@ -117,26 +117,26 @@ export const useUpdateSecretV3 = ({ }); return data; }, - onSuccess: (_, { workspaceId, environment, secretPath }) => { + onSuccess: (_, { projectId, environment, secretPath }) => { queryClient.invalidateQueries({ - queryKey: dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath }) + queryKey: dashboardKeys.getDashboardSecrets({ projectId, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }) + queryKey: secretKeys.getProjectSecret({ projectId, environment, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.list({ environment, workspaceId, directory: secretPath }) + queryKey: secretSnapshotKeys.list({ environment, projectId, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath }) + queryKey: secretSnapshotKeys.count({ environment, projectId, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.count({ projectId: workspaceId, environment, directory: secretPath }) + queryKey: commitKeys.count({ projectId, environment, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath }) + queryKey: commitKeys.history({ projectId, environment, directory: secretPath }) }); - queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) }); + queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ projectId }) }); }, ...options }); @@ -150,17 +150,10 @@ export const useDeleteSecretV3 = ({ const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ - secretPath = "/", - type, - environment, - workspaceId, - secretKey, - secretId - }) => { - const { data } = await apiRequest.delete(`/api/v3/secrets/raw/${secretKey}`, { + mutationFn: async ({ secretPath = "/", type, environment, projectId, secretKey, secretId }) => { + const { data } = await apiRequest.delete(`/api/v4/secrets/${secretKey}`, { data: { - workspaceId, + projectId, environment, type, secretPath, @@ -169,26 +162,26 @@ export const useDeleteSecretV3 = ({ }); return data; }, - onSuccess: (_, { workspaceId, environment, secretPath }) => { + onSuccess: (_, { projectId, environment, secretPath }) => { queryClient.invalidateQueries({ - queryKey: dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath }) + queryKey: dashboardKeys.getDashboardSecrets({ projectId, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }) + queryKey: secretKeys.getProjectSecret({ projectId, environment, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.list({ environment, workspaceId, directory: secretPath }) + queryKey: secretSnapshotKeys.list({ environment, projectId, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath }) + queryKey: secretSnapshotKeys.count({ environment, projectId, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.count({ projectId: workspaceId, environment, directory: secretPath }) + queryKey: commitKeys.count({ projectId, environment, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath }) + queryKey: commitKeys.history({ projectId, environment, directory: secretPath }) }); - queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) }); + queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ projectId }) }); }, ...options }); @@ -202,35 +195,35 @@ export const useCreateSecretBatch = ({ const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ secretPath = "/", workspaceId, environment, secrets }) => { - const { data } = await apiRequest.post("/api/v3/secrets/batch/raw", { - workspaceId, + mutationFn: async ({ secretPath = "/", projectId, environment, secrets }) => { + const { data } = await apiRequest.post("/api/v4/secrets/batch", { + projectId, environment, secretPath, secrets }); return data; }, - onSuccess: (_, { workspaceId, environment, secretPath }) => { + onSuccess: (_, { projectId, environment, secretPath }) => { queryClient.invalidateQueries({ - queryKey: dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath }) + queryKey: dashboardKeys.getDashboardSecrets({ projectId, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }) + queryKey: secretKeys.getProjectSecret({ projectId, environment, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.list({ environment, workspaceId, directory: secretPath }) + queryKey: secretSnapshotKeys.list({ environment, projectId, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath }) + queryKey: secretSnapshotKeys.count({ environment, projectId, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.count({ projectId: workspaceId, environment, directory: secretPath }) + queryKey: commitKeys.count({ projectId, environment, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath }) + queryKey: commitKeys.history({ projectId, environment, directory: secretPath }) }); - queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) }); + queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ projectId }) }); }, ...options }); @@ -244,35 +237,35 @@ export const useUpdateSecretBatch = ({ const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ secretPath = "/", workspaceId, environment, secrets }) => { - const { data } = await apiRequest.patch("/api/v3/secrets/batch/raw", { - workspaceId, + mutationFn: async ({ secretPath = "/", projectId, environment, secrets }) => { + const { data } = await apiRequest.patch("/api/v4/secrets/batch", { + projectId, environment, secretPath, secrets }); return data; }, - onSuccess: (_, { workspaceId, environment, secretPath }) => { + onSuccess: (_, { projectId, environment, secretPath }) => { queryClient.invalidateQueries({ - queryKey: dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath }) + queryKey: dashboardKeys.getDashboardSecrets({ projectId, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }) + queryKey: secretKeys.getProjectSecret({ projectId, environment, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.list({ environment, workspaceId, directory: secretPath }) + queryKey: secretSnapshotKeys.list({ environment, projectId, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath }) + queryKey: secretSnapshotKeys.count({ environment, projectId, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.count({ projectId: workspaceId, environment, directory: secretPath }) + queryKey: commitKeys.count({ projectId, environment, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath }) + queryKey: commitKeys.history({ projectId, environment, directory: secretPath }) }); - queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) }); + queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ projectId }) }); }, ...options }); @@ -286,10 +279,10 @@ export const useDeleteSecretBatch = ({ const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ secretPath = "/", workspaceId, environment, secrets }) => { - const { data } = await apiRequest.delete("/api/v3/secrets/batch/raw", { + mutationFn: async ({ secretPath = "/", projectId, environment, secrets }) => { + const { data } = await apiRequest.delete("/api/v4/secrets/batch", { data: { - workspaceId, + projectId, environment, secretPath, secrets @@ -297,26 +290,26 @@ export const useDeleteSecretBatch = ({ }); return data; }, - onSuccess: (_, { workspaceId, environment, secretPath }) => { + onSuccess: (_, { projectId, environment, secretPath }) => { queryClient.invalidateQueries({ - queryKey: dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath }) + queryKey: dashboardKeys.getDashboardSecrets({ projectId, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }) + queryKey: secretKeys.getProjectSecret({ projectId, environment, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.list({ environment, workspaceId, directory: secretPath }) + queryKey: secretSnapshotKeys.list({ environment, projectId, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath }) + queryKey: secretSnapshotKeys.count({ environment, projectId, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.count({ projectId: workspaceId, environment, directory: secretPath }) + queryKey: commitKeys.count({ projectId, environment, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath }) + queryKey: commitKeys.history({ projectId, environment, directory: secretPath }) }); - queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) }); + queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ projectId }) }); }, ...options }); @@ -349,7 +342,7 @@ export const useMoveSecrets = ({ const { data } = await apiRequest.post<{ isSourceUpdated: boolean; isDestinationUpdated: boolean; - }>("/api/v3/secrets/move", { + }>("/api/v4/secrets/move", { sourceEnvironment, sourceSecretPath, projectSlug, @@ -370,7 +363,7 @@ export const useMoveSecrets = ({ }); queryClient.invalidateQueries({ queryKey: secretKeys.getProjectSecret({ - workspaceId: projectId, + projectId, environment: sourceEnvironment, secretPath: sourceSecretPath }) @@ -378,33 +371,33 @@ export const useMoveSecrets = ({ queryClient.invalidateQueries({ queryKey: secretSnapshotKeys.list({ environment: sourceEnvironment, - workspaceId: projectId, + projectId, directory: sourceSecretPath }) }); queryClient.invalidateQueries({ queryKey: secretSnapshotKeys.count({ environment: sourceEnvironment, - workspaceId: projectId, + projectId, directory: sourceSecretPath }) }); queryClient.invalidateQueries({ queryKey: commitKeys.count({ - projectId: projectId, + projectId, environment: sourceEnvironment, directory: sourceSecretPath }) }); queryClient.invalidateQueries({ queryKey: commitKeys.history({ - workspaceId: projectId, + projectId, environment: sourceEnvironment, directory: sourceSecretPath }) }); queryClient.invalidateQueries({ - queryKey: secretApprovalRequestKeys.count({ workspaceId: projectId }) + queryKey: secretApprovalRequestKeys.count({ projectId }) }); }, ...options @@ -432,16 +425,16 @@ export const useCreateCommit = () => { object, object, { - workspaceId: string; + projectId: string; environment: string; secretPath: string; pendingChanges: PendingChanges; message: string; } >({ - mutationFn: async ({ workspaceId, environment, secretPath, pendingChanges, message }) => { + mutationFn: async ({ projectId, environment, secretPath, pendingChanges, message }) => { const { data } = await apiRequest.post("/api/v1/pit/batch/commit", { - projectId: workspaceId, + projectId, environment, secretPath, changes: { @@ -501,26 +494,26 @@ export const useCreateCommit = () => { }); return data; }, - onSuccess: (_, { workspaceId, environment, secretPath }) => { + onSuccess: (_, { projectId, environment, secretPath }) => { queryClient.invalidateQueries({ - queryKey: dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath }) + queryKey: dashboardKeys.getDashboardSecrets({ projectId, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }) + queryKey: secretKeys.getProjectSecret({ projectId, environment, secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.list({ environment, workspaceId, directory: secretPath }) + queryKey: secretSnapshotKeys.list({ environment, projectId, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath }) + queryKey: secretSnapshotKeys.count({ environment, projectId, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.count({ projectId: workspaceId, environment, directory: secretPath }) + queryKey: commitKeys.count({ projectId, environment, directory: secretPath }) }); queryClient.invalidateQueries({ - queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath }) + queryKey: commitKeys.history({ projectId, environment, directory: secretPath }) }); - queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) }); + queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ projectId }) }); } }); }; diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index 443b74df8..df3a78c35 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -27,35 +27,35 @@ import { export const secretKeys = { // this is also used in secretSnapshot part getProjectSecret: ({ - workspaceId, + projectId, environment, secretPath, viewSecretValue }: TGetProjectSecretsKey) => - [{ workspaceId, environment, secretPath, viewSecretValue }, "secrets"] as const, + [{ projectId, environment, secretPath, viewSecretValue }, "secrets"] as const, getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"] as const, getSecretAccessList: ({ - workspaceId, + projectId, environment, secretPath, secretKey }: TGetSecretAccessListDTO) => - ["secret-access-list", { workspaceId, environment, secretPath, secretKey }] as const, + ["secret-access-list", { projectId, environment, secretPath, secretKey }] as const, getSecretReferenceTree: (dto: TGetSecretReferenceTreeDTO) => ["secret-reference-tree", dto] }; export const fetchProjectSecrets = async ({ - workspaceId, + projectId, environment, secretPath, includeImports, expandSecretReferences, viewSecretValue }: TGetProjectSecretsKey) => { - const { data } = await apiRequest.get("/api/v3/secrets/raw", { + const { data } = await apiRequest.get("/api/v4/secrets", { params: { environment, - workspaceId, + projectId, secretPath, viewSecretValue, expandSecretReferences, @@ -116,7 +116,7 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => { }; export const useGetProjectSecrets = ({ - workspaceId, + projectId, environment, secretPath, viewSecretValue, @@ -135,14 +135,14 @@ export const useGetProjectSecrets = ({ useQuery({ ...options, // wait for all values to be available - enabled: Boolean(workspaceId && environment) && (options?.enabled ?? true), + enabled: Boolean(projectId && environment) && (options?.enabled ?? true), queryKey: secretKeys.getProjectSecret({ - workspaceId, + projectId, environment, secretPath, viewSecretValue }), - queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath, viewSecretValue }), + queryFn: () => fetchProjectSecrets({ projectId, environment, secretPath, viewSecretValue }), select: useCallback( (data: Awaited>) => mergePersonalSecrets(data.secrets), [] @@ -150,7 +150,7 @@ export const useGetProjectSecrets = ({ }); export const useGetProjectSecretsAllEnv = ({ - workspaceId, + projectId, envs, secretPath }: TGetProjectSecretsAllEnvDTO) => { @@ -159,11 +159,11 @@ export const useGetProjectSecretsAllEnv = ({ const secrets = useQueries({ queries: envs.map((environment) => ({ queryKey: secretKeys.getProjectSecret({ - workspaceId, + projectId, environment, secretPath }), - enabled: Boolean(workspaceId && environment), + enabled: Boolean(projectId && environment), onError: (error: unknown) => { if (axios.isAxiosError(error) && !isErrorHandled) { const { message, requestId } = error.response?.data as { @@ -187,7 +187,7 @@ export const useGetProjectSecretsAllEnv = ({ setIsErrorHandled.on(); } }, - queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath }), + queryFn: () => fetchProjectSecrets({ projectId, environment, secretPath }), staleTime: 60 * 1000, // eslint-disable-next-line react-hooks/rules-of-hooks select: useCallback( @@ -270,7 +270,7 @@ export const useGetSecretAccessList = (dto: TGetSecretAccessListDTO) => users: SecretAccessListEntry[]; }>(`/api/v1/secrets/${dto.secretKey}/access-list`, { params: { - workspaceId: dto.workspaceId, + projectId: dto.projectId, environment: dto.environment, secretPath: dto.secretPath } @@ -287,11 +287,11 @@ const fetchSecretReferenceTree = async ({ environmentSlug }: TGetSecretReferenceTreeDTO) => { const { data } = await apiRequest.get<{ tree: TSecretReferenceTraceNode; value: string }>( - `/api/v3/secrets/raw/${secretKey}/secret-reference-tree`, + `/api/v4/secrets/${secretKey}/secret-reference-tree`, { params: { secretPath, - workspaceId: projectId, + projectId, environment: environmentSlug } } diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index 1b232cfac..95855992a 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -17,30 +17,6 @@ export type SecretReminderRecipient = { }; id: string; }; -export type EncryptedSecret = { - id: string; - version: number; - workspace: string; - type: SecretType; - environment: string; - secretKeyCiphertext: string; - secretKeyIV: string; - secretKeyTag: string; - secretValueCiphertext: string; - secretValueIV: string; - secretValueTag: string; - secretValueHidden: boolean; - __v: number; - createdAt: string; - updatedAt: string; - skipMultilineEncoding?: boolean; - secretCommentCiphertext: string; - secretCommentIV: string; - secretCommentTag: string; - secretReminderRepeatDays?: number | null; - secretReminderNote?: string | null; - tags: WsTag[]; -}; // both personal and shared secret stitched together for dashboard export type SecretV3RawSanitized = { @@ -76,7 +52,7 @@ export type SecretV3RawSanitized = { export type SecretV3Raw = { id: string; _id: string; - workspace: string; + project: string; environment: string; version: number; type: string; @@ -113,7 +89,7 @@ export type SecretVersions = { id: string; secretId: string; version: number; - workspace: string; + project: string; type: SecretType; isDeleted: boolean; envId: string; @@ -136,7 +112,7 @@ export type SecretVersions = { // dto export type TGetProjectSecretsKey = { - workspaceId: string; + projectId: string; environment: string; secretPath?: string; includeImports?: boolean; @@ -148,7 +124,7 @@ export type TGetProjectSecretsKey = { export type TGetProjectSecretsDTO = TGetProjectSecretsKey; export type TGetProjectSecretsAllEnvDTO = { - workspaceId: string; + projectId: string; envs: string[]; folderId?: string; secretPath?: string; @@ -162,7 +138,7 @@ export type GetSecretVersionsDTO = { }; export type TGetSecretAccessListDTO = { - workspaceId: string; + projectId: string; environment: string; secretPath: string; secretKey: string; @@ -174,14 +150,14 @@ export type TCreateSecretsV3DTO = { secretComment: string; skipMultilineEncoding?: boolean; secretPath: string; - workspaceId: string; + projectId: string; environment: string; type: SecretType; tagIds?: string[]; }; export type TUpdateSecretsV3DTO = { - workspaceId: string; + projectId: string; environment: string; secretPath: string; type: SecretType; @@ -198,7 +174,7 @@ export type TUpdateSecretsV3DTO = { }; export type TDeleteSecretsV3DTO = { - workspaceId: string; + projectId: string; environment: string; type: SecretType; secretPath: string; @@ -207,7 +183,7 @@ export type TDeleteSecretsV3DTO = { }; export type TCreateSecretBatchDTO = { - workspaceId: string; + projectId: string; environment: string; secretPath: string; secrets: Array<{ @@ -224,7 +200,7 @@ export type TCreateSecretBatchDTO = { }; export type TUpdateSecretBatchDTO = { - workspaceId: string; + projectId: string; environment: string; secretPath: string; secrets: Array<{ @@ -241,7 +217,7 @@ export type TUpdateSecretBatchDTO = { }; export type TDeleteSecretBatchDTO = { - workspaceId: string; + projectId: string; environment: string; secretPath: string; secrets: Array<{