From c33741d5881a0a56844276a426a9424b9f0565be Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 6 Feb 2024 03:23:01 +0400 Subject: [PATCH] Ghost user WIP --- .../src/server/routes/v3/project-router.ts | 198 ++++++++++++++---- 1 file changed, 156 insertions(+), 42 deletions(-) diff --git a/backend/src/server/routes/v3/project-router.ts b/backend/src/server/routes/v3/project-router.ts index 5f83167a7..db39c5677 100644 --- a/backend/src/server/routes/v3/project-router.ts +++ b/backend/src/server/routes/v3/project-router.ts @@ -1,10 +1,15 @@ +import { ForbiddenError } from "@casl/ability"; import crypto from "crypto"; import { z } from "zod"; import { ProjectMembershipRole, ProjectsSchema } from "@app/db/schemas"; +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { encryptAsymmetric } from "@app/lib/crypto"; +import { BadRequestError } from "@app/lib/errors"; import { createWsMembers } from "@app/lib/project"; import { authRateLimit } from "@app/server/config/rateLimiter"; +import { ActorType } from "@app/services/auth/auth-type"; const projectWithEnv = ProjectsSchema.merge( z.object({ @@ -24,103 +29,212 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { schema: { body: z.object({ projectName: z.string().trim(), - inviteAllOrgMembers: z.boolean(), + inviteMemberEmails: z.array(z.string().email()).optional(), organizationId: z.string().trim() }), response: { 200: z.object({ - workspace: projectWithEnv + project: projectWithEnv }) } }, handler: async (req) => { - // 1. create the ghost user and add it to the org as admin - const ghost = await server.services.org.addGhostUser(req.body.organizationId); + const { permission } = await server.services.permission.getOrgPermission( + req.permission.type, + req.permission.id, + req.body.organizationId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); - // 2. create the workspace - const workspace = await server.services.project.createProject({ - actorId: ghost.user.id, + // 2. Create a new project (will set the e2ee db field to false). + const { project, ghostUser } = await server.services.project.createProject({ + actorId: req.permission.id, actor: req.permission.type, orgId: req.body.organizationId, workspaceName: req.body.projectName }); - // 3. create a random key that we'll use as the project key + // 3. Create a random key that we'll use as the project key. const randomBytes = crypto.randomBytes(16).toString("hex"); - const ghostPrivateKey = ghost.keys.plainPrivateKey; - + // 4. Encrypt the project key with the users key pair. const { ciphertext: encryptedProjectKey, nonce: encryptedProjectKeyIv } = encryptAsymmetric( randomBytes, - ghost.keys.publicKey, - ghostPrivateKey + ghostUser.keys.publicKey, + ghostUser.keys.plainPrivateKey ); - // 3. create workspace keys for the ghost user + // 4. Save the project key for the ghost user. await server.services.projectKey.uploadProjectKeys({ - projectId: workspace.id, + projectId: project.id, actor: req.permission.type, - actorId: ghost.user.id, + actorId: ghostUser.user.id, nonce: encryptedProjectKeyIv, - receiverId: ghost.user.id, + receiverId: ghostUser.user.id, encryptedKey: encryptedProjectKey }); - // 4. create a project bot + // 5. Create a bot for the project. const bot = await server.services.projectBot.findBotByProjectId({ - actorId: ghost.user.id, + actorId: ghostUser.user.id, actor: req.permission.type, - projectId: workspace.id + projectId: project.id, + + // We set the publicKey and privateKey of the bot to the same as the ghost user. + // We do this because we'll need to access the private key again later, when adding new members to the project. + publicKey: ghostUser.keys.publicKey, + privateKey: ghostUser.keys.plainPrivateKey }); - // 5. activate the bot + // 6. Activate the bot. await server.services.projectBot.setBotActiveState({ botKey: { encryptedKey: encryptedProjectKey, nonce: encryptedProjectKeyIv }, - actorId: ghost.user.id, + actorId: ghostUser.user.id, isActive: true, actor: req.permission.type, botId: bot.id }); - // 6. get the current user & org membership + // 7. get the current user & org membership const user = await server.services.user.getMe(req.permission.id); const userOrgMembership = await server.services.permission.getUserOrgPermission(user.id, req.body.organizationId); // 7. Get the latest key from the ghost! const latestKey = await server.services.projectKey.getLatestProjectKey({ - actorId: ghost.user.id, + actorId: ghostUser.user.id, actor: req.permission.type, - projectId: workspace.id + projectId: project.id }); if (!latestKey) throw new Error("Failed to get latest key"); - // 8. Create workspace members for the current user + // If the project is being created by a user, add the user to the project as an admin + if (req.permission.type === ActorType.USER) { + const projectAdmin = createWsMembers({ + decryptKey: latestKey, + members: [ + { + userPublicKey: user.publicKey, + orgMembershipId: userOrgMembership.membership.id, + projectMembershipRole: ProjectMembershipRole.Admin // <-- Make the first user an admin + } + ], + userPrivateKey: ghostUser.keys.plainPrivateKey + }); - const projectAdmin = await createWsMembers({ - decryptKey: latestKey, - members: [ - { - userPublicKey: user.publicKey, - orgMembershipId: userOrgMembership.membership.id, - projectMembershipRole: ProjectMembershipRole.Admin // <-- Make the first user an admin - } - ], - userPrivateKey: ghostPrivateKey - }); + await server.services.projectMembership.addUsersToProject({ + projectId: project.id, + actorId: ghostUser.user.id, + actor: req.permission.type, + members: projectAdmin + }); + } + // If the project is being created by an identity, add the identity to the project as an admin + else if (req.permission.type === ActorType.IDENTITY) { + await server.services.identityProject.createProjectIdentity({ + actor: ActorType.IDENTITY, + actorId: ghostUser.user.id, + identityId: req.permission.id, + projectId: project.id, + role: ProjectMembershipRole.Admin + }); + } - // 9. Add the current user to the workspace - await server.services.projectMembership.addUsersToProject({ - projectId: workspace.id, - actorId: ghost.user.id, + return { project }; + } + }); + + server.route({ + method: "POST", + url: "/:projectId/memberships", + config: { + rateLimit: authRateLimit + }, + schema: { + params: z.object({ + projectId: z.string() + }), + body: z.object({ + emails: z.string().email().array() + }) + }, + handler: async (req) => { + const { permission } = await server.services.permission.getProjectPermission( + req.permission.type, + req.permission.id, + req.params.projectId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member); + + const project = await server.services.project.getAProject({ + actorId: req.permission.id, actor: req.permission.type, - members: projectAdmin + projectId: req.params.projectId }); - return { workspace }; + const ghostUser = await server.services.project.findProjectGhostUser(req.params.projectId); + + if (!ghostUser) { + throw new BadRequestError({ + message: "Failed" // TODO: Add a message + }); + } + + const latestKey = await server.services.projectKey.getLatestProjectKey({ + actorId: ghostUser.id, + actor: ActorType.USER, + projectId: req.params.projectId + }); + + if (!latestKey) { + throw new BadRequestError({ + message: "Failed to find project key" + }); + } + + const bot = await server.services.projectBot.findBotByProjectId({ + actor: req.permission.type, + actorId: req.permission.id, + projectId: req.params.projectId + }); + + // We get the bot private key, because the bot private key is the same as the ghost user's private key. + const botPrivateKey = server.services.projectBot.getBotPrivateKey({ bot }); + + const members = await server.services.org.findOrgMembersByEmail({ + actor: req.permission.type, + actorId: req.permission.id, + orgId: project.orgId, + emails: req.body.emails + }); + + if (members.length !== req.body.emails.length) { + throw new BadRequestError({ + message: "Some users are not part of the organization" + }); + } + + const wsMembers = createWsMembers({ + members: members.map((membership) => ({ + orgMembershipId: membership.id, + projectMembershipRole: ProjectMembershipRole.Member, + userPublicKey: membership.user.publicKey + })), + decryptKey: latestKey, + userPrivateKey: botPrivateKey + }); + + await server.services.projectMembership.addUsersToProject({ + projectId: req.params.projectId, + actorId: ghostUser.id, // We set the actor ID to the ghost user, because this is used as senderId in the project key sharing + actor: ActorType.USER, + members: wsMembers + }); + + return {}; } }); };