mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 12:27:28 +00:00
414 lines
13 KiB
TypeScript
414 lines
13 KiB
TypeScript
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<typeof orgServiceFactory>;
|
|
|
|
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
|
|
};
|
|
};
|