From 50610945bec32b1c29bb48853d85a1e92e81e405 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 5 Mar 2025 05:40:57 +0400 Subject: [PATCH 1/3] feat: get secret by ID --- backend/src/server/routes/index.ts | 1 + backend/src/server/routes/v3/secret-router.ts | 42 ++++++++++ .../secret-v2-bridge/secret-v2-bridge-dal.ts | 8 +- .../secret-v2-bridge-service.ts | 76 ++++++++++++++++++- backend/src/services/secret/secret-service.ts | 42 +++++++++- backend/src/services/secret/secret-types.ts | 17 ++++- 6 files changed, 178 insertions(+), 8 deletions(-) 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; From 77431b47197e77e1bc372defdaa84ff9bf0359c5 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 6 Mar 2025 20:00:11 +0400 Subject: [PATCH 2/3] requested changes --- backend/src/server/routes/index.ts | 1 - .../secret-v2-bridge-service.ts | 23 ++++--- backend/src/services/secret/secret-service.ts | 38 +++--------- backend/src/services/secret/secret-types.ts | 11 +--- .../SecretListView/SecretDetailSidebar.tsx | 61 +++++++++++++------ 5 files changed, 64 insertions(+), 70 deletions(-) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 118b2d753..efc1cb865 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1073,7 +1073,6 @@ export const registerRoutes = async ( const secretService = secretServiceFactory({ folderDAL, secretVersionDAL, - secretV2BridgeDAL, secretVersionTagDAL, secretBlindIndexDAL, permissionService, 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 ef1ff9788..1891b3eac 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 @@ -962,20 +962,23 @@ export const secretV2BridgeServiceFactory = ({ }; }; - const getSecretById = async ({ actorId, actor, actorOrgId, actorAuthMethod, secret }: TGetASecretByIdDTO) => { - const folder = await folderDAL.findById(secret.folderId); - if (!folder) { + const getSecretById = async ({ actorId, actor, actorOrgId, actorAuthMethod, secretId }: TGetASecretByIdDTO) => { + const secret = await secretDAL.findOneWithTags({ + id: secretId + }); + + if (!secret) { throw new NotFoundError({ - message: `Folder with id '${secret.folderId}' not found`, + message: `Secret with ID '${secretId}' not found`, name: "GetSecretById" }); } - const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(folder.projectId, [folder.id]); + const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(secret.projectId, [secret.folderId]); if (!folderWithPath) { throw new NotFoundError({ - message: `Folder with id '${folder.id}' not found`, + message: `Folder with id '${secret.folderId}' not found`, name: "GetSecretById" }); } @@ -983,7 +986,7 @@ export const secretV2BridgeServiceFactory = ({ const { permission } = await permissionService.getProjectPermission({ actor, actorId, - projectId: folder.projectId, + projectId: secret.projectId, actorAuthMethod, actorOrgId, actionProjectType: ActionProjectType.SecretManager @@ -992,7 +995,7 @@ export const secretV2BridgeServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { - environment: folder.environment.envSlug, + environment: folderWithPath.environmentSlug, secretPath: folderWithPath.path, secretName: secret.key, secretTags: secret.tags.map((i) => i.slug) @@ -1008,7 +1011,7 @@ export const secretV2BridgeServiceFactory = ({ const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, - projectId: folder.projectId + projectId: secret.projectId }); const secretValue = secret.encryptedValue @@ -1019,7 +1022,7 @@ export const secretV2BridgeServiceFactory = ({ ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() : ""; - return reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, folderWithPath.path, { + return reshapeBridgeSecret(secret.projectId, folderWithPath.environmentSlug, folderWithPath.path, { ...secret, value: secretValue, comment: secretComment diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 96e0ab351..cfb47d1dd 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -9,8 +9,7 @@ import { SecretEncryptionAlgo, SecretKeyEncoding, SecretsSchema, - SecretType, - TableName + SecretType } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; @@ -45,7 +44,6 @@ 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"; @@ -92,7 +90,6 @@ import { TSecretVersionTagDALFactory } from "./secret-version-tag-dal"; type TSecretServiceFactoryDep = { secretDAL: TSecretDALFactory; - secretV2BridgeDAL: Pick; secretTagDAL: TSecretTagDALFactory; secretVersionDAL: TSecretVersionDALFactory; projectDAL: Pick; @@ -128,7 +125,6 @@ type TSecretServiceFactoryDep = { export type TSecretServiceFactory = ReturnType; export const secretServiceFactory = ({ secretDAL, - secretV2BridgeDAL, projectEnvDAL, secretTagDAL, secretVersionDAL, @@ -1388,33 +1384,15 @@ export const secretServiceFactory = ({ }; const getSecretByIdRaw = async ({ secretId, actorId, actor, actorOrgId, actorAuthMethod }: TGetASecretByIdRawDTO) => { - const sec = await secretV2BridgeDAL.findOneWithTags({ - [`${TableName.SecretV2}.id` as "id"]: secretId + const secret = await secretV2BridgeService.getSecretById({ + secretId, + actorId, + actor, + actorOrgId, + actorAuthMethod }); - 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." - }); + return secret; }; const getSecretByNameRaw = async ({ diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index 371752594..46aedc2ee 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, TSecretsV2 } from "@app/db/schemas"; +import { SecretType, TSecretBlindIndexes, TSecrets, TSecretsInsert, TSecretsUpdate } 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"; @@ -122,14 +122,7 @@ export type TGetASecretDTO = { } & TProjectPermission; export type TGetASecretByIdDTO = { - secret: TSecretsV2 & { - tags: { - id: string; - color?: string | null; - slug: string; - name: string; - }[]; - }; + secretId: string; } & Omit; export type TCreateBulkSecretDTO = { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx index d78815222..5e7af286d 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -5,6 +5,7 @@ import { faArrowRotateRight, faCheckCircle, faClock, + faCopy, faDesktop, faEyeSlash, faPlus, @@ -990,29 +991,49 @@ export const SecretDetailSidebar = ({ )} - - {(isAllowed) => ( +
+ { + await navigator.clipboard.writeText(secret.id); + + createNotification({ + title: "Secret ID Copied", + text: "The secret ID has been copied to your clipboard.", + type: "success" + }); + }} > - - - + - )} - + + + {(isAllowed) => ( + + + + + + )} + +
From 07b93c5cec7ab6f1438f2bb36ee640a320f87f41 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 6 Mar 2025 20:36:45 +0400 Subject: [PATCH 3/3] Update secret-v2-bridge-service.ts --- .../src/services/secret-v2-bridge/secret-v2-bridge-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1891b3eac..d6ba02856 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 @@ -964,7 +964,7 @@ export const secretV2BridgeServiceFactory = ({ const getSecretById = async ({ actorId, actor, actorOrgId, actorAuthMethod, secretId }: TGetASecretByIdDTO) => { const secret = await secretDAL.findOneWithTags({ - id: secretId + [`${TableName.SecretV2}.id` as "id"]: secretId }); if (!secret) {