diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index b08c70d93..7d998f8ea 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -447,7 +447,9 @@ export const PROJECT_USERS = { INVITE_MEMBER: { projectId: "The ID of the project to invite the member to.", emails: "A list of organization member emails to invite to the project.", - usernames: "A list of usernames to invite to the project." + usernames: "A list of usernames to invite to the project.", + roleSlugs: + "A list of role slugs to assign to the newly created project membership. If nothing is provided, it will default to the Member role." }, REMOVE_MEMBER: { projectId: "The ID of the project to remove the member from.", diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index af0aa3d8a..f0675dff5 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -499,6 +499,7 @@ export const registerRoutes = async ( tokenService, projectUserAdditionalPrivilegeDAL, projectUserMembershipRoleDAL, + projectRoleDAL, projectDAL, projectMembershipDAL, orgMembershipDAL, @@ -506,8 +507,7 @@ export const registerRoutes = async ( smtpService, userDAL, groupDAL, - orgBotDAL, - projectRoleDAL + orgBotDAL }); const signupService = authSignupServiceFactory({ tokenService, diff --git a/backend/src/server/routes/v2/project-membership-router.ts b/backend/src/server/routes/v2/project-membership-router.ts index 6ab574d64..4aa03f33c 100644 --- a/backend/src/server/routes/v2/project-membership-router.ts +++ b/backend/src/server/routes/v2/project-membership-router.ts @@ -26,7 +26,8 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider }), body: z.object({ emails: z.string().email().array().default([]).describe(PROJECT_USERS.INVITE_MEMBER.emails), - usernames: z.string().array().default([]).describe(PROJECT_USERS.INVITE_MEMBER.usernames) + usernames: z.string().array().default([]).describe(PROJECT_USERS.INVITE_MEMBER.usernames), + roleSlugs: z.string().array().optional().describe(PROJECT_USERS.INVITE_MEMBER.roleSlugs) }), response: { 200: z.object({ diff --git a/backend/src/services/org-admin/org-admin-service.ts b/backend/src/services/org-admin/org-admin-service.ts index 4759db309..4ee192c88 100644 --- a/backend/src/services/org-admin/org-admin-service.ts +++ b/backend/src/services/org-admin/org-admin-service.ts @@ -153,7 +153,6 @@ export const orgAdminServiceFactory = ({ members: [ { orgMembershipId: membership.id, - projectMembershipRole: ProjectMembershipRole.Admin, userPublicKey: userEncryptionKey.publicKey } ] diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 9dad7086e..5b4efb992 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -114,11 +114,11 @@ export const orgServiceFactory = ({ tokenService, orgBotDAL, licenseService, + projectRoleDAL, samlConfigDAL, userGroupMembershipDAL, projectBotDAL, - projectUserMembershipRoleDAL, - projectRoleDAL + projectUserMembershipRoleDAL }: TOrgServiceFactoryDep) => { /* * Get organization details by the organization id diff --git a/backend/src/services/project-membership/project-membership-fns.ts b/backend/src/services/project-membership/project-membership-fns.ts deleted file mode 100644 index 98acd6eec..000000000 --- a/backend/src/services/project-membership/project-membership-fns.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { Knex } from "knex"; - -import { ProjectMembershipRole, SecretKeyEncoding, TProjectMemberships } from "@app/db/schemas"; -import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; -import { getConfig } from "@app/lib/config/env"; -import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; -import { BadRequestError } from "@app/lib/errors"; -import { groupBy } from "@app/lib/fn"; - -import { TOrgDALFactory } from "../org/org-dal"; -import { TProjectDALFactory } from "../project/project-dal"; -import { assignWorkspaceKeysToMembers } from "../project/project-fns"; -import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; -import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; -import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; -import { TProjectMembershipDALFactory } from "./project-membership-dal"; -import { TProjectUserMembershipRoleDALFactory } from "./project-user-membership-role-dal"; - -type TAddMembersToProjectArg = { - orgDAL: Pick; - projectMembershipDAL: Pick; - projectDAL: Pick; - projectKeyDAL: Pick; - projectBotDAL: Pick; - userGroupMembershipDAL: Pick; - projectUserMembershipRoleDAL: Pick; - smtpService: Pick; -}; - -type AddMembersToNonE2EEProjectDTO = { - emails: string[]; - usernames: string[]; - projectId: string; - projectMembershipRole: ProjectMembershipRole; - sendEmails?: boolean; -}; - -type AddMembersToNonE2EEProjectOptions = { - tx?: Knex; - throwOnProjectNotFound?: boolean; -}; - -export const addMembersToProject = ({ - orgDAL, - projectDAL, - projectMembershipDAL, - projectKeyDAL, - projectBotDAL, - userGroupMembershipDAL, - projectUserMembershipRoleDAL, - smtpService -}: TAddMembersToProjectArg) => { - // Can create multiple memberships for a singular project, based on user email / username - const addMembersToNonE2EEProject = async ( - { emails, usernames, projectId, projectMembershipRole, sendEmails }: AddMembersToNonE2EEProjectDTO, - options: AddMembersToNonE2EEProjectOptions = { throwOnProjectNotFound: true } - ) => { - const processTransaction = async (tx: Knex) => { - const usernamesAndEmails = [...emails, ...usernames]; - - const project = await projectDAL.findProjectById(projectId); - if (!project) { - if (options.throwOnProjectNotFound) { - throw new BadRequestError({ message: "Project not found when attempting to add user to project" }); - } - - return []; - } - - const orgMembers = await orgDAL.findOrgMembersByUsername( - project.orgId, - [...new Set(usernamesAndEmails.map((element) => element.toLowerCase()))], - tx - ); - - if (orgMembers.length !== usernamesAndEmails.length) - throw new BadRequestError({ message: "Some users are not part of org" }); - - if (!orgMembers.length) return []; - - const existingMembers = await projectMembershipDAL.find({ - projectId, - $in: { userId: orgMembers.map(({ user }) => user.id).filter(Boolean) } - }); - if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" }); - - const ghostUser = await projectDAL.findProjectGhostUser(projectId); - - if (!ghostUser) { - throw new BadRequestError({ - message: "Failed to find sudo user" - }); - } - - const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId); - - if (!ghostUserLatestKey) { - throw new BadRequestError({ - message: "Failed to find sudo user latest key" - }); - } - - const bot = await projectBotDAL.findOne({ projectId }); - if (!bot) { - throw new BadRequestError({ - message: "Failed to find bot" - }); - } - - const botPrivateKey = infisicalSymmetricDecrypt({ - keyEncoding: bot.keyEncoding as SecretKeyEncoding, - iv: bot.iv, - tag: bot.tag, - ciphertext: bot.encryptedPrivateKey - }); - - const newWsMembers = assignWorkspaceKeysToMembers({ - decryptKey: ghostUserLatestKey, - userPrivateKey: botPrivateKey, - members: orgMembers.map((membership) => ({ - orgMembershipId: membership.id, - projectMembershipRole, - userPublicKey: membership.user.publicKey - })) - }); - - const members: TProjectMemberships[] = []; - - const userIdsToExcludeForProjectKeyAddition = new Set( - await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernamesAndEmails, projectId) - ); - const projectMemberships = await projectMembershipDAL.insertMany( - orgMembers.map(({ user }) => ({ - projectId, - userId: user.id - })), - tx - ); - await projectUserMembershipRoleDAL.insertMany( - projectMemberships.map(({ id }) => ({ projectMembershipId: id, role: projectMembershipRole })), - tx - ); - - members.push(...projectMemberships); - - const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId); - await projectKeyDAL.insertMany( - orgMembers - .filter(({ user }) => !userIdsToExcludeForProjectKeyAddition.has(user.id)) - .map(({ user, id }) => ({ - encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey, - nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce, - senderId: ghostUser.id, - receiverId: user.id, - projectId - })), - tx - ); - - if (sendEmails) { - const recipients = orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string); - - const appCfg = getConfig(); - - if (recipients.length) { - await smtpService.sendMail({ - template: SmtpTemplates.WorkspaceInvite, - subjectLine: "Infisical project invitation", - recipients: orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string), - substitutions: { - workspaceName: project.name, - callback_url: `${appCfg.SITE_URL}/login` - } - }); - } - } - - return members; - }; - - if (options.tx) { - return processTransaction(options.tx); - } - return projectMembershipDAL.transaction(processTransaction); - }; - - return { - addMembersToNonE2EEProject - }; -}; diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts index 49a86f31b..e2ed4647d 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -42,7 +42,7 @@ type TProjectMembershipServiceFactoryDep = { projectUserMembershipRoleDAL: Pick; userDAL: Pick; userGroupMembershipDAL: TUserGroupMembershipDALFactory; - projectRoleDAL: Pick; + projectRoleDAL: Pick; orgDAL: Pick; projectDAL: Pick; projectKeyDAL: Pick; diff --git a/backend/src/services/project-membership/project-membership-types.ts b/backend/src/services/project-membership/project-membership-types.ts index 21efaf691..c50ee4c1b 100644 --- a/backend/src/services/project-membership/project-membership-types.ts +++ b/backend/src/services/project-membership/project-membership-types.ts @@ -53,4 +53,5 @@ export type TAddUsersToWorkspaceNonE2EEDTO = { sendEmails?: boolean; emails: string[]; usernames: string[]; + roleSlugs?: string[]; } & TProjectPermission; diff --git a/backend/src/services/project/project-fns.ts b/backend/src/services/project/project-fns.ts index cb836d932..d6b01c33b 100644 --- a/backend/src/services/project/project-fns.ts +++ b/backend/src/services/project/project-fns.ts @@ -16,7 +16,7 @@ export const assignWorkspaceKeysToMembers = ({ members, decryptKey, userPrivateK privateKey: userPrivateKey }); - const newWsMembers = members.map(({ orgMembershipId, userPublicKey, projectMembershipRole }) => { + const newWsMembers = members.map(({ orgMembershipId, userPublicKey }) => { const { ciphertext: inviteeCipherText, nonce: inviteeNonce } = encryptAsymmetric( plaintextProjectKey, userPublicKey, @@ -25,7 +25,6 @@ export const assignWorkspaceKeysToMembers = ({ members, decryptKey, userPrivateK return { orgMembershipId, - projectRole: projectMembershipRole, workspaceEncryptedKey: inviteeCipherText, workspaceEncryptedNonce: inviteeNonce }; diff --git a/backend/src/services/project/project-queue.ts b/backend/src/services/project/project-queue.ts index 8f1e3fc3f..d59bde6c1 100644 --- a/backend/src/services/project/project-queue.ts +++ b/backend/src/services/project/project-queue.ts @@ -300,8 +300,7 @@ export const projectQueueFactory = ({ members: [ { userPublicKey: user.publicKey, - orgMembershipId: orgMembership.id, - projectMembershipRole: ProjectMembershipRole.Admin + orgMembershipId: orgMembership.id } ] }); diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index f51c561d7..31ab297b8 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -277,8 +277,7 @@ export const projectServiceFactory = ({ members: [ { userPublicKey: user.publicKey, - orgMembershipId: orgMembership.id, - projectMembershipRole: ProjectMembershipRole.Admin + orgMembershipId: orgMembership.id } ] }); @@ -292,7 +291,7 @@ export const projectServiceFactory = ({ tx ); await projectUserMembershipRoleDAL.create( - { projectMembershipId: userProjectMembership.id, role: projectAdmin.projectRole }, + { projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin }, tx ); diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index fbbd32d2b..3bfdae709 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -1,4 +1,4 @@ -import { ProjectMembershipRole, TProjectKeys } from "@app/db/schemas"; +import { TProjectKeys } from "@app/db/schemas"; import { TProjectPermission } from "@app/lib/types"; import { ActorAuthMethod, ActorType } from "../auth/auth-type"; @@ -88,7 +88,6 @@ export type AddUserToWsDTO = { userPrivateKey: string; members: { orgMembershipId: string; - projectMembershipRole: ProjectMembershipRole; userPublicKey: string; }[]; }; diff --git a/frontend/src/components/signup/TeamInviteStep.tsx b/frontend/src/components/signup/TeamInviteStep.tsx index 934935a42..60276d217 100644 --- a/frontend/src/components/signup/TeamInviteStep.tsx +++ b/frontend/src/components/signup/TeamInviteStep.tsx @@ -60,7 +60,7 @@ export default function TeamInviteStep(): JSX.Element { placeholder="email@example.com, email2@example.com..." /> -
+