diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index efc1cb865..118b2d753 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1073,6 +1073,7 @@ export const registerRoutes = async ( const secretService = secretServiceFactory({ folderDAL, secretVersionDAL, + secretV2BridgeDAL, secretVersionTagDAL, secretBlindIndexDAL, permissionService, diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index a5dc39485..4935345dc 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -380,6 +380,48 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "GET", + url: "/raw/id/:secretId", + config: { + rateLimit: secretsLimit + }, + schema: { + params: z.object({ + secretId: z.string() + }), + response: { + 200: z.object({ + secret: secretRawSchema.extend({ + secretPath: z.string(), + tags: SecretTagsSchema.pick({ + id: true, + slug: true, + color: true + }) + .extend({ name: z.string() }) + .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: "/raw/:secretName", 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 99980fba7..b4619abd3 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 @@ -613,6 +613,9 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id` ) + + .leftJoin(TableName.SecretFolder, `${TableName.SecretV2}.folderId`, `${TableName.SecretFolder}.id`) + .leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) .leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`) .select(selectAllTableCols(TableName.SecretV2)) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) @@ -622,12 +625,13 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"), db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"), db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue") - ); + ) + .select(db.ref("projectId").withSchema(TableName.Environment).as("projectId")); const docs = sqlNestRelationships({ data: rawDocs, key: "id", - parentMapper: (el) => ({ _id: el.id, ...SecretsV2Schema.parse(el) }), + parentMapper: (el) => ({ _id: el.id, projectId: el.projectId, ...SecretsV2Schema.parse(el) }), childrenMapper: [ { key: "tagId", diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 9063da5c4..ef1ff9788 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -28,6 +28,7 @@ import { KmsDataKey } from "../kms/kms-types"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal"; import { TSecretQueueFactory } from "../secret/secret-queue"; +import { TGetASecretByIdDTO } from "../secret/secret-types"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns"; @@ -73,7 +74,13 @@ type TSecretV2BridgeServiceFactoryDep = { projectEnvDAL: Pick; folderDAL: Pick< TSecretFolderDALFactory, - "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" | "findBySecretPathMultiEnv" + | "findBySecretPath" + | "updateById" + | "findById" + | "findByManySecretPath" + | "find" + | "findBySecretPathMultiEnv" + | "findSecretPathByFolderIds" >; secretImportDAL: Pick; secretQueueService: Pick; @@ -955,6 +962,70 @@ export const secretV2BridgeServiceFactory = ({ }; }; + const getSecretById = async ({ actorId, actor, actorOrgId, actorAuthMethod, secret }: TGetASecretByIdDTO) => { + const folder = await folderDAL.findById(secret.folderId); + if (!folder) { + throw new NotFoundError({ + message: `Folder with id '${secret.folderId}' not found`, + name: "GetSecretById" + }); + } + + const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(folder.projectId, [folder.id]); + + if (!folderWithPath) { + throw new NotFoundError({ + message: `Folder with id '${folder.id}' not found`, + name: "GetSecretById" + }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: folder.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { + environment: folder.environment.envSlug, + secretPath: folderWithPath.path, + secretName: secret.key, + secretTags: secret.tags.map((i) => i.slug) + }) + ); + + if (secret.type === SecretType.Personal && secret.userId !== actorId) { + throw new ForbiddenRequestError({ + message: "You are not allowed to access this secret", + name: "GetSecretById" + }); + } + + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId: folder.projectId + }); + + const secretValue = secret.encryptedValue + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() + : ""; + + const secretComment = secret.encryptedComment + ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() + : ""; + + return reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, folderWithPath.path, { + ...secret, + value: secretValue, + comment: secretComment + }); + }; + const getSecretByName = async ({ actorId, actor, @@ -2237,6 +2308,7 @@ export const secretV2BridgeServiceFactory = ({ getSecretsCountMultiEnv, getSecretsMultiEnv, getSecretReferenceTree, - getSecretsByFolderMappings + getSecretsByFolderMappings, + getSecretById }; }; diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 93f68e813..96e0ab351 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -9,7 +9,8 @@ import { SecretEncryptionAlgo, SecretKeyEncoding, SecretsSchema, - SecretType + SecretType, + TableName } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; @@ -44,6 +45,7 @@ import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; import { fnSecretsFromImports } from "../secret-import/secret-import-fns"; import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal"; +import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; import { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bridge-service"; import { TGetSecretReferencesTreeDTO } from "../secret-v2-bridge/secret-v2-bridge-types"; import { TSecretDALFactory } from "./secret-dal"; @@ -71,6 +73,7 @@ import { TDeleteManySecretRawDTO, TDeleteSecretDTO, TDeleteSecretRawDTO, + TGetASecretByIdRawDTO, TGetASecretDTO, TGetASecretRawDTO, TGetSecretAccessListDTO, @@ -89,13 +92,14 @@ import { TSecretVersionTagDALFactory } from "./secret-version-tag-dal"; type TSecretServiceFactoryDep = { secretDAL: TSecretDALFactory; + secretV2BridgeDAL: Pick; secretTagDAL: TSecretTagDALFactory; secretVersionDAL: TSecretVersionDALFactory; projectDAL: Pick; projectEnvDAL: Pick; folderDAL: Pick< TSecretFolderDALFactory, - "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" + "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" | "findSecretPathByFolderIds" >; secretV2BridgeService: TSecretV2BridgeServiceFactory; secretBlindIndexDAL: TSecretBlindIndexDALFactory; @@ -124,6 +128,7 @@ type TSecretServiceFactoryDep = { export type TSecretServiceFactory = ReturnType; export const secretServiceFactory = ({ secretDAL, + secretV2BridgeDAL, projectEnvDAL, secretTagDAL, secretVersionDAL, @@ -1382,6 +1387,36 @@ export const secretServiceFactory = ({ }; }; + const getSecretByIdRaw = async ({ secretId, actorId, actor, actorOrgId, actorAuthMethod }: TGetASecretByIdRawDTO) => { + const sec = await secretV2BridgeDAL.findOneWithTags({ + [`${TableName.SecretV2}.id` as "id"]: secretId + }); + + if (!sec) { + throw new NotFoundError({ + message: `Secret with id '${secretId}' not found`, + name: "GetSecretById" + }); + } + + const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(sec.projectId); + + if (shouldUseSecretV2Bridge) { + const secret = await secretV2BridgeService.getSecretById({ + secret: sec, + actorId, + actor, + actorOrgId, + actorAuthMethod + }); + + return secret; + } + throw new BadRequestError({ + message: "Project version not supported. Please upgrade your project." + }); + }; + const getSecretByNameRaw = async ({ type, path, @@ -3088,6 +3123,7 @@ export const secretServiceFactory = ({ getSecretsRawMultiEnv, getSecretReferenceTree, getSecretsRawByFolderMappings, - getSecretAccessList + getSecretAccessList, + getSecretByIdRaw }; }; diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index 158605276..371752594 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -1,7 +1,7 @@ import { Knex } from "knex"; import { z } from "zod"; -import { SecretType, TSecretBlindIndexes, TSecrets, TSecretsInsert, TSecretsUpdate } from "@app/db/schemas"; +import { SecretType, TSecretBlindIndexes, TSecrets, TSecretsInsert, TSecretsUpdate, TSecretsV2 } from "@app/db/schemas"; import { OrderByDirection, TProjectPermission } from "@app/lib/types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; @@ -121,6 +121,17 @@ export type TGetASecretDTO = { version?: number; } & TProjectPermission; +export type TGetASecretByIdDTO = { + secret: TSecretsV2 & { + tags: { + id: string; + color?: string | null; + slug: string; + name: string; + }[]; + }; +} & Omit; + export type TCreateBulkSecretDTO = { path: string; environment: string; @@ -213,6 +224,10 @@ export type TGetASecretRawDTO = { projectId?: string; } & Omit; +export type TGetASecretByIdRawDTO = { + secretId: string; +} & Omit; + export type TCreateSecretRawDTO = TProjectPermission & { secretName: string; secretPath: string;