From 96c0e718d03905f1428e9c118711f9e9f418968a Mon Sep 17 00:00:00 2001 From: = Date: Mon, 14 Oct 2024 17:37:51 +0530 Subject: [PATCH] feat: added auto ghost user creation and fixed ghost user creation in v1 --- backend/src/server/routes/index.ts | 12 ++- backend/src/services/org/org-service.ts | 94 +++++++++++++++---- .../services/project-bot/project-bot-fns.ts | 16 ++-- .../project-bot/project-bot-service.ts | 4 +- backend/src/services/secret/secret-queue.ts | 72 +++++++++++++- 5 files changed, 166 insertions(+), 32 deletions(-) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index c195c92d7..2b84e2881 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -493,6 +493,9 @@ export const registerRoutes = async ( authDAL, userDAL }); + + const projectBotService = projectBotServiceFactory({ permissionService, projectBotDAL, projectDAL }); + const orgService = orgServiceFactory({ userAliasDAL, identityMetadataDAL, @@ -515,7 +518,8 @@ export const registerRoutes = async ( userDAL, groupDAL, orgBotDAL, - oidcConfigDAL + oidcConfigDAL, + projectBotService }); const signupService = authSignupServiceFactory({ tokenService, @@ -574,7 +578,6 @@ export const registerRoutes = async ( secretScanningDAL, secretScanningQueue }); - const projectBotService = projectBotServiceFactory({ permissionService, projectBotDAL, projectDAL }); const projectMembershipService = projectMembershipServiceFactory({ projectMembershipDAL, @@ -838,7 +841,10 @@ export const registerRoutes = async ( integrationAuthDAL, snapshotDAL, snapshotSecretV2BridgeDAL, - secretApprovalRequestDAL + secretApprovalRequestDAL, + projectKeyDAL, + projectUserMembershipRoleDAL, + orgService }); const secretImportService = secretImportServiceFactory({ licenseService, diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index fd9b8106b..b4b8775f0 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -41,8 +41,9 @@ import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TokenType } from "../auth-token/auth-token-types"; import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal"; import { TProjectDALFactory } from "../project/project-dal"; -import { assignWorkspaceKeysToMembers } from "../project/project-fns"; +import { assignWorkspaceKeysToMembers, createProjectKey } from "../project/project-fns"; import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; +import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; @@ -80,7 +81,7 @@ type TOrgServiceFactoryDep = { TProjectMembershipDALFactory, "findProjectMembershipsByUserId" | "delete" | "create" | "find" | "insertMany" | "transaction" >; - projectKeyDAL: Pick; + projectKeyDAL: Pick; orgMembershipDAL: Pick; incidentContactDAL: TIncidentContactsDALFactory; samlConfigDAL: Pick; @@ -94,8 +95,9 @@ type TOrgServiceFactoryDep = { >; projectUserAdditionalPrivilegeDAL: Pick; projectRoleDAL: Pick; - projectBotDAL: Pick; - projectUserMembershipRoleDAL: Pick; + projectBotDAL: Pick; + projectUserMembershipRoleDAL: Pick; + projectBotService: Pick; }; export type TOrgServiceFactory = ReturnType; @@ -122,7 +124,8 @@ export const orgServiceFactory = ({ oidcConfigDAL, projectBotDAL, projectUserMembershipRoleDAL, - identityMetadataDAL + identityMetadataDAL, + projectBotService }: TOrgServiceFactoryDep) => { /* * Get organization details by the organization id @@ -718,20 +721,67 @@ export const orgServiceFactory = ({ const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); - const ghostUser = await projectDAL.findProjectGhostUser(projectId, tx); - if (!ghostUser) { - throw new NotFoundError({ - name: "InviteUser", - message: "Failed to find project owner" - }); - } + // this will auto generate bot + const { botKey, bot: autoGeneratedBot } = await projectBotService.getBotKey(projectId, true); - const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId, tx); - if (!ghostUserLatestKey) { - throw new NotFoundError({ - name: "InviteUser", - message: "Failed to find project owner's latest key" + const ghostUser = await projectDAL.findProjectGhostUser(projectId, tx); + let ghostUserId = ghostUser?.id; + + // backfill missing ghost user + if (!ghostUserId) { + const newGhostUser = await addGhostUser(project.orgId, tx); + const projectMembership = await projectMembershipDAL.create( + { + userId: newGhostUser.user.id, + projectId: project.id + }, + tx + ); + await projectUserMembershipRoleDAL.create( + { projectMembershipId: projectMembership.id, role: ProjectMembershipRole.Admin }, + tx + ); + + const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({ + publicKey: newGhostUser.keys.publicKey, + privateKey: newGhostUser.keys.plainPrivateKey, + plainProjectKey: botKey }); + + // 4. Save the project key for the ghost user. + await projectKeyDAL.create( + { + projectId: project.id, + receiverId: newGhostUser.user.id, + encryptedKey: encryptedProjectKey, + nonce: encryptedProjectKeyIv, + senderId: newGhostUser.user.id + }, + tx + ); + + const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt( + newGhostUser.keys.plainPrivateKey + ); + if (autoGeneratedBot) { + await projectBotDAL.updateById( + autoGeneratedBot.id, + { + tag, + iv, + encryptedProjectKey, + encryptedProjectKeyNonce: encryptedProjectKeyIv, + encryptedPrivateKey: ciphertext, + isActive: true, + publicKey: newGhostUser.keys.publicKey, + senderId: newGhostUser.user.id, + algorithm, + keyEncoding: encoding + }, + tx + ); + } + ghostUserId = newGhostUser.user.id; } const bot = await projectBotDAL.findOne({ projectId }, tx); @@ -742,6 +792,14 @@ export const orgServiceFactory = ({ }); } + const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUserId, projectId, tx); + if (!ghostUserLatestKey) { + throw new NotFoundError({ + name: "InviteUser", + message: "Failed to find project owner's latest key" + }); + } + const botPrivateKey = infisicalSymmetricDecrypt({ keyEncoding: bot.keyEncoding as SecretKeyEncoding, iv: bot.iv, @@ -785,7 +843,7 @@ export const orgServiceFactory = ({ newWsMembers.map((el) => ({ encryptedKey: el.workspaceEncryptedKey, nonce: el.workspaceEncryptedNonce, - senderId: ghostUser.id, + senderId: ghostUserId, receiverId: el.orgMembershipId, projectId })), diff --git a/backend/src/services/project-bot/project-bot-fns.ts b/backend/src/services/project-bot/project-bot-fns.ts index 252fd03e5..315ef2e0d 100644 --- a/backend/src/services/project-bot/project-bot-fns.ts +++ b/backend/src/services/project-bot/project-bot-fns.ts @@ -24,14 +24,14 @@ export const getBotKeyFnFactory = ( projectBotDAL: TProjectBotDALFactory, projectDAL: Pick ) => { - const getBotKeyFn = async (projectId: string) => { + const getBotKeyFn = async (projectId: string, shouldGetBotKey?: boolean) => { const project = await projectDAL.findById(projectId); if (!project) throw new NotFoundError({ message: "Project not found during bot lookup. Are you sure you are using the correct project ID?" }); - if (project.version === 3) { + if (project.version === 3 && !shouldGetBotKey) { return { project, shouldUseSecretV2Bridge: true }; } @@ -65,8 +65,9 @@ export const getBotKeyFnFactory = ( const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(botKey.privateKey); const encryptedWorkspaceKey = encryptAsymmetric(workspaceKey, botKey.publicKey, userPrivateKey); + let botId; if (!bot) { - await projectBotDAL.create({ + const newBot = await projectBotDAL.create({ name: "Infisical Bot (Ghost)", projectId, isActive: true, @@ -80,8 +81,9 @@ export const getBotKeyFnFactory = ( encryptedProjectKeyNonce: encryptedWorkspaceKey.nonce, senderId: projectV1Keys.userId }); + botId = newBot.id; } else { - await projectBotDAL.updateById(bot.id, { + const updatedBot = await projectBotDAL.updateById(bot.id, { isActive: true, tag, iv, @@ -93,8 +95,10 @@ export const getBotKeyFnFactory = ( encryptedProjectKeyNonce: encryptedWorkspaceKey.nonce, senderId: projectV1Keys.userId }); + botId = updatedBot.id; } - return { botKey: workspaceKey, project, shouldUseSecretV2Bridge: false }; + + return { botKey: workspaceKey, project, shouldUseSecretV2Bridge: false, bot: { id: botId } }; } const botPrivateKey = getBotPrivateKey({ bot }); @@ -104,7 +108,7 @@ export const getBotKeyFnFactory = ( nonce: bot.encryptedProjectKeyNonce, publicKey: bot.sender.publicKey }); - return { botKey, project, shouldUseSecretV2Bridge: false }; + return { botKey, project, shouldUseSecretV2Bridge: false, bot: { id: bot.id } }; }; return getBotKeyFn; diff --git a/backend/src/services/project-bot/project-bot-service.ts b/backend/src/services/project-bot/project-bot-service.ts index cc327df54..6a6178c9f 100644 --- a/backend/src/services/project-bot/project-bot-service.ts +++ b/backend/src/services/project-bot/project-bot-service.ts @@ -27,8 +27,8 @@ export const projectBotServiceFactory = ({ }: TProjectBotServiceFactoryDep) => { const getBotKeyFn = getBotKeyFnFactory(projectBotDAL, projectDAL); - const getBotKey = async (projectId: string) => { - return getBotKeyFn(projectId); + const getBotKey = async (projectId: string, shouldGetBotKey?: boolean) => { + return getBotKeyFn(projectId, shouldGetBotKey); }; const findBotByProjectId = async ({ diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 15fd33027..4076b179f 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -17,6 +17,7 @@ import { TSnapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/sn import { KeyStorePrefixes, KeyStoreTtls, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; +import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { daysToMillisecond, secondsToMillis } from "@app/lib/dates"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { getTimeDifferenceInSeconds, groupBy, isSamePath, unique } from "@app/lib/fn"; @@ -37,10 +38,14 @@ import { syncIntegrationSecrets } from "../integration-auth/integration-sync-sec import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; import { TOrgDALFactory } from "../org/org-dal"; +import { TOrgServiceFactory } from "../org/org-service"; import { TProjectDALFactory } from "../project/project-dal"; +import { createProjectKey } from "../project/project-fns"; import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; +import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; +import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns"; @@ -77,7 +82,8 @@ type TSecretQueueFactoryDep = { projectEnvDAL: Pick; projectDAL: TProjectDALFactory; projectBotDAL: TProjectBotDALFactory; - projectMembershipDAL: Pick; + projectKeyDAL: Pick; + projectMembershipDAL: Pick; smtpService: TSmtpService; orgDAL: Pick; secretVersionDAL: TSecretVersionDALFactory; @@ -95,6 +101,8 @@ type TSecretQueueFactoryDep = { snapshotSecretV2BridgeDAL: Pick; keyStore: Pick; auditLogService: Pick; + orgService: Pick; + projectUserMembershipRoleDAL: Pick; }; export type TGetSecrets = { @@ -111,6 +119,8 @@ type TIntegrationSecret = Record< string, { value: string; comment?: string; skipMultilineEncoding?: boolean | null | undefined } >; + +// TODO(akhilmhdh): split this into multiple queue export const secretQueueFactory = ({ queueService, integrationDAL, @@ -141,7 +151,10 @@ export const secretQueueFactory = ({ snapshotSecretV2BridgeDAL, secretApprovalRequestDAL, keyStore, - auditLogService + auditLogService, + orgService, + projectUserMembershipRoleDAL, + projectKeyDAL }: TSecretQueueFactoryDep) => { const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => { const appCfg = getConfig(); @@ -1028,11 +1041,13 @@ export const secretQueueFactory = ({ const { botKey, shouldUseSecretV2Bridge: isProjectUpgradedToV3, - project + project, + bot } = await projectBotService.getBotKey(projectId); if (isProjectUpgradedToV3 || project.upgradeStatus === ProjectUpgradeStatus.InProgress) { return; } + if (!botKey) throw new NotFoundError({ message: "Project bot not found" }); await projectDAL.updateById(projectId, { upgradeStatus: ProjectUpgradeStatus.InProgress }); @@ -1044,6 +1059,57 @@ export const secretQueueFactory = ({ const folders = await folderDAL.findByProjectId(projectId); // except secret version and snapshot migrate rest of everything first in a transaction await secretDAL.transaction(async (tx) => { + // if project v1 create the project ghost user + if (project.version === ProjectVersion.V1) { + const ghostUser = await orgService.addGhostUser(project.orgId, tx); + const projectMembership = await projectMembershipDAL.create( + { + userId: ghostUser.user.id, + projectId: project.id + }, + tx + ); + await projectUserMembershipRoleDAL.create( + { projectMembershipId: projectMembership.id, role: ProjectMembershipRole.Admin }, + tx + ); + + const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({ + publicKey: ghostUser.keys.publicKey, + privateKey: ghostUser.keys.plainPrivateKey, + plainProjectKey: botKey + }); + + // 4. Save the project key for the ghost user. + await projectKeyDAL.create( + { + projectId: project.id, + receiverId: ghostUser.user.id, + encryptedKey: encryptedProjectKey, + nonce: encryptedProjectKeyIv, + senderId: ghostUser.user.id + }, + tx + ); + const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(ghostUser.keys.plainPrivateKey); + await projectBotDAL.updateById( + bot.id, + { + tag, + iv, + encryptedProjectKey, + encryptedProjectKeyNonce: encryptedProjectKeyIv, + encryptedPrivateKey: ciphertext, + isActive: true, + publicKey: ghostUser.keys.publicKey, + senderId: ghostUser.user.id, + algorithm, + keyEncoding: encoding + }, + tx + ); + } + for (const folder of folders) { const folderId = folder.id; /*