From a366dbb16d96c5b4e61eb3233babdb2f43b181f1 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Sun, 4 Feb 2024 13:40:47 +0400 Subject: [PATCH] Ghost user! --- backend/src/server/routes/v3/index.ts | 2 + .../src/server/routes/v3/project-router.ts | 126 ++++++++++++++++++ .../project-key/project-key-service.ts | 3 +- .../src/pages/org/[id]/overview/index.tsx | 19 +++ 4 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 backend/src/server/routes/v3/project-router.ts diff --git a/backend/src/server/routes/v3/index.ts b/backend/src/server/routes/v3/index.ts index 6a061a635..21d57d359 100644 --- a/backend/src/server/routes/v3/index.ts +++ b/backend/src/server/routes/v3/index.ts @@ -1,4 +1,5 @@ import { registerLoginRouter } from "./login-router"; +import { registerProjectRouter } from "./project-router"; import { registerSecretBlindIndexRouter } from "./secret-blind-index-router"; import { registerSecretRouter } from "./secret-router"; import { registerSignupRouter } from "./signup-router"; @@ -10,4 +11,5 @@ export const registerV3Routes = async (server: FastifyZodProvider) => { await server.register(registerUserRouter, { prefix: "/users" }); await server.register(registerSecretRouter, { prefix: "/secrets" }); await server.register(registerSecretBlindIndexRouter, { prefix: "/workspaces" }); + await server.register(registerProjectRouter, { prefix: "/projects" }); }; diff --git a/backend/src/server/routes/v3/project-router.ts b/backend/src/server/routes/v3/project-router.ts new file mode 100644 index 000000000..5f83167a7 --- /dev/null +++ b/backend/src/server/routes/v3/project-router.ts @@ -0,0 +1,126 @@ +import crypto from "crypto"; +import { z } from "zod"; + +import { ProjectMembershipRole, ProjectsSchema } from "@app/db/schemas"; +import { encryptAsymmetric } from "@app/lib/crypto"; +import { createWsMembers } from "@app/lib/project"; +import { authRateLimit } from "@app/server/config/rateLimiter"; + +const projectWithEnv = ProjectsSchema.merge( + z.object({ + _id: z.string(), + environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array() + }) +); + +export const registerProjectRouter = async (server: FastifyZodProvider) => { + /* Create new project */ + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: authRateLimit + }, + schema: { + body: z.object({ + projectName: z.string().trim(), + inviteAllOrgMembers: z.boolean(), + organizationId: z.string().trim() + }), + response: { + 200: z.object({ + workspace: 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); + + // 2. create the workspace + const workspace = await server.services.project.createProject({ + actorId: ghost.user.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 + const randomBytes = crypto.randomBytes(16).toString("hex"); + + const ghostPrivateKey = ghost.keys.plainPrivateKey; + + const { ciphertext: encryptedProjectKey, nonce: encryptedProjectKeyIv } = encryptAsymmetric( + randomBytes, + ghost.keys.publicKey, + ghostPrivateKey + ); + + // 3. create workspace keys for the ghost user + await server.services.projectKey.uploadProjectKeys({ + projectId: workspace.id, + actor: req.permission.type, + actorId: ghost.user.id, + nonce: encryptedProjectKeyIv, + receiverId: ghost.user.id, + encryptedKey: encryptedProjectKey + }); + + // 4. create a project bot + const bot = await server.services.projectBot.findBotByProjectId({ + actorId: ghost.user.id, + actor: req.permission.type, + projectId: workspace.id + }); + + // 5. activate the bot + await server.services.projectBot.setBotActiveState({ + botKey: { + encryptedKey: encryptedProjectKey, + nonce: encryptedProjectKeyIv + }, + actorId: ghost.user.id, + isActive: true, + actor: req.permission.type, + botId: bot.id + }); + + // 6. 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, + actor: req.permission.type, + projectId: workspace.id + }); + + if (!latestKey) throw new Error("Failed to get latest key"); + + // 8. Create workspace members for the current user + + 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 + }); + + // 9. Add the current user to the workspace + await server.services.projectMembership.addUsersToProject({ + projectId: workspace.id, + actorId: ghost.user.id, + actor: req.permission.type, + members: projectAdmin + }); + + return { workspace }; + } + }); +}; diff --git a/backend/src/services/project-key/project-key-service.ts b/backend/src/services/project-key/project-key-service.ts index fa77760a4..81b589bd0 100644 --- a/backend/src/services/project-key/project-key-service.ts +++ b/backend/src/services/project-key/project-key-service.ts @@ -1,4 +1,5 @@ import { ForbiddenError } from "@casl/ability"; +import { Knex } from "knex"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; @@ -43,7 +44,7 @@ export const projectKeyServiceFactory = ({ name: "Upload project keys" }); - await projectKeyDAL.create({ projectId, receiverId, encryptedKey, nonce, senderId: actorId }); + await projectKeyDAL.create({ projectId, receiverId, encryptedKey, nonce, senderId: actorId }, tx); }; const getLatestProjectKey = async ({ actorId, projectId, actor, actorOrgId }: TGetLatestProjectKeyDTO) => { diff --git a/frontend/src/pages/org/[id]/overview/index.tsx b/frontend/src/pages/org/[id]/overview/index.tsx index 8a703b3dc..f3b6f67b0 100644 --- a/frontend/src/pages/org/[id]/overview/index.tsx +++ b/frontend/src/pages/org/[id]/overview/index.tsx @@ -509,6 +509,7 @@ const OrganizationPage = withPermission( projectName: name }); + /* if (addMembers) { const orgUsers = await fetchOrgUsers(currentOrg); @@ -523,6 +524,24 @@ const OrganizationPage = withPermission( // eslint-disable-next-line no-promise-executor-return -- We do this because the function returns too fast, which sometimes causes an error when the user is redirected. await new Promise((resolve) => setTimeout(resolve, 2_000)); + const decryptKey = await fetchUserWsKey(newWorkspaceId); + + await addWsUser.mutateAsync({ + workspaceId: newWorkspaceId, + decryptKey, + userPrivateKey: PRIVATE_KEY, + members: orgUsers + .filter( + ({ status, user: orgUser }) => status === "accepted" && user.email !== orgUser.email + ) + .map(({ user: orgUser, id: orgMembershipId }) => ({ + userPublicKey: orgUser.publicKey, + orgMembershipId + })) + }); + } + */ + createNotification({ text: "Workspace created", type: "success" }); handlePopUpClose("addNewWs"); createNotification({ text: "Workspace created", type: "success" }); router.push(`/project/${newProjectId}/secrets/overview`);