mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Ghost user WIP
This commit is contained in:
@@ -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 {};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,6 +2,8 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import crypto from "crypto";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { Knex } from "knex";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
import { OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas";
|
||||
import { TProjects } from "@app/db/schemas/projects";
|
||||
@@ -30,6 +32,7 @@ import { TOrgRoleDALFactory } from "./org-role-dal";
|
||||
import {
|
||||
TDeleteOrgMembershipDTO,
|
||||
TFindAllWorkspacesDTO,
|
||||
TFindOrgMembersByEmailDTO,
|
||||
TInviteUserToOrgDTO,
|
||||
TUpdateOrgDTO,
|
||||
TUpdateOrgMembershipDTO,
|
||||
@@ -95,6 +98,15 @@ export const orgServiceFactory = ({
|
||||
return members;
|
||||
};
|
||||
|
||||
const findOrgMembersByEmail = async ({ actor, actorId, orgId, emails }: TFindOrgMembersByEmailDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
|
||||
|
||||
const members = await orgDAL.findOrgMembersByEmail(orgId, emails);
|
||||
|
||||
return members;
|
||||
};
|
||||
|
||||
const findAllWorkspaces = async ({ actor, actorId, actorOrgId, orgId }: TFindAllWorkspacesDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
|
||||
@@ -120,31 +132,38 @@ export const orgServiceFactory = ({
|
||||
return workspaces.filter((workspace) => organizationWorkspaceIds.has(workspace.id));
|
||||
};
|
||||
|
||||
const addGhostUser = async (orgId: string) => {
|
||||
const email = `ghost@${orgId}.com`;
|
||||
const addGhostUser = async (orgId: string, tx?: Knex) => {
|
||||
const email = `ghost@${nanoid(8)}-${orgId}.com`; // We add a nanoid because the email is unique. And we have to create a new ghost user each time, so we can have access to the private key.
|
||||
const password = crypto.randomBytes(128).toString("hex");
|
||||
|
||||
const user = await userDAL.create({
|
||||
ghost: true,
|
||||
authMethods: [AuthMethod.EMAIL],
|
||||
email: `ghost@${orgId}.com`,
|
||||
isAccepted: true
|
||||
});
|
||||
const user = await userDAL.create(
|
||||
{
|
||||
ghost: true,
|
||||
authMethods: [AuthMethod.EMAIL],
|
||||
email,
|
||||
isAccepted: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const encKeys = await generateUserSrpKeys(email, password);
|
||||
|
||||
await userDAL.upsertUserEncryptionKey(user.id, {
|
||||
encryptionVersion: 2,
|
||||
protectedKey: encKeys.protectedKey,
|
||||
protectedKeyIV: encKeys.protectedKeyIV,
|
||||
protectedKeyTag: encKeys.protectedKeyTag,
|
||||
publicKey: encKeys.publicKey,
|
||||
encryptedPrivateKey: encKeys.encryptedPrivateKey,
|
||||
iv: encKeys.encryptedPrivateKeyIV,
|
||||
tag: encKeys.encryptedPrivateKeyTag,
|
||||
salt: encKeys.salt,
|
||||
verifier: encKeys.verifier
|
||||
});
|
||||
await userDAL.upsertUserEncryptionKey(
|
||||
user.id,
|
||||
{
|
||||
encryptionVersion: 2,
|
||||
protectedKey: encKeys.protectedKey,
|
||||
protectedKeyIV: encKeys.protectedKeyIV,
|
||||
protectedKeyTag: encKeys.protectedKeyTag,
|
||||
publicKey: encKeys.publicKey,
|
||||
encryptedPrivateKey: encKeys.encryptedPrivateKey,
|
||||
iv: encKeys.encryptedPrivateKeyIV,
|
||||
tag: encKeys.encryptedPrivateKeyTag,
|
||||
salt: encKeys.salt,
|
||||
verifier: encKeys.verifier
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const createMembershipData = {
|
||||
orgId,
|
||||
@@ -153,9 +172,7 @@ export const orgServiceFactory = ({
|
||||
status: OrgMembershipStatus.Accepted
|
||||
};
|
||||
|
||||
console.log("createMembershipData", createMembershipData);
|
||||
|
||||
await orgDAL.createMembership(createMembershipData);
|
||||
await orgDAL.createMembership(createMembershipData, tx);
|
||||
|
||||
return {
|
||||
user,
|
||||
@@ -533,6 +550,7 @@ export const orgServiceFactory = ({
|
||||
inviteUserToOrganization,
|
||||
verifyUserToOrg,
|
||||
updateOrg,
|
||||
findOrgMembersByEmail,
|
||||
createOrganization,
|
||||
deleteOrganizationById,
|
||||
deleteOrgMembership,
|
||||
|
||||
@@ -30,6 +30,13 @@ export type TVerifyUserToOrgDTO = {
|
||||
code: string;
|
||||
};
|
||||
|
||||
export type TFindOrgMembersByEmailDTO = {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
orgId: string;
|
||||
emails: string[];
|
||||
};
|
||||
|
||||
export type TFindAllWorkspacesDTO = {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
|
||||
@@ -13,10 +13,9 @@ import {
|
||||
} from "@app/lib/crypto";
|
||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { TProjectBotDALFactory } from "./project-bot-dal";
|
||||
import { TGetPrivateKeyDTO, TSetActiveStateDTO } from "./project-bot-types";
|
||||
import { TFindBotByProjectIdDTO, TGetPrivateKeyDTO, TSetActiveStateDTO } from "./project-bot-types";
|
||||
|
||||
type TProjectBotServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
@@ -26,12 +25,12 @@ type TProjectBotServiceFactoryDep = {
|
||||
export type TProjectBotServiceFactory = ReturnType<typeof projectBotServiceFactory>;
|
||||
|
||||
export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: TProjectBotServiceFactoryDep) => {
|
||||
const getBotPrivateKey = async ({ encoding, nonce, tag, encryptedPrivateKey }: TGetPrivateKeyDTO) =>
|
||||
const getBotPrivateKey = ({ bot }: TGetPrivateKeyDTO) =>
|
||||
infisicalSymmetricDecrypt({
|
||||
keyEncoding: encoding,
|
||||
iv: nonce,
|
||||
tag,
|
||||
ciphertext: encryptedPrivateKey
|
||||
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
|
||||
iv: bot.iv,
|
||||
tag: bot.tag,
|
||||
ciphertext: bot.encryptedPrivateKey
|
||||
});
|
||||
|
||||
const getBotKey = async (projectId: string) => {
|
||||
@@ -41,24 +40,24 @@ export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: T
|
||||
if (!bot.encryptedProjectKeyNonce || !bot.encryptedProjectKey)
|
||||
throw new BadRequestError({ message: "Encryption key missing" });
|
||||
|
||||
const privateKeyBot = await getBotPrivateKey({
|
||||
nonce: bot.iv,
|
||||
tag: bot.tag,
|
||||
encryptedPrivateKey: bot.encryptedPrivateKey,
|
||||
encoding: bot.keyEncoding as SecretKeyEncoding
|
||||
});
|
||||
|
||||
console.log("privateKeyBot", privateKeyBot);
|
||||
const botPrivateKey = getBotPrivateKey({ bot });
|
||||
|
||||
return decryptAsymmetric({
|
||||
ciphertext: bot.encryptedProjectKey,
|
||||
privateKey: privateKeyBot,
|
||||
privateKey: botPrivateKey,
|
||||
nonce: bot.encryptedProjectKeyNonce,
|
||||
publicKey: bot.sender.publicKey
|
||||
});
|
||||
};
|
||||
|
||||
const findBotByProjectId = async ({ actorId, actor, actorOrgId, projectId }: TProjectPermission) => {
|
||||
const findBotByProjectId = async ({
|
||||
actorId,
|
||||
actor,
|
||||
projectId,
|
||||
actorOrgId,
|
||||
privateKey,
|
||||
publicKey
|
||||
}: TFindBotByProjectIdDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||
const appCfg = getConfig();
|
||||
@@ -67,9 +66,10 @@ export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: T
|
||||
const doc = await projectBotDAL.findOne({ projectId }, tx);
|
||||
if (doc) return doc;
|
||||
|
||||
const { publicKey, privateKey } = generateAsymmetricKeyPair();
|
||||
const keys = privateKey && publicKey ? { privateKey, publicKey } : generateAsymmetricKeyPair();
|
||||
|
||||
if (appCfg.ROOT_ENCRYPTION_KEY) {
|
||||
const { iv, tag, ciphertext } = encryptSymmetric(privateKey, appCfg.ROOT_ENCRYPTION_KEY);
|
||||
const { iv, tag, ciphertext } = encryptSymmetric(keys.privateKey, appCfg.ROOT_ENCRYPTION_KEY);
|
||||
return projectBotDAL.create(
|
||||
{
|
||||
name: "Infisical Bot",
|
||||
@@ -78,7 +78,7 @@ export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: T
|
||||
iv,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
isActive: false,
|
||||
publicKey,
|
||||
publicKey: keys.publicKey,
|
||||
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
||||
keyEncoding: SecretKeyEncoding.BASE64
|
||||
},
|
||||
@@ -86,7 +86,7 @@ export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: T
|
||||
);
|
||||
}
|
||||
if (appCfg.ENCRYPTION_KEY) {
|
||||
const { iv, tag, ciphertext } = encryptSymmetric128BitHexKeyUTF8(privateKey, appCfg.ENCRYPTION_KEY);
|
||||
const { iv, tag, ciphertext } = encryptSymmetric128BitHexKeyUTF8(keys.privateKey, appCfg.ENCRYPTION_KEY);
|
||||
return projectBotDAL.create(
|
||||
{
|
||||
name: "Infisical Bot",
|
||||
@@ -95,7 +95,7 @@ export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: T
|
||||
iv,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
isActive: false,
|
||||
publicKey,
|
||||
publicKey: keys.publicKey,
|
||||
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
||||
keyEncoding: SecretKeyEncoding.UTF8
|
||||
},
|
||||
@@ -107,6 +107,15 @@ export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: T
|
||||
return bot;
|
||||
};
|
||||
|
||||
const findProjectByBotId = async (botId: string) => {
|
||||
try {
|
||||
const bot = await projectBotDAL.findProjectByBotId(botId);
|
||||
return bot;
|
||||
} catch (e) {
|
||||
throw new BadRequestError({ message: "Failed to find bot by ID" });
|
||||
}
|
||||
};
|
||||
|
||||
const setBotActiveState = async ({ actor, botId, botKey, actorId, actorOrgId, isActive }: TSetActiveStateDTO) => {
|
||||
const bot = await projectBotDAL.findById(botId);
|
||||
if (!bot) throw new BadRequestError({ message: "Bot not found" });
|
||||
@@ -145,6 +154,7 @@ export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: T
|
||||
findBotByProjectId,
|
||||
setBotActiveState,
|
||||
getBotPrivateKey,
|
||||
findProjectByBotId,
|
||||
getBotKey
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||
// import { SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { TProjectBots } from "@app/db/schemas";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TSetActiveStateDTO = {
|
||||
@@ -10,9 +11,16 @@ export type TSetActiveStateDTO = {
|
||||
botId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TFindBotByProjectIdDTO = {
|
||||
privateKey?: string;
|
||||
publicKey?: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetPrivateKeyDTO = {
|
||||
encoding: SecretKeyEncoding;
|
||||
nonce: string;
|
||||
tag: string;
|
||||
encryptedPrivateKey: string;
|
||||
// encoding: SecretKeyEncoding;
|
||||
// nonce: string;
|
||||
// tag: string;
|
||||
// encryptedPrivateKey: string;
|
||||
|
||||
bot: TProjectBots;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { OrgMembershipStatus, ProjectMembershipRole, TableName } from "@app/db/schemas";
|
||||
import { OrgMembershipStatus, ProjectMembershipRole, TableName, TUsers } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
@@ -27,7 +28,7 @@ type TProjectMembershipServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
smtpService: TSmtpService;
|
||||
projectMembershipDAL: TProjectMembershipDALFactory;
|
||||
userDAL: Pick<TUserDALFactory, "findById" | "findOne">;
|
||||
userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId">;
|
||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "findOne">;
|
||||
orgDAL: Pick<TOrgDALFactory, "findMembership">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
@@ -55,64 +56,78 @@ export const projectMembershipServiceFactory = ({
|
||||
return projectMembershipDAL.findAllProjectMembers(projectId);
|
||||
};
|
||||
|
||||
const inviteUserToProject = async ({ actorId, actor, actorOrgId, projectId, email }: TInviteUserToProjectDTO) => {
|
||||
const inviteUserToProject = async ({ actorId, actor, actorOrgId, projectId, emails }: TInviteUserToProjectDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
|
||||
|
||||
const invitee = await userDAL.findOne({ email });
|
||||
if (!invitee || !invitee.isAccepted)
|
||||
throw new BadRequestError({
|
||||
message: "Faield to validate invitee",
|
||||
name: "Invite user to project"
|
||||
const invitees: TUsers[] = [];
|
||||
|
||||
for (const email of emails) {
|
||||
const invitee = await userDAL.findOne({ email });
|
||||
if (!invitee || !invitee.isAccepted)
|
||||
throw new BadRequestError({
|
||||
message: "Faield to validate invitee",
|
||||
name: "Invite user to project"
|
||||
});
|
||||
|
||||
const inviteeMembership = await projectMembershipDAL.findOne({
|
||||
userId: invitee.id,
|
||||
projectId
|
||||
});
|
||||
if (inviteeMembership)
|
||||
throw new BadRequestError({
|
||||
message: "Existing member of project",
|
||||
name: "Invite user to project"
|
||||
});
|
||||
|
||||
const project = await projectDAL.findById(projectId);
|
||||
const inviteeMembershipOrg = await orgDAL.findMembership({
|
||||
userId: invitee.id,
|
||||
orgId: project.orgId,
|
||||
status: OrgMembershipStatus.Accepted
|
||||
});
|
||||
if (!inviteeMembershipOrg)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to validate invitee org membership",
|
||||
name: "Invite user to project"
|
||||
});
|
||||
|
||||
await projectMembershipDAL.create({
|
||||
userId: invitee.id,
|
||||
projectId,
|
||||
role: ProjectMembershipRole.Member
|
||||
});
|
||||
|
||||
const inviteeMembership = await projectMembershipDAL.findOne({
|
||||
userId: invitee.id,
|
||||
projectId
|
||||
});
|
||||
if (inviteeMembership)
|
||||
throw new BadRequestError({
|
||||
message: "Existing member of project",
|
||||
name: "Invite user to project"
|
||||
const sender = await userDAL.findById(actorId);
|
||||
const appCfg = getConfig();
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.WorkspaceInvite,
|
||||
subjectLine: "Infisical workspace invitation",
|
||||
recipients: [invitee.email],
|
||||
substitutions: {
|
||||
inviterFirstName: sender.firstName,
|
||||
inviterEmail: sender.email,
|
||||
workspaceName: project.name,
|
||||
callback_url: `${appCfg.SITE_URL}/login`
|
||||
}
|
||||
});
|
||||
|
||||
const project = await projectDAL.findById(projectId);
|
||||
const inviteeMembershipOrg = await orgDAL.findMembership({
|
||||
userId: invitee.id,
|
||||
orgId: project.orgId,
|
||||
status: OrgMembershipStatus.Accepted
|
||||
});
|
||||
if (!inviteeMembershipOrg)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to validate invitee org membership",
|
||||
name: "Invite user to project"
|
||||
});
|
||||
invitees.push(invitee);
|
||||
}
|
||||
|
||||
const latestKey = await projectKeyDAL.findLatestProjectKey(actorId, projectId);
|
||||
await projectMembershipDAL.create({
|
||||
userId: invitee.id,
|
||||
projectId,
|
||||
role: ProjectMembershipRole.Member
|
||||
});
|
||||
|
||||
const sender = await userDAL.findById(actorId);
|
||||
const appCfg = getConfig();
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.WorkspaceInvite,
|
||||
subjectLine: "Infisical workspace invitation",
|
||||
recipients: [invitee.email],
|
||||
substitutions: {
|
||||
inviterFirstName: sender.firstName,
|
||||
inviterEmail: sender.email,
|
||||
workspaceName: project.name,
|
||||
callback_url: `${appCfg.SITE_URL}/login`
|
||||
}
|
||||
});
|
||||
|
||||
return { invitee, latestKey };
|
||||
return { invitees, latestKey };
|
||||
};
|
||||
|
||||
const addUsersToProject = async ({ projectId, actorId, actor, actorOrgId, members }: TAddUsersToWorkspaceDTO) => {
|
||||
const addUsersToProject = async ({
|
||||
projectId,
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
members,
|
||||
sendEmails = true
|
||||
}: TAddUsersToWorkspaceDTO) => {
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
|
||||
@@ -158,19 +173,22 @@ export const projectMembershipServiceFactory = ({
|
||||
tx
|
||||
);
|
||||
});
|
||||
const sender = await userDAL.findById(actorId);
|
||||
const appCfg = getConfig();
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.WorkspaceInvite,
|
||||
subjectLine: "Infisical workspace invitation",
|
||||
recipients: orgMembers.map(({ email }) => email).filter(Boolean),
|
||||
substitutions: {
|
||||
inviterFirstName: sender.firstName,
|
||||
inviterEmail: sender.email,
|
||||
workspaceName: project.name,
|
||||
callback_url: `${appCfg.SITE_URL}/login`
|
||||
}
|
||||
});
|
||||
|
||||
if (sendEmails) {
|
||||
const sender = await userDAL.findById(actorId);
|
||||
const appCfg = getConfig();
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.WorkspaceInvite,
|
||||
subjectLine: "Infisical workspace invitation",
|
||||
recipients: orgMembers.map(({ email }) => email).filter(Boolean),
|
||||
substitutions: {
|
||||
inviterFirstName: sender.firstName,
|
||||
inviterEmail: sender.email,
|
||||
workspaceName: project.name,
|
||||
callback_url: `${appCfg.SITE_URL}/login`
|
||||
}
|
||||
});
|
||||
}
|
||||
return orgMembers;
|
||||
};
|
||||
|
||||
@@ -185,6 +203,15 @@ export const projectMembershipServiceFactory = ({
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
||||
|
||||
const membershipUser = await userDAL.findUserByProjectMembershipId(membershipId);
|
||||
|
||||
if (membershipUser?.ghost) {
|
||||
throw new BadRequestError({
|
||||
message: "Unauthorized member update",
|
||||
name: "Update project membership"
|
||||
});
|
||||
}
|
||||
|
||||
const isCustomRole = !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole);
|
||||
if (isCustomRole) {
|
||||
const customRole = await projectRoleDAL.findOne({ slug: role, projectId });
|
||||
@@ -220,6 +247,15 @@ export const projectMembershipServiceFactory = ({
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member);
|
||||
|
||||
const membershipUser = await userDAL.findUserByProjectMembershipId(membershipId);
|
||||
|
||||
if (membershipUser?.ghost) {
|
||||
throw new BadRequestError({
|
||||
message: "Cannot delete ghost",
|
||||
name: "Delete project membership"
|
||||
});
|
||||
}
|
||||
|
||||
const membership = await projectMembershipDAL.transaction(async (tx) => {
|
||||
const [deletedMembership] = await projectMembershipDAL.delete({ projectId, id: membershipId }, tx);
|
||||
await projectKeyDAL.delete({ receiverId: deletedMembership.userId, projectId }, tx);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { TProjectPermission } from "@app/lib/types";
|
||||
export type TGetProjectMembershipDTO = TProjectPermission;
|
||||
|
||||
export type TInviteUserToProjectDTO = {
|
||||
email: string;
|
||||
emails: string[];
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TUpdateProjectMembershipDTO = {
|
||||
@@ -17,6 +17,7 @@ export type TDeleteProjectMembershipDTO = {
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TAddUsersToWorkspaceDTO = {
|
||||
sendEmails?: boolean;
|
||||
members: {
|
||||
orgMembershipId: string;
|
||||
workspaceEncryptedKey: string;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { createSecretBlindIndex } from "@app/lib/crypto";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { TOrgServiceFactory } from "../org/org-service";
|
||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
|
||||
@@ -28,7 +29,8 @@ type TProjectServiceFactoryDep = {
|
||||
projectDAL: TProjectDALFactory;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "insertMany">;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser">;
|
||||
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
||||
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">;
|
||||
permissionService: TPermissionServiceFactory;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
@@ -40,6 +42,7 @@ export const projectServiceFactory = ({
|
||||
projectDAL,
|
||||
permissionService,
|
||||
folderDAL,
|
||||
orgService,
|
||||
secretBlindIndexDAL,
|
||||
projectMembershipDAL,
|
||||
projectEnvDAL,
|
||||
@@ -64,7 +67,9 @@ export const projectServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const newProject = projectDAL.transaction(async (tx) => {
|
||||
const results = await projectDAL.transaction(async (tx) => {
|
||||
const ghostUser = await orgService.addGhostUser(orgId, tx);
|
||||
|
||||
const project = await projectDAL.create(
|
||||
{
|
||||
name: workspaceName,
|
||||
@@ -74,10 +79,10 @@ export const projectServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
// set user as admin member for project
|
||||
// set ghost user as admin of project
|
||||
await projectMembershipDAL.create(
|
||||
{
|
||||
userId: actorId,
|
||||
userId: ghostUser.user.id,
|
||||
role: ProjectMembershipRole.Admin,
|
||||
projectId: project.id
|
||||
},
|
||||
@@ -106,10 +111,23 @@ export const projectServiceFactory = ({
|
||||
tx
|
||||
);
|
||||
// _id for backward compat
|
||||
return { ...project, environments: envs, _id: project.id };
|
||||
return {
|
||||
project: {
|
||||
...project,
|
||||
environments: envs,
|
||||
_id: project.id
|
||||
},
|
||||
ghostUser
|
||||
};
|
||||
});
|
||||
|
||||
return newProject;
|
||||
return results;
|
||||
};
|
||||
|
||||
const findProjectGhostUser = async (projectId: string) => {
|
||||
const user = await projectMembershipDAL.findProjectGhostUser(projectId);
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
const deleteProject = async ({ actor, actorId, actorOrgId, projectId }: TDeleteProjectDTO) => {
|
||||
@@ -156,6 +174,7 @@ export const projectServiceFactory = ({
|
||||
createProject,
|
||||
deleteProject,
|
||||
getProjects,
|
||||
findProjectGhostUser,
|
||||
getAProject,
|
||||
toggleAutoCapitalization,
|
||||
updateName
|
||||
|
||||
Reference in New Issue
Block a user