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