feat: almost done with most ts stuff

This commit is contained in:
=
2025-10-10 00:09:46 +05:30
parent 44c9575a3d
commit c2490380f0
28 changed files with 551 additions and 1432 deletions
+3 -3
View File
@@ -344,7 +344,7 @@ export const orgDALFactory = (db: TDbClient) => {
const count = await db
.replicaNode()(TableName.Membership)
.where(`${TableName.Membership}.orgId`, orgId)
.where(`${TableName.Membership}.scopeOrgId`, orgId)
.where(`${TableName.Membership}.scope`, AccessScope.Organization)
.whereNotNull(`${TableName.Membership}.actorUserId`)
.count("*")
@@ -376,7 +376,7 @@ export const orgDALFactory = (db: TDbClient) => {
.select(
conn.ref("id").withSchema(TableName.Membership),
conn.ref("inviteEmail").withSchema(TableName.Membership),
conn.ref("orgId").withSchema(TableName.Membership),
conn.ref("scopeOrgId").withSchema(TableName.Membership).as("orgId"),
db.ref("role").withSchema(TableName.MembershipRole),
db.ref("customRoleId").withSchema(TableName.MembershipRole).as("roleId"),
conn.ref("status").withSchema(TableName.Membership),
@@ -416,7 +416,7 @@ export const orgDALFactory = (db: TDbClient) => {
.select(
conn.ref("id").withSchema(TableName.Membership),
conn.ref("inviteEmail").withSchema(TableName.Membership),
conn.ref("orgId").withSchema(TableName.Membership),
conn.ref("scopeOrgId").withSchema(TableName.Membership).as("orgId"),
conn.ref("role").withSchema(TableName.MembershipRole),
conn.ref("customRoleId").withSchema(TableName.MembershipRole).as("roleId"),
conn.ref("status").withSchema(TableName.Membership),
+51 -102
View File
@@ -1,122 +1,59 @@
import { AccessScope } from "@app/db/schemas";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { BadRequestError } from "@app/lib/errors";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
import { TMembershipRoleDALFactory } from "../membership/membership-role-dal";
import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal";
type TDeleteOrgMembership = {
orgMembershipId: string;
orgId: string;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "deleteMembershipById" | "transaction">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findProjectMembershipsByUserId">;
membershipUserDAL: Pick<TMembershipUserDALFactory, "delete">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete">;
userAliasDAL: Pick<TUserAliasDALFactory, "delete">;
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
userId?: string;
};
type TDeleteOrgMemberships = {
orgMembershipIds: string[];
orgId: string;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "deleteMembershipsById" | "transaction">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findProjectMembershipsByUserIds">;
membershipUserDAL: Pick<TMembershipUserDALFactory, "delete">;
orgDAL: Pick<TOrgDALFactory, "transaction">;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "delete">;
membershipUserDAL: Pick<TMembershipUserDALFactory, "delete" | "find">;
membershipRoleDAL: Pick<TMembershipRoleDALFactory, "delete">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete">;
userAliasDAL: Pick<TUserAliasDALFactory, "delete">;
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
userId?: string;
};
export const deleteOrgMembershipFn = async ({
orgMembershipId,
orgId,
orgDAL,
projectMembershipDAL,
projectKeyDAL,
userAliasDAL,
licenseService,
userId,
membershipUserDAL
}: TDeleteOrgMembership) => {
const deletedMembership = await orgDAL.transaction(async (tx) => {
const orgMembership = await orgDAL.deleteMembershipById(orgMembershipId, orgId, tx);
if (userId && orgMembership.actorUserId === userId) {
// scott: this is temporary, we will add a leave org endpoint with proper handling to ensure org isn't abandoned/broken
throw new BadRequestError({ message: "You cannot remove yourself from an organization" });
}
const deletedUserId = orgMembership.actorUserId;
if (!deletedUserId) {
await licenseService.updateSubscriptionOrgMemberCount(orgId);
return orgMembership;
}
await userAliasDAL.delete(
{
userId: deletedUserId,
orgId
},
tx
);
// Get all the project memberships of the user in the organization
const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserId(orgId, deletedUserId);
// Delete all the project memberships of the user in the organization
await membershipUserDAL.delete(
{
scope: AccessScope.Project,
$in: {
id: projectMemberships.map((membership) => membership.id)
}
},
tx
);
// Get all the project keys of the user in the organization
const projectKeys = await projectKeyDAL.find({
$in: {
projectId: projectMemberships.map((membership) => membership.projectId)
},
receiverId: deletedUserId
});
// Delete all the project keys of the user in the organization
await projectKeyDAL.delete(
{
$in: {
id: projectKeys.map((key) => key.id)
}
},
tx
);
await licenseService.updateSubscriptionOrgMemberCount(orgId);
return orgMembership;
});
return deletedMembership;
};
export const deleteOrgMembershipsFn = async ({
orgMembershipIds,
orgId,
orgDAL,
projectMembershipDAL,
projectKeyDAL,
userAliasDAL,
licenseService,
userId,
membershipUserDAL
membershipUserDAL,
userGroupMembershipDAL,
membershipRoleDAL
}: TDeleteOrgMemberships) => {
const deletedMemberships = await orgDAL.transaction(async (tx) => {
const orgMemberships = await orgDAL.deleteMembershipsById(orgMembershipIds, orgId, tx);
await membershipRoleDAL.delete(
{
$in: {
membershipId: orgMembershipIds
}
},
tx
);
const orgMemberships = await membershipUserDAL.delete(
{
scopeOrgId: orgId,
scope: AccessScope.Organization,
$in: {
id: orgMembershipIds
}
},
tx
);
const membershipUserIds = orgMemberships
.filter((member) => Boolean(member.actorUserId))
@@ -143,32 +80,44 @@ export const deleteOrgMembershipsFn = async ({
);
// Get all the project memberships of the users in the organization
const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserIds(orgId, membershipUserIds);
// Delete all the project memberships of the users in the organization
await membershipUserDAL.delete(
const otherMemberships = await membershipUserDAL.delete(
{
scope: AccessScope.Project,
scopeOrgId: orgId,
$in: {
id: projectMemberships.map((membership) => membership.id)
actorUserId: membershipUserIds
}
},
tx
);
// Get all the project keys of the user in the organization
const projectKeys = await projectKeyDAL.find({
$in: {
projectId: projectMemberships.map((membership) => membership.projectId),
receiverId: membershipUserIds
}
const orgGroups = await membershipUserDAL.find({
scopeOrgId: orgId,
$notNull: ["actorGroupId"]
});
const groupIds = orgGroups.filter((el) => el.actorGroupId).map((el) => el.actorGroupId as string);
await userGroupMembershipDAL.delete(
{
$in: {
userId: membershipUserIds,
groupId: groupIds
}
},
tx
);
const projectIds = otherMemberships
.filter((el) => el.scope === AccessScope.Project && el.scopeProjectId)
.map((el) => el.scopeProjectId as string);
// Delete all the project keys of the user in the organization
await projectKeyDAL.delete(
{
$in: {
id: projectKeys.map((key) => key.id)
projectId: projectIds,
receiverId: membershipUserIds
}
},
tx
@@ -1,237 +0,0 @@
import { ForbiddenError } from "@casl/ability";
import { packRules } from "@casl/ability/extra";
import { TOrgRolesInsert, TOrgRolesUpdate } from "@app/db/schemas";
import {
orgAdminPermissions,
orgMemberPermissions,
orgNoAccessPermissions,
OrgPermissionActions,
OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TExternalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
type TOrgRoleServiceFactoryDep = {
orgRoleDAL: TOrgRoleDALFactory;
permissionService: TPermissionServiceFactory;
orgDAL: TOrgDALFactory;
externalGroupOrgRoleMappingDAL: TExternalGroupOrgRoleMappingDALFactory;
};
export type TOrgRoleServiceFactory = ReturnType<typeof orgRoleServiceFactory>;
export const orgRoleServiceFactory = ({
orgRoleDAL,
orgDAL,
permissionService,
externalGroupOrgRoleMappingDAL
}: TOrgRoleServiceFactoryDep) => {
const createRole = async (
userId: string,
orgId: string,
data: Omit<TOrgRolesInsert, "orgId">,
actorAuthMethod: ActorAuthMethod,
actorOrgId: string | undefined
) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Role);
const existingRole = await orgRoleDAL.findOne({ slug: data.slug, orgId });
if (existingRole) throw new BadRequestError({ name: "Create Role", message: "Duplicate role" });
const role = await orgRoleDAL.create({
...data,
orgId,
permissions: JSON.stringify(data.permissions)
});
return role;
};
const getRole = async (
userId: string,
orgId: string,
roleId: string,
actorAuthMethod: ActorAuthMethod,
actorOrgId: string | undefined
) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
switch (roleId) {
case "b11b49a9-09a9-4443-916a-4246f9ff2c69": {
return {
id: roleId,
orgId,
name: "Admin",
slug: "admin",
description: "Complete administration access over the organization",
permissions: packRules(orgAdminPermissions),
createdAt: new Date(),
updatedAt: new Date()
};
}
case "b11b49a9-09a9-4443-916a-4246f9ff2c70": {
return {
id: roleId,
orgId,
name: "Member",
slug: "member",
description: "Non-administrative role in an organization",
permissions: packRules(orgMemberPermissions),
createdAt: new Date(),
updatedAt: new Date()
};
}
case "b10d49a9-09a9-4443-916a-4246f9ff2c72": {
return {
id: "b10d49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response
orgId,
name: "No Access",
slug: "no-access",
description: "No access to any resources in the organization",
permissions: packRules(orgNoAccessPermissions),
createdAt: new Date(),
updatedAt: new Date()
};
}
default: {
const role = await orgRoleDAL.findOne({ id: roleId, orgId });
if (!role) throw new NotFoundError({ message: `Organization role with ID '${roleId}' not found` });
return role;
}
}
};
const updateRole = async (
userId: string,
orgId: string,
roleId: string,
data: Omit<TOrgRolesUpdate, "orgId">,
actorAuthMethod: ActorAuthMethod,
actorOrgId: string | undefined
) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Role);
if (data?.slug) {
const existingRole = await orgRoleDAL.findOne({ slug: data.slug, orgId });
if (existingRole && existingRole.id !== roleId)
throw new BadRequestError({ name: "Update Role", message: "Duplicate role" });
}
const [updatedRole] = await orgRoleDAL.update(
{ id: roleId, orgId },
{ ...data, permissions: data.permissions ? JSON.stringify(data.permissions) : undefined }
);
if (!updatedRole) throw new NotFoundError({ message: `Organization role with ID '${roleId}' not found` });
return updatedRole;
};
const deleteRole = async (
userId: string,
orgId: string,
roleId: string,
actorAuthMethod: ActorAuthMethod,
actorOrgId: string | undefined
) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Role);
const org = await orgDAL.findOrgById(orgId);
if (!org)
throw new NotFoundError({
message: `Organization with ID '${orgId}' not found`
});
if (org.defaultMembershipRole === roleId)
throw new BadRequestError({
message: "Cannot delete default org membership role. Please re-assign and try again."
});
const externalGroupMapping = await externalGroupOrgRoleMappingDAL.findOne({
orgId,
roleId
});
if (externalGroupMapping)
throw new BadRequestError({
message:
"Cannot delete role assigned to external group organization role mapping. Please re-assign external mapping and try again."
});
const [deletedRole] = await orgRoleDAL.delete({ id: roleId, orgId });
if (!deletedRole)
throw new NotFoundError({ message: `Organization role with ID '${roleId}' not found`, name: "UpdateRole" });
return deletedRole;
};
const listRoles = async (
userId: string,
orgId: string,
actorAuthMethod: ActorAuthMethod,
actorOrgId: string | undefined
) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
const customRoles = await orgRoleDAL.find({ orgId });
const roles = [
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // dummy userid
orgId,
name: "Admin",
slug: "admin",
description: "Complete administration access over the organization",
permissions: packRules(orgAdminPermissions),
createdAt: new Date(),
updatedAt: new Date()
},
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c70", // dummy user for zod validation in response
orgId,
name: "Member",
slug: "member",
description: "Non-administrative role in an organization",
permissions: packRules(orgMemberPermissions),
createdAt: new Date(),
updatedAt: new Date()
},
{
id: "b10d49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response
orgId,
name: "No Access",
slug: "no-access",
description: "No access to any resources in the organization",
permissions: packRules(orgNoAccessPermissions),
createdAt: new Date(),
updatedAt: new Date()
},
...(customRoles || []).map(({ permissions, ...data }) => ({
...data,
permissions
}))
];
return roles;
};
const getUserPermission = async (
userId: string,
orgId: string,
actorAuthMethod: ActorAuthMethod,
actorOrgId: string | undefined
) => {
const { permission, memberships } = await permissionService.getOrgPermission(
ActorType.USER,
userId,
orgId,
actorAuthMethod,
actorOrgId
);
return { permissions: packRules(permission.rules), memberships };
};
return { createRole, getRole, updateRole, deleteRole, listRoles, getUserPermission };
};
+15 -394
View File
@@ -4,19 +4,14 @@ import { Knex } from "knex";
import {
AccessScope,
ActionProjectType,
OrgMembershipRole,
OrgMembershipStatus,
ProjectMembershipRole,
ProjectVersion,
TableName,
TOidcConfigs,
TProjectMemberships,
TProjectUserMembershipRolesInsert,
TSamlConfigs,
TUsers
TSamlConfigs
} from "@app/db/schemas";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { TLdapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
@@ -26,29 +21,16 @@ import {
OrgPermissionSecretShareAction,
OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission";
import {
constructPermissionErrorMessage,
validatePrivilegeChangeOperation
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSamlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal";
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography";
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
import { applyJitter } from "@app/lib/dates";
import { delay as delayMs } from "@app/lib/delay";
import {
BadRequestError,
ForbiddenRequestError,
NotFoundError,
PermissionBoundaryError,
UnauthorizedError
} from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { isDisposableEmail } from "@app/lib/validator";
import { QueueName } from "@app/queue";
import { getDefaultOrgMembershipRoleForUpdateOrg } from "@app/services/org/org-role-fns";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
@@ -76,7 +58,7 @@ import { TUserDALFactory } from "../user/user-dal";
import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
import { TOrgBotDALFactory } from "./org-bot-dal";
import { TOrgDALFactory } from "./org-dal";
import { deleteOrgMembershipFn, deleteOrgMembershipsFn } from "./org-fns";
import { deleteOrgMembershipsFn } from "./org-fns";
import {
TDeleteOrgMembershipDTO,
TDeleteOrgMembershipsDTO,
@@ -84,7 +66,6 @@ import {
TFindOrgMembersByEmailDTO,
TGetOrgGroupsDTO,
TGetOrgMembershipDTO,
TInviteUserToOrgDTO,
TListProjectMembershipsByOrgMembershipIdDTO,
TResendOrgMemberInvitationDTO,
TUpdateOrgDTO,
@@ -130,6 +111,7 @@ type TOrgServiceFactoryDep = {
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
loginService: Pick<TAuthLoginFactory, "generateUserTokens">;
reminderService: Pick<TReminderServiceFactory, "deleteReminderBySecretId">;
userGroupMembershipDAL: TUserGroupMembershipDALFactory;
};
export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
@@ -161,7 +143,8 @@ export const orgServiceFactory = ({
loginService,
reminderService,
membershipRoleDAL,
membershipUserDAL
membershipUserDAL,
userGroupMembershipDAL
}: TOrgServiceFactoryDep) => {
/*
* Get organization details by the organization id
@@ -898,369 +881,6 @@ export const orgServiceFactory = ({
return { signupToken: undefined };
};
/*
* Invite user to organization
*/
const inviteUserToOrganization = async ({
orgId,
actorId,
actor,
inviteeEmails,
organizationRoleSlug,
projects: invitedProjects,
actorAuthMethod,
actorOrgId
}: TInviteUserToOrgDTO) => {
const appCfg = getConfig();
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
const invitingUser = await userDAL.findOne({ id: actorId });
const org = await orgDAL.findOrgById(orgId);
const isEmailInvalid = await isDisposableEmail(inviteeEmails);
if (isEmailInvalid) {
throw new BadRequestError({
message: "Disposable emails are not allowed",
name: "InviteUser"
});
}
const plan = await licenseService.getPlan(orgId);
const isCustomOrgRole = !Object.values(OrgMembershipRole).includes(organizationRoleSlug as OrgMembershipRole);
if (isCustomOrgRole) {
if (!plan?.rbac)
throw new BadRequestError({
message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
});
}
const projectsToInvite = invitedProjects?.length
? await projectDAL.find({
orgId,
$in: {
id: invitedProjects?.map(({ id }) => id)
}
})
: [];
if (projectsToInvite.length !== invitedProjects?.length) {
throw new ForbiddenRequestError({
message: "Access denied to one or more of the specified projects"
});
}
if (projectsToInvite.some((el) => el.version !== ProjectVersion.V3)) {
throw new BadRequestError({
message: "One or more selected projects are not compatible with this operation. Please upgrade your projects."
});
}
const mailsForOrgInvitation: { email: string; userId: string; firstName: string; lastName: string }[] = [];
const mailsForProjectInvitation: { email: string[]; projectName: string }[] = [];
const newProjectMemberships: TProjectMemberships[] = [];
await orgDAL.transaction(async (tx) => {
const users: Pick<TUsers, "id" | "firstName" | "lastName" | "email" | "username">[] = [];
for await (const inviteeEmail of inviteeEmails) {
const usersByUsername = await userDAL.findUserByUsername(inviteeEmail, tx);
let inviteeUser =
usersByUsername?.length > 1
? usersByUsername.find((el) => el.username === inviteeEmail)
: usersByUsername?.[0];
// if the user doesn't exist we create the user with the email
if (!inviteeUser) {
// TODO(carlos): will be removed once the function receives usernames instead of emails
const usersByEmail = await userDAL.findUserByEmail(inviteeEmail, tx);
if (usersByEmail?.length === 1) {
[inviteeUser] = usersByEmail;
} else {
inviteeUser = await userDAL.create(
{
isAccepted: false,
email: inviteeEmail,
username: inviteeEmail,
authMethods: [AuthMethod.EMAIL],
isGhost: false
},
tx
);
}
}
const inviteeUserId = inviteeUser?.id;
const existingEncrytionKey = await userDAL.findUserEncKeyByUserId(inviteeUserId, tx);
// when user is missing the encrytion keys
// this could happen either if user doesn't exist or user didn't find step 3 of generating the encryption keys of srp
// So what we do is we generate a random secure password and then encrypt it with a random pub-private key
// Then when user sign in (as login is not possible as isAccepted is false) we rencrypt the private key with the user password
if (!inviteeUser || (inviteeUser && !inviteeUser?.isAccepted && !existingEncrytionKey)) {
await userDAL.createUserEncryption(
{
userId: inviteeUserId,
encryptionVersion: 2
},
tx
);
}
const [inviteeOrgMembership] = await orgDAL.findMembership(
{
[`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId,
[`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization,
[`${TableName.Membership}.id` as "id"]: inviteeUserId
},
{ tx }
);
// if there exist no org membership we set is as given by the request
if (!inviteeOrgMembership) {
if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
// limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
throw new BadRequestError({
name: "InviteUser",
message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members."
});
}
if (org?.authEnforced) {
throw new ForbiddenRequestError({
name: "InviteUser",
message: "Failed to invite user due to org-level auth enforced for organization"
});
}
// as its used by project invite also
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
let roleId;
const orgRole = isCustomOrgRole ? OrgMembershipRole.Custom : organizationRoleSlug;
if (isCustomOrgRole) {
const customRole = await roleDAL.findOne({ slug: organizationRoleSlug, orgId });
if (!customRole) {
throw new NotFoundError({
name: "InviteUser",
message: `Custom organization role with slug '${orgRole}' not found`
});
}
roleId = customRole.id;
}
const membership = await orgDAL.createMembership(
{
actorUserId: inviteeUser.id,
inviteEmail: inviteeEmail,
scopeOrgId: orgId,
status: OrgMembershipStatus.Invited,
isActive: true,
scope: AccessScope.Organization
},
tx
);
await membershipRoleDAL.create(
{
membershipId: membership.id,
role: orgRole,
customRoleId: roleId
},
tx
);
mailsForOrgInvitation.push({
email: inviteeEmail,
userId: inviteeUser.id,
firstName: inviteeUser?.firstName || "",
lastName: inviteeUser.lastName || ""
});
}
users.push(inviteeUser);
}
const userIds = users.map(({ id }) => id);
const userEncryptionKeys = await userDAL.findUserEncKeyByUserIdsBatch({ userIds }, tx);
// we don't need to spam with email. Thus org invitation doesn't need project invitation again
const userIdsWithOrgInvitation = new Set(mailsForOrgInvitation.map((el) => el.userId));
// if there exist no project membership we set is as given by the request
for await (const project of projectsToInvite) {
const projectId = project.id;
const { permission: projectPermission, membership } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(projectPermission).throwUnlessCan(
ProjectPermissionMemberActions.Create,
ProjectPermissionSub.Member
);
const existingMembers = await membershipUserDAL.find(
{
scopeOrgId: project.orgId,
scope: AccessScope.Project,
scopeProjectId: project.id,
$in: { userId: userIds }
},
{ tx }
);
const existingMembersGroupByUserId = groupBy(existingMembers, (i) => i.actorUserId as string);
const userWithEncryptionKeyInvitedToProject = userEncryptionKeys.filter(
(user) => !existingMembersGroupByUserId?.[user.userId]
);
// eslint-disable-next-line no-continue
if (!userWithEncryptionKeyInvitedToProject.length) continue;
// validate custom project role
const invitedProjectRoles = invitedProjects.find((el) => el.id === project.id)?.projectRoleSlug || [
ProjectMembershipRole.Member
];
for await (const invitedRole of invitedProjectRoles) {
const { permission: rolePermission } = await permissionService.getProjectPermissionByRole(
invitedRole,
projectId
);
if (invitedRole !== ProjectMembershipRole.NoAccess) {
const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionMemberActions.GrantPrivileges,
ProjectPermissionSub.Member,
projectPermission,
rolePermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to invite user to the project",
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionMemberActions.GrantPrivileges,
ProjectPermissionSub.Member
),
details: { missingPermissions: permissionBoundary.missingPermissions }
});
}
}
const customProjectRoles = invitedProjectRoles.filter(
(role) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole)
);
const hasCustomRole = Boolean(customProjectRoles.length);
if (hasCustomRole) {
if (!plan?.rbac)
throw new BadRequestError({
name: "InviteUser",
message:
"Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
});
}
const customRoles = hasCustomRole
? await projectRoleDAL.find({
projectId,
$in: { slug: customProjectRoles.map((role) => role) }
})
: [];
if (customRoles.length !== customProjectRoles.length) {
throw new NotFoundError({ name: "InviteUser", message: "Custom project role not found" });
}
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
const projectMemberships = await projectMembershipDAL.insertMany(
userWithEncryptionKeyInvitedToProject.map((userEnc) => ({
projectId,
userId: userEnc.userId
})),
tx
);
newProjectMemberships.push(...projectMemberships);
const sanitizedProjectMembershipRoles: TProjectUserMembershipRolesInsert[] = [];
invitedProjectRoles.forEach((projectRole) => {
const isCustomRole = Boolean(customRolesGroupBySlug?.[projectRole]?.[0]);
projectMemberships.forEach((membershipEntry) => {
sanitizedProjectMembershipRoles.push({
projectMembershipId: membershipEntry.id,
role: isCustomRole ? ProjectMembershipRole.Custom : projectRole,
customRoleId: customRolesGroupBySlug[projectRole] ? customRolesGroupBySlug[projectRole][0].id : null
});
});
});
await projectUserMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
mailsForProjectInvitation.push({
email: userWithEncryptionKeyInvitedToProject
.filter((el) => !userIdsWithOrgInvitation.has(el.userId))
.map((el) => el.email || el.username),
projectName: project.name
});
}
return users;
});
await licenseService.updateSubscriptionOrgMemberCount(orgId);
const signupTokens: { email: string; link: string }[] = [];
// send org invite mail
await Promise.allSettled(
mailsForOrgInvitation.map(async (el) => {
const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
userId: el.userId,
orgId
});
signupTokens.push({
email: el.email,
link: `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${el.email}&organization_id=${org?.id}`
});
return smtpService.sendMail({
template: SmtpTemplates.OrgInvite,
subjectLine: "Infisical organization invitation",
recipients: [el.email],
substitutions: {
inviterFirstName: invitingUser?.firstName,
inviterUsername: invitingUser?.email,
organizationName: org?.name,
email: el.email,
organizationId: org?.id.toString(),
token,
callback_url: `${appCfg.SITE_URL}/signupinvite`
}
});
})
);
await Promise.allSettled(
mailsForProjectInvitation
.filter((el) => Boolean(el.email.length))
.map(async (el) => {
return smtpService.sendMail({
template: SmtpTemplates.WorkspaceInvite,
subjectLine: "Infisical project invitation",
recipients: el.email,
substitutions: {
workspaceName: el.projectName,
callback_url: `${appCfg.SITE_URL}/login`
}
});
})
);
if (!appCfg.isSmtpConfigured) {
return { signupTokens, projectMemberships: newProjectMemberships };
}
return { signupTokens: undefined, projectMemberships: newProjectMemberships };
};
/**
* 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
@@ -1370,16 +990,17 @@ export const orgServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member);
const deletedMembership = await deleteOrgMembershipFn({
orgMembershipId: membershipId,
const [deletedMembership] = await deleteOrgMembershipsFn({
orgMembershipIds: [membershipId],
orgId,
orgDAL,
projectMembershipDAL,
projectKeyDAL,
userAliasDAL,
licenseService,
userId,
membershipUserDAL
membershipUserDAL,
membershipRoleDAL,
userGroupMembershipDAL
});
return deletedMembership;
@@ -1409,12 +1030,13 @@ export const orgServiceFactory = ({
orgMembershipIds: membershipIds,
orgId,
orgDAL,
projectMembershipDAL,
projectKeyDAL,
userAliasDAL,
licenseService,
userId,
membershipUserDAL
membershipUserDAL,
membershipRoleDAL,
userGroupMembershipDAL
});
return deletedMemberships;
@@ -1572,7 +1194,6 @@ export const orgServiceFactory = ({
findAllOrgMembers,
findAllOrganizationOfUser,
findIdentityOrganization,
inviteUserToOrganization,
verifyUserToOrg,
updateOrg,
findOrgMembersByUsername,