import { ForbiddenError } from "@casl/ability"; import jwt from "jsonwebtoken"; import { OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { getConfig } from "@app/lib/config/env"; import { generateAsymmetricKeyPair } from "@app/lib/crypto"; import { generateSymmetricKey, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { isDisposableEmail } from "@app/lib/validator"; import { AuthTokenType } from "../auth/auth-type"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TokenType } from "../auth-token/auth-token-types"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TUserDalFactory } from "../user/user-dal"; import { TIncidentContactsDalFactory } from "./incident-contacts-dal"; import { TOrgBotDalFactory } from "./org-bot-dal"; import { TOrgDalFactory } from "./org-dal"; import { TOrgRoleDalFactory } from "./org-role-dal"; import { TDeleteOrgMembershipDTO, TInviteUserToOrgDTO, TUpdateOrgMembershipDTO, TVerifyUserToOrgDTO } from "./org-types"; type TOrgServiceFactoryDep = { orgDal: TOrgDalFactory; orgBotDal: TOrgBotDalFactory; orgRoleDal: TOrgRoleDalFactory; userDal: TUserDalFactory; incidentContactDal: TIncidentContactsDalFactory; smtpService: TSmtpService; tokenService: TAuthTokenServiceFactory; permissionService: TPermissionServiceFactory; }; export type TOrgServiceFactory = ReturnType; export const orgServiceFactory = ({ orgDal, userDal, orgRoleDal, incidentContactDal, permissionService, smtpService, tokenService, orgBotDal }: TOrgServiceFactoryDep) => { /* * Get organization details by the organization id * */ const findOrganizationById = async (userId: string, orgId: string) => { await permissionService.getUserOrgPermission(userId, orgId); const org = await orgDal.findOrgById(orgId); if (!org) throw new BadRequestError({ name: "Org not found", message: "Organization not found" }); return org; }; /* * Get all organization a user part of * */ const findAllOrganizationOfUser = async (userId: string) => { const orgs = await orgDal.findAllOrgsByUserId(userId); return orgs; }; /* * Get all workspace members * */ const findAllOrgMembers = async (userId: string, orgId: string) => { const { permission } = await permissionService.getUserOrgPermission(userId, orgId); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionActions.Read, OrgPermissionSubjects.Member ); const members = await orgDal.findAllOrgMembers(orgId); return members; }; /* * Update organization settings * */ const updateOrgName = async (userId: string, orgId: string, name: string) => { const { permission } = await permissionService.getUserOrgPermission(userId, orgId); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionActions.Edit, OrgPermissionSubjects.Settings ); const org = await orgDal.updateById(orgId, { name }); if (!org) throw new BadRequestError({ name: "Org not found", message: "Organization not found" }); return org; }; /* * Create organization * */ const createOrganization = async (userId: string, orgName: string) => { const { privateKey, publicKey } = generateAsymmetricKeyPair(); const key = generateSymmetricKey(); const { ciphertext: encryptedPrivateKey, iv: privateKeyIV, tag: privateKeyTag, encoding: privateKeyKeyEncoding, algorithm: privateKeyAlgorithm } = infisicalSymmetricEncypt(privateKey); const { ciphertext: encryptedSymmetricKey, iv: symmetricKeyIV, tag: symmetricKeyTag, encoding: symmetricKeyKeyEncoding, algorithm: symmetricKeyAlgorithm } = infisicalSymmetricEncypt(key); const organization = await orgDal.transaction(async (tx) => { const org = await orgDal.create({ name: orgName }, tx); await orgDal.createMembership( { userId, orgId: org.id, role: OrgMembershipRole.Admin, status: OrgMembershipStatus.Accepted }, tx ); await orgBotDal.create( { name: org.name, publicKey, privateKeyIV, encryptedPrivateKey, symmetricKeyIV, symmetricKeyTag, encryptedSymmetricKey, symmetricKeyAlgorithm, orgId: org.id, privateKeyTag, privateKeyAlgorithm, privateKeyKeyEncoding, symmetricKeyKeyEncoding }, tx ); return org; }); return organization; }; /* * Delete organization by id * */ const deleteOrganizationById = async (userId: string, orgId: string) => { const { membership } = await permissionService.getUserOrgPermission(userId, orgId); if (membership.role !== OrgMembershipRole.Admin) throw new UnauthorizedError({ name: "Delete org by id", message: "Not an admin" }); const organization = await orgDal.deleteById(orgId); return organization; }; /* * Org membership management * Not another service because it has close ties with how an org works doesn't make sense to seperate them * */ const updateOrgMembership = async ({ role, orgId, userId, membershipId }: TUpdateOrgMembershipDTO) => { const { permission } = await permissionService.getUserOrgPermission(userId, orgId); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionActions.Edit, OrgPermissionSubjects.Member ); const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole); if (isCustomRole) { const customRole = await orgRoleDal.findOne({ slug: role, orgId }); if (!customRole) throw new BadRequestError({ name: "Update membership", message: "Role not found" }); const [membership] = await orgDal.updateMembership( { id: membershipId, orgId }, { role: OrgMembershipRole.Custom, roleId: customRole.id } ); return membership; } const [membership] = await orgDal.updateMembership( { id: membershipId, orgId }, { role, roleId: null } ); return membership; }; /* * Invite user to organization */ const inviteUserToOrganization = async ({ orgId, userId, inviteeEmail }: TInviteUserToOrgDTO) => { const { permission } = await permissionService.getUserOrgPermission(userId, orgId); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionActions.Create, OrgPermissionSubjects.Member ); // TODO(akhilmhdh-pg): SAML SSO check and licence check limit org members const invitee = await orgDal.transaction(async (tx) => { const inviteeUser = await userDal.findUserByEmail(inviteeEmail, tx); if (inviteeUser) { // if user already exist means its already part of infisical // Thus the signup flow is not needed anymore const [inviteeMembership] = await orgDal.findMembership( { orgId, userId: inviteeUser.id }, { tx } ); if (inviteeMembership && inviteeMembership.status === OrgMembershipStatus.Accepted) { throw new BadRequestError({ message: "Failed to invite an existing member of org", name: "Invite user to org" }); } if (!inviteeMembership) { await orgDal.createMembership( { userId: inviteeUser.id, inviteEmail: inviteeEmail, orgId, role: OrgMembershipRole.Member, status: OrgMembershipStatus.Invited }, tx ); } return inviteeUser; } const isEmailInvalid = await isDisposableEmail(inviteeEmail); if (isEmailInvalid) { throw new BadRequestError({ message: "Provided a disposable email", name: "Org invite" }); } // not invited before const user = await userDal.create({ email: inviteeEmail, isAccepted: false }); await orgDal.createMembership({ inviteEmail: inviteeEmail, orgId, role: OrgMembershipRole.Member, status: OrgMembershipStatus.Invited }); return user; }); const token = await tokenService.createTokenForUser({ type: TokenType.TOKEN_EMAIL_ORG_INVITATION, userId: invitee.id, orgId }); const org = await orgDal.findOrgById(orgId); const user = await userDal.findById(userId); const appCfg = getConfig(); await smtpService.sendMail({ template: SmtpTemplates.OrgInvite, subjectLine: "Infisical organization invitation", recipients: [inviteeEmail], substitutions: { inviterFirstName: user.firstName, inviterEmail: user.email, organizationName: org?.name, email: inviteeEmail, organizationId: org?.id.toString(), token, callback_url: `${appCfg.SITE_URL}/signupinvite` } }); if (!appCfg.isSmtpConfigured) { return `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${inviteeEmail}&organization_id=${org?.id}`; } }; /** * Organization invitation step 2: Verify that code [code] was sent to email [email] as part of * magic link and issue a temporary signup token for user to complete setting up their account */ const verifyUserToOrg = async ({ orgId, email, code }: TVerifyUserToOrgDTO) => { const user = await userDal.findUserByEmail(email); if (!user) { throw new BadRequestError({ message: "Invalid request", name: "Verify user to org" }); } const [orgMembership] = await orgDal.findMembership({ userId: user.id, status: OrgMembershipStatus.Invited, orgId }); if (!orgMembership) throw new BadRequestError({ message: "Failed to find invitation", name: "Verify user to org" }); await tokenService.validateTokenForUser({ type: TokenType.TOKEN_EMAIL_ORG_INVITATION, userId: user.id, orgId: orgMembership.orgId, code }); if (user.isAccepted) { // this means user has already completed signup process // isAccepted is set true when keys are exchanged await orgDal.updateMembershipById(orgMembership.id, { orgId, status: OrgMembershipStatus.Accepted }); // TODO(akhilmhdh-pg): update org licence subscription return { user }; } const appCfg = getConfig(); const token = jwt.sign( { authTokenType: AuthTokenType.SIGNUP_TOKEN, userId: user.id }, appCfg.JWT_AUTH_SECRET, { expiresIn: appCfg.JWT_SIGNUP_LIFETIME } ); return { token, user }; }; const deleteOrgMembership = async ({ orgId, userId, membershipId }: TDeleteOrgMembershipDTO) => { const { permission } = await permissionService.getUserOrgPermission(userId, orgId); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionActions.Delete, OrgPermissionSubjects.Member ); const membership = await orgDal.deleteMembershipById(membershipId, orgId); return membership; }; /* * CRUD operations of incident contacts * */ const findIncidentContacts = async (userId: string, orgId: string) => { const { permission } = await permissionService.getUserOrgPermission(userId, orgId); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount ); const incidentContacts = await incidentContactDal.findByOrgId(orgId); return incidentContacts; }; const createIncidentContact = async (userId: string, orgId: string, email: string) => { const { permission } = await permissionService.getUserOrgPermission(userId, orgId); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionActions.Create, OrgPermissionSubjects.IncidentAccount ); const doesIncidentContactExist = await incidentContactDal.findOne(orgId, { email }); if (doesIncidentContactExist) { throw new BadRequestError({ message: "Incident contact already exist", name: "Incident contact exist" }); } const incidentContact = await incidentContactDal.create(orgId, email); return incidentContact; }; const deleteIncidentContact = async (userId: string, orgId: string, id: string) => { const { permission } = await permissionService.getUserOrgPermission(userId, orgId); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionActions.Delete, OrgPermissionSubjects.IncidentAccount ); const incidentContact = await incidentContactDal.deleteById(id, orgId); return incidentContact; }; return { findOrganizationById, findAllOrgMembers, findAllOrganizationOfUser, inviteUserToOrganization, verifyUserToOrg, updateOrgName, createOrganization, deleteOrganizationById, deleteOrgMembership, updateOrgMembership, // incident contacts findIncidentContacts, createIncidentContact, deleteIncidentContact }; };