Ghost user WIP

This commit is contained in:
Daniel Hougaard
2024-02-22 05:00:26 +01:00
parent 5dfc84190d
commit c33741d588
+156 -42
View File
@@ -1,10 +1,15 @@
import { ForbiddenError } from "@casl/ability";
import crypto from "crypto"; import crypto from "crypto";
import { z } from "zod"; import { z } from "zod";
import { ProjectMembershipRole, ProjectsSchema } from "@app/db/schemas"; 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 { encryptAsymmetric } from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors";
import { createWsMembers } from "@app/lib/project"; import { createWsMembers } from "@app/lib/project";
import { authRateLimit } from "@app/server/config/rateLimiter"; import { authRateLimit } from "@app/server/config/rateLimiter";
import { ActorType } from "@app/services/auth/auth-type";
const projectWithEnv = ProjectsSchema.merge( const projectWithEnv = ProjectsSchema.merge(
z.object({ z.object({
@@ -24,103 +29,212 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
body: z.object({ body: z.object({
projectName: z.string().trim(), projectName: z.string().trim(),
inviteAllOrgMembers: z.boolean(), inviteMemberEmails: z.array(z.string().email()).optional(),
organizationId: z.string().trim() organizationId: z.string().trim()
}), }),
response: { response: {
200: z.object({ 200: z.object({
workspace: projectWithEnv project: projectWithEnv
}) })
} }
}, },
handler: async (req) => { handler: async (req) => {
// 1. create the ghost user and add it to the org as admin const { permission } = await server.services.permission.getOrgPermission(
const ghost = await server.services.org.addGhostUser(req.body.organizationId); req.permission.type,
req.permission.id,
req.body.organizationId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
// 2. create the workspace // 2. Create a new project (will set the e2ee db field to false).
const workspace = await server.services.project.createProject({ const { project, ghostUser } = await server.services.project.createProject({
actorId: ghost.user.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
orgId: req.body.organizationId, orgId: req.body.organizationId,
workspaceName: req.body.projectName 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 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( const { ciphertext: encryptedProjectKey, nonce: encryptedProjectKeyIv } = encryptAsymmetric(
randomBytes, randomBytes,
ghost.keys.publicKey, ghostUser.keys.publicKey,
ghostPrivateKey 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({ await server.services.projectKey.uploadProjectKeys({
projectId: workspace.id, projectId: project.id,
actor: req.permission.type, actor: req.permission.type,
actorId: ghost.user.id, actorId: ghostUser.user.id,
nonce: encryptedProjectKeyIv, nonce: encryptedProjectKeyIv,
receiverId: ghost.user.id, receiverId: ghostUser.user.id,
encryptedKey: encryptedProjectKey encryptedKey: encryptedProjectKey
}); });
// 4. create a project bot // 5. Create a bot for the project.
const bot = await server.services.projectBot.findBotByProjectId({ const bot = await server.services.projectBot.findBotByProjectId({
actorId: ghost.user.id, actorId: ghostUser.user.id,
actor: req.permission.type, 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({ await server.services.projectBot.setBotActiveState({
botKey: { botKey: {
encryptedKey: encryptedProjectKey, encryptedKey: encryptedProjectKey,
nonce: encryptedProjectKeyIv nonce: encryptedProjectKeyIv
}, },
actorId: ghost.user.id, actorId: ghostUser.user.id,
isActive: true, isActive: true,
actor: req.permission.type, actor: req.permission.type,
botId: bot.id 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 user = await server.services.user.getMe(req.permission.id);
const userOrgMembership = await server.services.permission.getUserOrgPermission(user.id, req.body.organizationId); const userOrgMembership = await server.services.permission.getUserOrgPermission(user.id, req.body.organizationId);
// 7. Get the latest key from the ghost! // 7. Get the latest key from the ghost!
const latestKey = await server.services.projectKey.getLatestProjectKey({ const latestKey = await server.services.projectKey.getLatestProjectKey({
actorId: ghost.user.id, actorId: ghostUser.user.id,
actor: req.permission.type, actor: req.permission.type,
projectId: workspace.id projectId: project.id
}); });
if (!latestKey) throw new Error("Failed to get latest key"); 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({ await server.services.projectMembership.addUsersToProject({
decryptKey: latestKey, projectId: project.id,
members: [ actorId: ghostUser.user.id,
{ actor: req.permission.type,
userPublicKey: user.publicKey, members: projectAdmin
orgMembershipId: userOrgMembership.membership.id, });
projectMembershipRole: ProjectMembershipRole.Admin // <-- Make the first user an admin }
} // 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) {
userPrivateKey: ghostPrivateKey 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 return { project };
await server.services.projectMembership.addUsersToProject({ }
projectId: workspace.id, });
actorId: ghost.user.id,
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, 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 {};
} }
}); });
}; };