diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index ce18b6bdf..8f5dd0f23 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -287,9 +287,6 @@ declare module "fastify" { telemetry: TTelemetryServiceFactory; dynamicSecret: TDynamicSecretServiceFactory; dynamicSecretLease: TDynamicSecretLeaseServiceFactory; - projectUserAdditionalPrivilege: TProjectUserAdditionalPrivilegeServiceFactory; - identityProjectAdditionalPrivilege: TIdentityProjectAdditionalPrivilegeServiceFactory; - identityProjectAdditionalPrivilegeV2: TIdentityProjectAdditionalPrivilegeV2ServiceFactory; secretSharing: TSecretSharingServiceFactory; rateLimit: TRateLimitServiceFactory; userEngagement: TUserEngagementServiceFactory; diff --git a/backend/src/db/migrations/20250925145952_simplify-membership.ts b/backend/src/db/migrations/20250925145952_simplify-membership.ts index 812ed3138..a5d8df844 100644 --- a/backend/src/db/migrations/20250925145952_simplify-membership.ts +++ b/backend/src/db/migrations/20250925145952_simplify-membership.ts @@ -40,7 +40,7 @@ export async function up(knex: Knex): Promise { t.string("status").defaultTo("invited"); t.string("inviteEmail"); t.datetime("lastInvitedAt"); - t.datetime("lastLoginAuthMethod"); + t.string("lastLoginAuthMethod"); t.datetime("lastLoginTime"); t.specificType("projectFavorites", "text[]"); t.timestamps(true, true, true); diff --git a/backend/src/db/schemas/memberships.ts b/backend/src/db/schemas/memberships.ts index 2179bce6e..55115091b 100644 --- a/backend/src/db/schemas/memberships.ts +++ b/backend/src/db/schemas/memberships.ts @@ -16,11 +16,11 @@ export const MembershipsSchema = z.object({ scopeOrgId: z.string().uuid(), scopeProjectId: z.string().nullable().optional(), scopeNamespaceId: z.string().uuid().nullable().optional(), - isActive: z.boolean().nullable().optional(), + isActive: z.boolean().default(true), status: z.string().default("invited").nullable().optional(), inviteEmail: z.string().nullable().optional(), lastInvitedAt: z.date().nullable().optional(), - lastLoginAuthMethod: z.date().nullable().optional(), + lastLoginAuthMethod: z.string().nullable().optional(), lastLoginTime: z.date().nullable().optional(), projectFavorites: z.string().array().nullable().optional(), createdAt: z.date(), diff --git a/backend/src/db/seeds/2-org.ts b/backend/src/db/seeds/2-org.ts index a02224dbc..99bd70f57 100644 --- a/backend/src/db/seeds/2-org.ts +++ b/backend/src/db/seeds/2-org.ts @@ -1,6 +1,6 @@ import { Knex } from "knex"; -import { OrgMembershipRole, OrgMembershipStatus, TableName } from "../schemas"; +import { AccessScope, OrgMembershipRole, OrgMembershipStatus, TableName } from "../schemas"; import { seedData1 } from "../seed-data"; export async function seed(knex: Knex): Promise { @@ -24,13 +24,22 @@ export async function seed(knex: Knex): Promise { ]) .returning("*"); - await knex(TableName.OrgMembership).insert([ + const [membership] = await knex(TableName.Membership) + .insert([ + { + scope: AccessScope.Organization, + scopeOrgId: org.id, + actorUserId: user.id, + isActive: true, + status: OrgMembershipStatus.Accepted + } + ]) + .returning("*"); + + await knex(TableName.MembershipRole).insert([ { - role: OrgMembershipRole.Admin, - orgId: org.id, - status: OrgMembershipStatus.Accepted, - userId: user.id, - isActive: true + membershipId: membership.id, + role: OrgMembershipRole.Admin } ]); } diff --git a/backend/src/db/seeds/3-project.ts b/backend/src/db/seeds/3-project.ts index 47a41a95c..0ed462365 100644 --- a/backend/src/db/seeds/3-project.ts +++ b/backend/src/db/seeds/3-project.ts @@ -6,14 +6,14 @@ import { generateUserSrpKeys } from "@app/lib/crypto/srp"; import { initLogger, logger } from "@app/lib/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { AuthMethod } from "@app/services/auth/auth-type"; +import { membershipUserDALFactory } from "@app/services/membership-user/membership-user-dal"; import { assignWorkspaceKeysToMembers, createProjectKey } from "@app/services/project/project-fns"; import { projectKeyDALFactory } from "@app/services/project-key/project-key-dal"; -import { projectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; -import { projectUserMembershipRoleDALFactory } from "@app/services/project-membership/project-user-membership-role-dal"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { userDALFactory } from "@app/services/user/user-dal"; import { + AccessScope, OrgMembershipRole, OrgMembershipStatus, ProjectMembershipRole, @@ -23,6 +23,7 @@ import { TableName } from "../schemas"; import { seedData1 } from "../seed-data"; +import { membershipRoleDALFactory } from "@app/services/membership/membership-role-dal"; export const DEFAULT_PROJECT_ENVS = [ { name: "Development", slug: "dev" }, @@ -39,8 +40,8 @@ const createUserWithGhostUser = async ( ) => { const projectKeyDAL = projectKeyDALFactory(knex); const userDAL = userDALFactory(knex); - const projectMembershipDAL = projectMembershipDALFactory(knex); - const projectUserMembershipRoleDAL = projectUserMembershipRoleDALFactory(knex); + const membershipDAL = membershipUserDALFactory(knex); + const membershipRoleDAL = membershipRoleDALFactory(knex); const email = `sudo-${alphaNumericNanoId(16)}-${orgId}@infisical.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. @@ -63,25 +64,36 @@ const createUserWithGhostUser = async ( .onConflict("userId") .merge(); - await knex(TableName.OrgMembership) + const [orgMembership] = await knex(TableName.Membership) .insert({ - orgId, - userId: ghostUser.id, - role: OrgMembershipRole.Admin, + scope: AccessScope.Organization, + scopeOrgId: orgId, + actorUserId: ghostUser.id, status: OrgMembershipStatus.Accepted, isActive: true }) .returning("*"); - const [projectMembership] = await knex(TableName.ProjectMembership) + await knex(TableName.MembershipRole).insert([ + { + membershipId: orgMembership.id, + role: OrgMembershipRole.Admin + } + ]); + + const [projectMembership] = await knex(TableName.Membership) .insert({ - userId: ghostUser.id, - projectId + actorUserId: ghostUser.id, + scopeProjectId: projectId, + scope: AccessScope.Project, + scopeOrgId: orgId, + status: OrgMembershipStatus.Accepted, + isActive: true }) .returning("*"); - await knex(TableName.ProjectUserMembershipRole).insert({ - projectMembershipId: projectMembership.id, + await knex(TableName.MembershipRole).insert({ + membershipId: projectMembership.id, role: ProjectMembershipRole.Admin }); @@ -142,17 +154,16 @@ const createUserWithGhostUser = async ( }); // Create a membership for the user - const userProjectMembership = await projectMembershipDAL.create( + const userProjectMembership = await membershipDAL.create( { - projectId, - userId: user.id + scopeProjectId: projectId, + scope: AccessScope.Project, + actorUserId: user.id, + scopeOrgId: orgId }, knex ); - await projectUserMembershipRoleDAL.create( - { projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin }, - knex - ); + await membershipRoleDAL.create({ membershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin }, knex); // Create a project key for the user await projectKeyDAL.create( @@ -195,10 +206,11 @@ export async function seed(knex: Knex): Promise { }) .returning("*"); - const userOrgMembership = await knex(TableName.OrgMembership) + const userOrgMembership = await knex(TableName.Membership) .where({ - orgId: seedData1.organization.id, - userId: seedData1.id + scopeOrgId: seedData1.organization.id, + actorUserId: seedData1.id, + scope: AccessScope.Organization }) .first(); diff --git a/backend/src/db/seeds/4-project-v3.ts b/backend/src/db/seeds/4-project-v3.ts index f89b965a6..920497a82 100644 --- a/backend/src/db/seeds/4-project-v3.ts +++ b/backend/src/db/seeds/4-project-v3.ts @@ -1,6 +1,6 @@ import { Knex } from "knex"; -import { ProjectMembershipRole, ProjectType, ProjectVersion, TableName } from "../schemas"; +import { AccessScope, ProjectMembershipRole, ProjectType, ProjectVersion, TableName } from "../schemas"; import { seedData1 } from "../seed-data"; export const DEFAULT_PROJECT_ENVS = [ @@ -23,15 +23,17 @@ export async function seed(knex: Knex): Promise { }) .returning("*"); - const projectMembershipV3 = await knex(TableName.ProjectMembership) + const projectMembershipV3 = await knex(TableName.Membership) .insert({ - projectId: projectV2.id, - userId: seedData1.id + scopeProjectId: projectV2.id, + actorUserId: seedData1.id, + scope: AccessScope.Project, + scopeOrgId: seedData1.organization.id }) .returning("*"); - await knex(TableName.ProjectUserMembershipRole).insert({ + await knex(TableName.MembershipRole).insert({ role: ProjectMembershipRole.Admin, - projectMembershipId: projectMembershipV3[0].id + membershipId: projectMembershipV3[0].id }); // create default environments and default folders diff --git a/backend/src/db/seeds/5-machine-identity.ts b/backend/src/db/seeds/5-machine-identity.ts index ae3d04514..333fc7e3a 100644 --- a/backend/src/db/seeds/5-machine-identity.ts +++ b/backend/src/db/seeds/5-machine-identity.ts @@ -5,13 +5,12 @@ import { crypto } from "@app/lib/crypto/cryptography"; import { initLogger, logger } from "@app/lib/logger"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; -import { IdentityAuthMethod, OrgMembershipRole, ProjectMembershipRole, TableName } from "../schemas"; +import { AccessScope, IdentityAuthMethod, OrgMembershipRole, ProjectMembershipRole, TableName } from "../schemas"; import { seedData1 } from "../seed-data"; export async function seed(knex: Knex): Promise { // Deletes ALL existing entries await knex(TableName.Identity).del(); - await knex(TableName.IdentityOrgMembership).del(); initLogger(); @@ -78,34 +77,47 @@ export async function seed(knex: Knex): Promise { isClientSecretRevoked: false } ]); - await knex(TableName.IdentityOrgMembership).insert([ + const [orgMembership] = await knex(TableName.Membership) + .insert([ + { + actorIdentityId: seedData1.machineIdentity.id, + scopeOrgId: seedData1.organization.id, + scope: AccessScope.Organization + } + ]) + .returning("*"); + await knex(TableName.MembershipRole).insert([ { - identityId: seedData1.machineIdentity.id, - orgId: seedData1.organization.id, + membershipId: orgMembership.id, role: OrgMembershipRole.Admin } ]); - const identityProjectMembership = await knex(TableName.IdentityProjectMembership) + const identityProjectMembership = await knex(TableName.Membership) .insert({ - identityId: seedData1.machineIdentity.id, - projectId: seedData1.project.id + actorIdentityId: seedData1.machineIdentity.id, + scopeOrgId: seedData1.organization.id, + scope: AccessScope.Project, + scopeProjectId: seedData1.project.id }) .returning("*"); - await knex(TableName.IdentityProjectMembershipRole).insert({ + await knex(TableName.MembershipRole).insert({ role: ProjectMembershipRole.Admin, - projectMembershipId: identityProjectMembership[0].id + membershipId: identityProjectMembership[0].id }); - const identityProjectMembershipV3 = await knex(TableName.IdentityProjectMembership) + + const identityProjectMembershipV3 = await knex(TableName.Membership) .insert({ - identityId: seedData1.machineIdentity.id, - projectId: seedData1.projectV3.id + actorIdentityId: seedData1.machineIdentity.id, + scopeOrgId: seedData1.organization.id, + scope: AccessScope.Project, + scopeProjectId: seedData1.projectV3.id }) .returning("*"); - await knex(TableName.IdentityProjectMembershipRole).insert({ + await knex(TableName.MembershipRole).insert({ role: ProjectMembershipRole.Admin, - projectMembershipId: identityProjectMembershipV3[0].id + membershipId: identityProjectMembershipV3[0].id }); } diff --git a/backend/src/ee/services/github-org-sync/github-org-sync-service.ts b/backend/src/ee/services/github-org-sync/github-org-sync-service.ts index 7c4ad15eb..47bdd96ea 100644 --- a/backend/src/ee/services/github-org-sync/github-org-sync-service.ts +++ b/backend/src/ee/services/github-org-sync/github-org-sync-service.ts @@ -78,10 +78,7 @@ type TGithubOrgSyncServiceFactoryDep = { >; groupDAL: Pick; licenseService: Pick; - orgMembershipDAL: Pick< - TOrgMembershipDALFactory, - "find" | "findOrgMembershipById" | "findOrgMembershipsWithUsersByOrgId" - >; + orgMembershipDAL: Pick; }; export type TGithubOrgSyncServiceFactory = ReturnType; diff --git a/backend/src/ee/services/group/group-dal.ts b/backend/src/ee/services/group/group-dal.ts index d3d332ffc..6e00e292b 100644 --- a/backend/src/ee/services/group/group-dal.ts +++ b/backend/src/ee/services/group/group-dal.ts @@ -195,12 +195,36 @@ export const groupDALFactory = (db: TDbClient) => { } }; + const findOne = async (filter: Partial, tx?: Knex): Promise => { + try { + const doc = await (tx || db.replicaNode())(TableName.Groups) + .join(TableName.Membership, `${TableName.Membership}.actorGroupId`, `${TableName.Groups}.id`) + .join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .where((queryBuilder) => { + Object.entries(filter).forEach(([key, value]) => { + void queryBuilder.where(`${TableName.Membership}.${key}`, value); + }); + }) + .select( + selectAllTableCols(TableName.Groups), + db.ref("role").withSchema(TableName.MembershipRole), + db.ref("customRoleId").as("roleId").withSchema(TableName.MembershipRole) + ) + .first(); + return doc; + } catch (error) { + throw new DatabaseError({ error, name: "Find one" }); + } + }; + return { ...groupOrm, findGroups, findByOrgId, findAllGroupPossibleMembers, findGroupsByProjectId, - findById + findById, + findOne }; }; diff --git a/backend/src/ee/services/group/group-fns.ts b/backend/src/ee/services/group/group-fns.ts index 56f8df6c0..c4384cc4e 100644 --- a/backend/src/ee/services/group/group-fns.ts +++ b/backend/src/ee/services/group/group-fns.ts @@ -1,6 +1,6 @@ import { Knex } from "knex"; -import { ProjectVersion, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas"; +import { AccessScope, ProjectVersion, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, ForbiddenRequestError, NotFoundError, ScimRequestError } from "@app/lib/errors"; @@ -16,7 +16,7 @@ const addAcceptedUsersToGroup = async ({ group, userGroupMembershipDAL, userDAL, - groupProjectDAL, + membershipGroupDAL, projectKeyDAL, projectDAL, projectBotDAL, @@ -42,13 +42,15 @@ const addAcceptedUsersToGroup = async ({ const projectIds = Array.from( new Set( ( - await groupProjectDAL.find( + await membershipGroupDAL.find( { - groupId: group.id + actorGroupId: group.id, + scopeOrgId: group.orgId, + scope: AccessScope.Project }, { tx } ) - ).map((gp) => gp.projectId) + ).map((gp) => gp.scopeProjectId as string) ) ); @@ -167,11 +169,11 @@ export const addUsersToGroupByUserIds = async ({ userDAL, userGroupMembershipDAL, orgDAL, - groupProjectDAL, projectKeyDAL, projectDAL, projectBotDAL, - tx: outerTx + tx: outerTx, + membershipGroupDAL }: TAddUsersToGroupByUserIds) => { const processAddition = async (tx: Knex) => { const foundMembers = await userDAL.find( @@ -214,15 +216,18 @@ export const addUsersToGroupByUserIds = async ({ // check if all user(s) are part of the organization const existingUserOrgMemberships = await orgDAL.findMembership( { - [`${TableName.OrgMembership}.orgId` as "orgId"]: group.orgId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: group.orgId, + scope: AccessScope.Organization, $in: { - [`${TableName.OrgMembership}.userId` as "userId"]: userIds + [`${TableName.Membership}.actorUserId` as "actorUserId"]: userIds } }, { tx } ); - const existingUserOrgMembershipsUserIdsSet = new Set(existingUserOrgMemberships.map((u) => u.userId)); + const existingUserOrgMembershipsUserIdsSet = new Set( + existingUserOrgMemberships.map((u) => u.actorUserId as string) + ); userIds.forEach((userId) => { if (!existingUserOrgMembershipsUserIdsSet.has(userId)) @@ -250,7 +255,7 @@ export const addUsersToGroupByUserIds = async ({ group, userDAL, userGroupMembershipDAL, - groupProjectDAL, + membershipGroupDAL, projectKeyDAL, projectDAL, projectBotDAL, @@ -292,9 +297,9 @@ export const removeUsersFromGroupByUserIds = async ({ userIds, userDAL, userGroupMembershipDAL, - groupProjectDAL, projectKeyDAL, - tx: outerTx + tx: outerTx, + membershipGroupDAL }: TRemoveUsersFromGroupByUserIds) => { const processRemoval = async (tx: Knex) => { const foundMembers = await userDAL.find({ @@ -352,13 +357,15 @@ export const removeUsersFromGroupByUserIds = async ({ const projectIds = Array.from( new Set( ( - await groupProjectDAL.find( + await membershipGroupDAL.find( { - groupId: group.id + scope: AccessScope.Project, + actorGroupId: group.id, + scopeOrgId: group.orgId }, { tx } ) - ).map((gp) => gp.projectId) + ).map((gp) => gp.scopeProjectId as string) ) ); @@ -422,11 +429,11 @@ export const convertPendingGroupAdditionsToGroupMemberships = async ({ userIds, userDAL, userGroupMembershipDAL, - groupProjectDAL, projectKeyDAL, projectDAL, projectBotDAL, - tx: outerTx + tx: outerTx, + membershipGroupDAL }: TConvertPendingGroupAdditionsToGroupMemberships) => { const processConversion = async (tx: Knex) => { const users = await userDAL.find( @@ -463,7 +470,7 @@ export const convertPendingGroupAdditionsToGroupMemberships = async ({ group: pendingGroupAddition.group, userDAL, userGroupMembershipDAL, - groupProjectDAL, + membershipGroupDAL, projectKeyDAL, projectDAL, projectBotDAL, diff --git a/backend/src/ee/services/group/group-service.ts b/backend/src/ee/services/group/group-service.ts index 7bb5be866..c8bde0641 100644 --- a/backend/src/ee/services/group/group-service.ts +++ b/backend/src/ee/services/group/group-service.ts @@ -1,11 +1,12 @@ import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; -import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas"; +import { AccessScope, OrgMembershipRole, TRoles } from "@app/db/schemas"; import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; -import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; +import { TMembershipRoleDALFactory } from "@app/services/membership/membership-role-dal"; +import { TMembershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; @@ -35,8 +36,9 @@ type TGroupServiceFactoryDep = { TGroupDALFactory, "create" | "findOne" | "update" | "delete" | "findAllGroupPossibleMembers" | "findById" | "transaction" >; - groupProjectDAL: Pick; - orgDAL: Pick; + membershipGroupDAL: Pick; + membershipRoleDAL: Pick; + orgDAL: Pick; userGroupMembershipDAL: Pick< TUserGroupMembershipDALFactory, "findOne" | "delete" | "filterProjectsByUserMembership" | "transaction" | "insertMany" | "find" @@ -54,7 +56,6 @@ export type TGroupServiceFactory = ReturnType; export const groupServiceFactory = ({ userDAL, groupDAL, - groupProjectDAL, orgDAL, userGroupMembershipDAL, projectDAL, @@ -62,12 +63,14 @@ export const groupServiceFactory = ({ projectKeyDAL, permissionService, licenseService, - oidcConfigDAL + oidcConfigDAL, + membershipGroupDAL, + membershipRoleDAL }: TGroupServiceFactoryDep) => { const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, @@ -82,25 +85,23 @@ export const groupServiceFactory = ({ message: "Failed to create group due to plan restriction. Upgrade plan to create group." }); - const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole( - role, - actorOrgId - ); - const isCustomRole = Boolean(customRole); + const [rolePermissionDetails] = await permissionService.getOrgPermissionByRoles([role], actorOrgId); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(actorOrgId); + const isCustomRole = Boolean(rolePermissionDetails?.role); if (role !== OrgMembershipRole.NoAccess) { const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionGroupActions.GrantPrivileges, OrgPermissionSubjects.Groups, permission, - rolePermission + rolePermissionDetails.permission ); if (!permissionBoundary.isValid) throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to create group", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionGroupActions.GrantPrivileges, OrgPermissionSubjects.Groups ), @@ -122,7 +123,25 @@ export const groupServiceFactory = ({ slug: slug || slugify(`${name}-${alphaNumericNanoId(4)}`), orgId: actorOrgId, role: isCustomRole ? OrgMembershipRole.Custom : role, - roleId: customRole?.id + roleId: rolePermissionDetails?.role?.id + }, + tx + ); + + const membership = await membershipGroupDAL.create( + { + actorGroupId: newGroup.id, + scope: AccessScope.Organization, + scopeOrgId: actorOrgId + }, + tx + ); + + await membershipRoleDAL.create( + { + membershipId: membership.id, + role: isCustomRole ? OrgMembershipRole.Custom : role, + customRoleId: rolePermissionDetails?.role?.id }, tx ); @@ -145,7 +164,7 @@ export const groupServiceFactory = ({ }: TUpdateGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, @@ -166,32 +185,31 @@ export const groupServiceFactory = ({ throw new NotFoundError({ message: `Failed to find group with ID ${id}` }); } - let customRole: TOrgRoles | undefined; + let customRole: TRoles | undefined; if (role) { - const { permission: rolePermission, role: customOrgRole } = await permissionService.getOrgPermissionByRole( - role, - group.orgId - ); + const [rolePermissionDetails] = await permissionService.getOrgPermissionByRoles([role], group.orgId); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(actorOrgId); + const isCustomRole = Boolean(rolePermissionDetails?.role); - const isCustomRole = Boolean(customOrgRole); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionGroupActions.GrantPrivileges, OrgPermissionSubjects.Groups, permission, - rolePermission + rolePermissionDetails.permission ); if (!permissionBoundary.isValid) throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to update group", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionGroupActions.GrantPrivileges, OrgPermissionSubjects.Groups ), details: { missingPermissions: permissionBoundary.missingPermissions } }); - if (isCustomRole) customRole = customOrgRole; + if (isCustomRole) customRole = rolePermissionDetails?.role; } const updatedGroup = await groupDAL.transaction(async (tx) => { @@ -211,17 +229,31 @@ export const groupServiceFactory = ({ }, { name, - slug: slug ? slugify(slug) : undefined, - ...(role - ? { - role: customRole ? OrgMembershipRole.Custom : role, - roleId: customRole?.id ?? null - } - : {}) + slug: slug ? slugify(slug) : undefined }, tx ); + if (role) { + const membership = await membershipGroupDAL.findOne( + { + scope: AccessScope.Organization, + actorGroupId: updated.id, + scopeOrgId: updated.orgId + }, + tx + ); + await membershipRoleDAL.delete({ membershipId: membership.id }, tx); + await membershipRoleDAL.create( + { + membershipId: membership.id, + role: customRole ? OrgMembershipRole.Custom : role, + customRoleId: customRole?.id ?? null + }, + tx + ); + } + return updated; }); @@ -341,7 +373,7 @@ export const groupServiceFactory = ({ const addUserToGroup = async ({ id, username, actor, actorId, actorAuthMethod, actorOrgId }: TAddUserToGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, @@ -373,22 +405,23 @@ export const groupServiceFactory = ({ }); } - const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId); + const [rolePermissionDetails] = await permissionService.getOrgPermissionByRoles([group.role], actorOrgId); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(actorOrgId); // check if user has broader or equal to privileges than group const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionGroupActions.AddMembers, OrgPermissionSubjects.Groups, permission, - groupRolePermission + rolePermissionDetails.permission ); if (!permissionBoundary.isValid) throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to add user to more privileged group", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionGroupActions.AddMembers, OrgPermissionSubjects.Groups ), @@ -407,7 +440,7 @@ export const groupServiceFactory = ({ userDAL, userGroupMembershipDAL, orgDAL, - groupProjectDAL, + membershipGroupDAL, projectKeyDAL, projectDAL, projectBotDAL @@ -431,7 +464,7 @@ export const groupServiceFactory = ({ }: TRemoveUserFromGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, @@ -463,21 +496,22 @@ export const groupServiceFactory = ({ }); } - const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId); + const [rolePermissionDetails] = await permissionService.getOrgPermissionByRoles([group.role], actorOrgId); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(actorOrgId); // check if user has broader or equal to privileges than group const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionGroupActions.RemoveMembers, OrgPermissionSubjects.Groups, permission, - groupRolePermission + rolePermissionDetails.permission ); if (!permissionBoundary.isValid) throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to delete user from more privileged group", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionGroupActions.RemoveMembers, OrgPermissionSubjects.Groups ), @@ -495,7 +529,7 @@ export const groupServiceFactory = ({ userIds: [user.id], userDAL, userGroupMembershipDAL, - groupProjectDAL, + membershipGroupDAL, projectKeyDAL }); diff --git a/backend/src/ee/services/group/group-types.ts b/backend/src/ee/services/group/group-types.ts index e91f7a47b..4b0742201 100644 --- a/backend/src/ee/services/group/group-types.ts +++ b/backend/src/ee/services/group/group-types.ts @@ -3,7 +3,7 @@ import { Knex } from "knex"; import { TGroups } from "@app/db/schemas"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TGenericPermission } from "@app/lib/types"; -import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; +import { TMembershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; @@ -63,7 +63,7 @@ export type TAddUsersToGroup = { group: TGroups; userDAL: Pick; userGroupMembershipDAL: Pick; - groupProjectDAL: Pick; + membershipGroupDAL: Pick; projectKeyDAL: Pick; projectDAL: Pick; projectBotDAL: Pick; @@ -76,7 +76,7 @@ export type TAddUsersToGroupByUserIds = { userDAL: Pick; userGroupMembershipDAL: Pick; orgDAL: Pick; - groupProjectDAL: Pick; + membershipGroupDAL: Pick; projectKeyDAL: Pick; projectDAL: Pick; projectBotDAL: Pick; @@ -88,7 +88,7 @@ export type TRemoveUsersFromGroupByUserIds = { userIds: string[]; userDAL: Pick; userGroupMembershipDAL: Pick; - groupProjectDAL: Pick; + membershipGroupDAL: Pick; projectKeyDAL: Pick; tx?: Knex; }; @@ -100,7 +100,7 @@ export type TConvertPendingGroupAdditionsToGroupMemberships = { TUserGroupMembershipDALFactory, "find" | "transaction" | "insertMany" | "deletePendingUserGroupMembershipsByUserIds" >; - groupProjectDAL: Pick; + membershipGroupDAL: Pick; projectKeyDAL: Pick; projectDAL: Pick; projectBotDAL: Pick; diff --git a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-dal.ts b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-dal.ts deleted file mode 100644 index a7d8794a4..000000000 --- a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-dal.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; - -export type TIdentityProjectAdditionalPrivilegeV2DALFactory = ReturnType< - typeof identityProjectAdditionalPrivilegeV2DALFactory ->; - -export const identityProjectAdditionalPrivilegeV2DALFactory = (db: TDbClient) => { - const orm = ormify(db, TableName.IdentityProjectAdditionalPrivilege); - return orm; -}; diff --git a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts deleted file mode 100644 index 27b67367e..000000000 --- a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts +++ /dev/null @@ -1,434 +0,0 @@ -import { ForbiddenError, subject } from "@casl/ability"; -import { packRules } from "@casl/ability/extra"; - -import { ActionProjectType, TableName } from "@app/db/schemas"; -import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; -import { ms } from "@app/lib/ms"; -import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; -import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission"; -import { ActorType } from "@app/services/auth/auth-type"; -import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; -import { TProjectDALFactory } from "@app/services/project/project-dal"; - -import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns"; -import { TPermissionServiceFactory } from "../permission/permission-service-types"; -import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "../permission/project-permission"; -import { TIdentityProjectAdditionalPrivilegeV2DALFactory } from "./identity-project-additional-privilege-v2-dal"; -import { - IdentityProjectAdditionalPrivilegeTemporaryMode, - TCreateIdentityPrivilegeDTO, - TDeleteIdentityPrivilegeByIdDTO, - TGetIdentityPrivilegeDetailsByIdDTO, - TGetIdentityPrivilegeDetailsBySlugDTO, - TListIdentityPrivilegesDTO, - TUpdateIdentityPrivilegeByIdDTO -} from "./identity-project-additional-privilege-v2-types"; - -type TIdentityProjectAdditionalPrivilegeV2ServiceFactoryDep = { - identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeV2DALFactory; - identityProjectDAL: Pick; - projectDAL: Pick; - permissionService: Pick; -}; - -export type TIdentityProjectAdditionalPrivilegeV2ServiceFactory = ReturnType< - typeof identityProjectAdditionalPrivilegeV2ServiceFactory ->; - -export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ - identityProjectAdditionalPrivilegeDAL, - identityProjectDAL, - projectDAL, - permissionService -}: TIdentityProjectAdditionalPrivilegeV2ServiceFactoryDep) => { - const create = async ({ - slug, - actor, - actorId, - projectId, - actorOrgId, - identityId, - permissions: customPermission, - actorAuthMethod, - ...dto - }: TCreateIdentityPrivilegeDTO) => { - const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); - if (!identityProjectMembership) - throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Edit, - subject(ProjectPermissionSub.Identity, { identityId }) - ); - const { permission: targetIdentityPermission, membership } = await permissionService.getProjectPermission({ - actor: ActorType.IDENTITY, - actorId: identityId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - - // we need to validate that the privilege given is not higher than the assigning users permission - // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules - targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission)); - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity, - permission, - targetIdentityPermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to update more privileged identity", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - validateHandlebarTemplate("Identity Additional Privilege Create", JSON.stringify(customPermission || []), { - allowedExpressions: (val) => val.includes("identity.") - }); - - const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ - slug, - projectMembershipId: identityProjectMembership.id - }); - if (existingSlug) throw new BadRequestError({ message: "Additional privilege with provided slug already exists" }); - - const packedPermission = JSON.stringify(packRules(customPermission)); - if (!dto.isTemporary) { - const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({ - projectMembershipId: identityProjectMembership.id, - slug, - permissions: packedPermission - }); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - } - - const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange); - const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({ - projectMembershipId: identityProjectMembership.id, - slug, - permissions: packedPermission, - isTemporary: true, - temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative, - temporaryRange: dto.temporaryRange, - temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), - temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) - }); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - }; - - const updateById = async ({ - id, - data, - actorOrgId, - actor, - actorId, - actorAuthMethod - }: TUpdateIdentityPrivilegeByIdDTO) => { - const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findById(id); - if (!identityPrivilege) throw new NotFoundError({ message: `Identity privilege with ${id} not found` }); - - const identityProjectMembership = await identityProjectDAL.findOne({ id: identityPrivilege.projectMembershipId }); - if (!identityProjectMembership) - throw new NotFoundError({ - message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}` - }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Edit, - subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) - ); - const { permission: targetIdentityPermission, membership } = await permissionService.getProjectPermission({ - actor: ActorType.IDENTITY, - actorId: identityProjectMembership.identityId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - - // we need to validate that the privilege given is not higher than the assigning users permission - // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules - targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || [])); - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity, - permission, - targetIdentityPermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to update more privileged identity", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - - validateHandlebarTemplate("Identity Additional Privilege Update", JSON.stringify(data.permissions || []), { - allowedExpressions: (val) => val.includes("identity.") - }); - - if (data?.slug) { - const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ - slug: data.slug, - projectMembershipId: identityProjectMembership.id - }); - if (existingSlug && existingSlug.id !== identityPrivilege.id) - throw new BadRequestError({ message: "Additional privilege with provided slug already exists" }); - } - - const isTemporary = typeof data?.isTemporary !== "undefined" ? data.isTemporary : identityPrivilege.isTemporary; - const packedPermission = data.permissions ? JSON.stringify(packRules(data.permissions)) : undefined; - if (isTemporary) { - const temporaryAccessStartTime = data?.temporaryAccessStartTime || identityPrivilege?.temporaryAccessStartTime; - const temporaryRange = data?.temporaryRange || identityPrivilege?.temporaryRange; - const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, { - slug: data.slug, - permissions: packedPermission, - isTemporary: data.isTemporary, - temporaryRange: data.temporaryRange, - temporaryMode: data.temporaryMode, - temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), - temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) - }); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - } - - const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, { - slug: data.slug, - permissions: packedPermission, - isTemporary: false, - temporaryAccessStartTime: null, - temporaryAccessEndTime: null, - temporaryRange: null, - temporaryMode: null - }); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - }; - - const deleteById = async ({ actorId, id, actor, actorOrgId, actorAuthMethod }: TDeleteIdentityPrivilegeByIdDTO) => { - const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findById(id); - if (!identityPrivilege) throw new NotFoundError({ message: `Identity privilege with ${id} not found` }); - - const identityProjectMembership = await identityProjectDAL.findOne({ id: identityPrivilege.projectMembershipId }); - if (!identityProjectMembership) - throw new NotFoundError({ - message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}` - }); - - const { permission, membership } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Edit, - subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) - ); - const { permission: identityRolePermission } = await permissionService.getProjectPermission({ - actor: ActorType.IDENTITY, - actorId: identityProjectMembership.identityId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity, - permission, - identityRolePermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to update more privileged identity", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - - const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...deletedPrivilege, - permissions: unpackPermissions(deletedPrivilege.permissions) - }; - }; - - const getPrivilegeDetailsById = async ({ - id, - actorOrgId, - actor, - actorId, - actorAuthMethod - }: TGetIdentityPrivilegeDetailsByIdDTO) => { - const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findById(id); - if (!identityPrivilege) throw new NotFoundError({ message: `Identity privilege with ${id} not found` }); - - const identityProjectMembership = await identityProjectDAL.findOne({ id: identityPrivilege.projectMembershipId }); - if (!identityProjectMembership) - throw new NotFoundError({ - message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}` - }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Read, - subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) - ); - - return { - ...identityPrivilege, - permissions: unpackPermissions(identityPrivilege.permissions) - }; - }; - - const getPrivilegeDetailsBySlug = async ({ - identityId, - slug, - projectSlug, - actorOrgId, - actor, - actorId, - actorAuthMethod - }: TGetIdentityPrivilegeDetailsBySlugDTO) => { - const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); - if (!project) throw new NotFoundError({ message: `Project with slug ${slug} not found` }); - const projectId = project.id; - - const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); - if (!identityProjectMembership) - throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Read, - subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) - ); - - const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ - slug, - projectMembershipId: identityProjectMembership.id - }); - if (!identityPrivilege) throw new NotFoundError({ message: "Identity additional privilege not found" }); - - return { - ...identityPrivilege, - permissions: unpackPermissions(identityPrivilege.permissions) - }; - }; - - const listIdentityProjectPrivileges = async ({ - identityId, - actorOrgId, - actor, - actorId, - actorAuthMethod, - projectId - }: TListIdentityPrivilegesDTO) => { - const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); - if (!identityProjectMembership) - throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Read, - subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) - ); - - const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find( - { - projectMembershipId: identityProjectMembership.id - }, - { sort: [[`${TableName.IdentityProjectAdditionalPrivilege}.slug` as "slug", "asc"]] } - ); - return identityPrivileges; - }; - - return { - getPrivilegeDetailsById, - getPrivilegeDetailsBySlug, - listIdentityProjectPrivileges, - create, - updateById, - deleteById - }; -}; diff --git a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types.ts b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types.ts deleted file mode 100644 index aab6b8510..000000000 --- a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { TProjectPermission } from "@app/lib/types"; - -import { TProjectPermissionV2Schema } from "../permission/project-permission"; - -export enum IdentityProjectAdditionalPrivilegeTemporaryMode { - Relative = "relative" -} - -export type TCreateIdentityPrivilegeDTO = { - permissions: TProjectPermissionV2Schema[]; - identityId: string; - projectId: string; - slug: string; -} & ( - | { - isTemporary: false; - } - | { - isTemporary: true; - temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative; - temporaryRange: string; - temporaryAccessStartTime: string; - } -) & - Omit; - -export type TUpdateIdentityPrivilegeByIdDTO = { id: string } & Omit & { - data: Partial<{ - permissions: TProjectPermissionV2Schema[]; - slug: string; - isTemporary: boolean; - temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative; - temporaryRange: string; - temporaryAccessStartTime: string; - }>; - }; - -export type TDeleteIdentityPrivilegeByIdDTO = Omit & { - id: string; -}; - -export type TGetIdentityPrivilegeDetailsByIdDTO = Omit & { - id: string; -}; - -export type TListIdentityPrivilegesDTO = Omit & { - identityId: string; - projectId: string; -}; - -export type TGetIdentityPrivilegeDetailsBySlugDTO = Omit & { - slug: string; - identityId: string; - projectSlug: string; -}; diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal.ts deleted file mode 100644 index 26252f2d1..000000000 --- a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; - -export type TIdentityProjectAdditionalPrivilegeDALFactory = ReturnType< - typeof identityProjectAdditionalPrivilegeDALFactory ->; - -export const identityProjectAdditionalPrivilegeDALFactory = (db: TDbClient) => { - const orm = ormify(db, TableName.IdentityProjectAdditionalPrivilege); - return orm; -}; diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts deleted file mode 100644 index ddba76920..000000000 --- a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts +++ /dev/null @@ -1,451 +0,0 @@ -import { ForbiddenError, MongoAbility, RawRuleOf, subject } from "@casl/ability"; -import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; - -import { ActionProjectType } from "@app/db/schemas"; -import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; -import { ms } from "@app/lib/ms"; -import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; -import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; -import { ActorType } from "@app/services/auth/auth-type"; -import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; -import { TProjectDALFactory } from "@app/services/project/project-dal"; - -import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns"; -import { TPermissionServiceFactory } from "../permission/permission-service-types"; -import { - ProjectPermissionIdentityActions, - ProjectPermissionSet, - ProjectPermissionSub -} from "../permission/project-permission"; -import { TIdentityProjectAdditionalPrivilegeDALFactory } from "./identity-project-additional-privilege-dal"; -import { - IdentityProjectAdditionalPrivilegeTemporaryMode, - TCreateIdentityPrivilegeDTO, - TDeleteIdentityPrivilegeDTO, - TGetIdentityPrivilegeDetailsDTO, - TListIdentityPrivilegesDTO, - TUpdateIdentityPrivilegeDTO -} from "./identity-project-additional-privilege-types"; - -type TIdentityProjectAdditionalPrivilegeServiceFactoryDep = { - identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeDALFactory; - identityProjectDAL: Pick; - projectDAL: Pick; - permissionService: Pick; -}; - -export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType< - typeof identityProjectAdditionalPrivilegeServiceFactory ->; - -const unpackPermissions = (permissions: unknown) => - UnpackedPermissionSchema.array().parse( - unpackRules((permissions || []) as PackRule>>[]) - ); - -export const identityProjectAdditionalPrivilegeServiceFactory = ({ - identityProjectAdditionalPrivilegeDAL, - identityProjectDAL, - permissionService, - projectDAL -}: TIdentityProjectAdditionalPrivilegeServiceFactoryDep) => { - const create = async ({ - slug, - actor, - actorId, - identityId, - projectSlug, - permissions: customPermission, - actorOrgId, - actorAuthMethod, - ...dto - }: TCreateIdentityPrivilegeDTO) => { - const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); - if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); - const projectId = project.id; - - const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); - if (!identityProjectMembership) - throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - - const { permission, membership } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Edit, - subject(ProjectPermissionSub.Identity, { identityId }) - ); - - const { permission: targetIdentityPermission } = await permissionService.getProjectPermission({ - actor: ActorType.IDENTITY, - actorId: identityId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - - // we need to validate that the privilege given is not higher than the assigning users permission - // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules - targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission)); - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity, - permission, - targetIdentityPermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to update more privileged identity", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - - const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ - slug, - projectMembershipId: identityProjectMembership.id - }); - if (existingSlug) throw new BadRequestError({ message: "Additional privilege of provided slug exist" }); - - validateHandlebarTemplate("Identity Additional Privilege Create", JSON.stringify(customPermission || []), { - allowedExpressions: (val) => val.includes("identity.") - }); - - const packedPermission = JSON.stringify(packRules(customPermission)); - if (!dto.isTemporary) { - const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({ - projectMembershipId: identityProjectMembership.id, - slug, - permissions: packedPermission - }); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - } - - const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange); - const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({ - projectMembershipId: identityProjectMembership.id, - slug, - permissions: packedPermission, - isTemporary: true, - temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative, - temporaryRange: dto.temporaryRange, - temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), - temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) - }); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - }; - - const updateBySlug = async ({ - projectSlug, - slug, - identityId, - data, - actorOrgId, - actor, - actorId, - actorAuthMethod - }: TUpdateIdentityPrivilegeDTO) => { - const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); - if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); - const projectId = project.id; - - const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); - if (!identityProjectMembership) - throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - - const { permission, membership } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Edit, - subject(ProjectPermissionSub.Identity, { identityId }) - ); - - const { permission: targetIdentityPermission } = await permissionService.getProjectPermission({ - actor: ActorType.IDENTITY, - actorId: identityProjectMembership.identityId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - - // we need to validate that the privilege given is not higher than the assigning users permission - // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules - targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || [])); - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity, - permission, - targetIdentityPermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to update more privileged identity", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - - const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ - slug, - projectMembershipId: identityProjectMembership.id - }); - if (!identityPrivilege) { - throw new NotFoundError({ - message: `Identity additional privilege with slug '${slug}' not found for the specified identity with ID '${identityProjectMembership.identityId}'` - }); - } - if (data?.slug) { - const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ - slug: data.slug, - projectMembershipId: identityProjectMembership.id - }); - if (existingSlug && existingSlug.id !== identityPrivilege.id) - throw new BadRequestError({ message: "Additional privilege of provided slug exist" }); - } - - const isTemporary = typeof data?.isTemporary !== "undefined" ? data.isTemporary : identityPrivilege.isTemporary; - validateHandlebarTemplate("Identity Additional Privilege Update", JSON.stringify(data.permissions || []), { - allowedExpressions: (val) => val.includes("identity.") - }); - - const packedPermission = data.permissions ? JSON.stringify(packRules(data.permissions)) : undefined; - if (isTemporary) { - const temporaryAccessStartTime = data?.temporaryAccessStartTime || identityPrivilege?.temporaryAccessStartTime; - const temporaryRange = data?.temporaryRange || identityPrivilege?.temporaryRange; - const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, { - slug: data.slug, - permissions: packedPermission, - isTemporary: data.isTemporary, - temporaryRange: data.temporaryRange, - temporaryMode: data.temporaryMode, - temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), - temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) - }); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - } - - const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, { - slug: data.slug, - permissions: packedPermission, - isTemporary: false, - temporaryAccessStartTime: null, - temporaryAccessEndTime: null, - temporaryRange: null, - temporaryMode: null - }); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - }; - - const deleteBySlug = async ({ - actorId, - slug, - identityId, - projectSlug, - actor, - actorOrgId, - actorAuthMethod - }: TDeleteIdentityPrivilegeDTO) => { - const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); - if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); - const projectId = project.id; - - const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); - if (!identityProjectMembership) - throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - - const { permission, membership } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Edit, - subject(ProjectPermissionSub.Identity, { identityId }) - ); - - const { permission: identityRolePermission } = await permissionService.getProjectPermission({ - actor: ActorType.IDENTITY, - actorId: identityProjectMembership.identityId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity, - permission, - identityRolePermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to edit more privileged identity", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - - const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ - slug, - projectMembershipId: identityProjectMembership.id - }); - if (!identityPrivilege) { - throw new NotFoundError({ - message: `Identity additional privilege with slug '${slug}' not found for the specified identity with ID '${identityProjectMembership.identityId}'` - }); - } - - const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...deletedPrivilege, - permissions: unpackPermissions(deletedPrivilege.permissions) - }; - }; - - const getPrivilegeDetailsBySlug = async ({ - projectSlug, - identityId, - slug, - actorOrgId, - actor, - actorId, - actorAuthMethod - }: TGetIdentityPrivilegeDetailsDTO) => { - const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); - if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); - const projectId = project.id; - - const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); - if (!identityProjectMembership) - throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Read, - subject(ProjectPermissionSub.Identity, { identityId }) - ); - - const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ - slug, - projectMembershipId: identityProjectMembership.id - }); - if (!identityPrivilege) { - throw new NotFoundError({ - message: `Identity additional privilege with slug '${slug}' not found for the specified identity with ID '${identityProjectMembership.identityId}'` - }); - } - return { - ...identityPrivilege, - permissions: unpackPermissions(identityPrivilege.permissions) - }; - }; - - const listIdentityProjectPrivileges = async ({ - identityId, - actorOrgId, - actor, - actorId, - actorAuthMethod, - projectSlug - }: TListIdentityPrivilegesDTO) => { - const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); - if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); - const projectId = project.id; - - const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); - if (!identityProjectMembership) - throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Read, - subject(ProjectPermissionSub.Identity, { identityId }) - ); - - const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find({ - projectMembershipId: identityProjectMembership.id - }); - return identityPrivileges.map((el) => ({ - ...el, - permissions: unpackPermissions(el.permissions) - })); - }; - - return { - create, - updateBySlug, - deleteBySlug, - getPrivilegeDetailsBySlug, - listIdentityProjectPrivileges - }; -}; diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types.ts deleted file mode 100644 index 6a0ecee5f..000000000 --- a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { TProjectPermission } from "@app/lib/types"; - -import { TProjectPermissionV2Schema } from "../permission/project-permission"; - -export enum IdentityProjectAdditionalPrivilegeTemporaryMode { - Relative = "relative" -} - -export type TCreateIdentityPrivilegeDTO = { - permissions: TProjectPermissionV2Schema[]; - identityId: string; - projectSlug: string; - slug: string; -} & ( - | { - isTemporary: false; - } - | { - isTemporary: true; - temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative; - temporaryRange: string; - temporaryAccessStartTime: string; - } -) & - Omit; - -export type TUpdateIdentityPrivilegeDTO = { slug: string; identityId: string; projectSlug: string } & Omit< - TProjectPermission, - "projectId" -> & { - data: Partial<{ - permissions: TProjectPermissionV2Schema[]; - slug: string; - isTemporary: boolean; - temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative; - temporaryRange: string; - temporaryAccessStartTime: string; - }>; - }; - -export type TDeleteIdentityPrivilegeDTO = Omit & { - slug: string; - identityId: string; - projectSlug: string; -}; - -export type TGetIdentityPrivilegeDetailsDTO = Omit & { - slug: string; - identityId: string; - projectSlug: string; -}; - -export type TListIdentityPrivilegesDTO = Omit & { - identityId: string; - projectSlug: string; -}; diff --git a/backend/src/ee/services/ldap-config/ldap-config-service.ts b/backend/src/ee/services/ldap-config/ldap-config-service.ts index 8643ecdac..4649d1dd4 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { Knex } from "knex"; -import { OrgMembershipStatus, TableName, TLdapConfigsUpdate, TUsers } from "@app/db/schemas"; +import { AccessScope, OrgMembershipStatus, TableName, TLdapConfigsUpdate, TUsers } from "@app/db/schemas"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; @@ -12,12 +12,12 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/ import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TokenType } from "@app/services/auth-token/auth-token-types"; -import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TMembershipRoleDALFactory } from "@app/services/membership/membership-role-dal"; +import { TMembershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns"; -import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal"; @@ -49,13 +49,13 @@ import { TLdapGroupMapDALFactory } from "./ldap-group-map-dal"; type TLdapConfigServiceFactoryDep = { ldapConfigDAL: Pick; ldapGroupMapDAL: Pick; - orgMembershipDAL: Pick; orgDAL: Pick< TOrgDALFactory, "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" >; groupDAL: Pick; - groupProjectDAL: Pick; + membershipGroupDAL: Pick; + membershipRoleDAL: Pick; projectKeyDAL: Pick; projectDAL: Pick; projectBotDAL: Pick; @@ -87,9 +87,9 @@ export const ldapConfigServiceFactory = ({ ldapConfigDAL, ldapGroupMapDAL, orgDAL, - orgMembershipDAL, groupDAL, - groupProjectDAL, + membershipGroupDAL, + membershipRoleDAL, projectKeyDAL, projectDAL, projectBotDAL, @@ -388,25 +388,33 @@ export const ldapConfigServiceFactory = ({ await userDAL.transaction(async (tx) => { const [orgMembership] = await orgDAL.findMembership( { - [`${TableName.OrgMembership}.userId` as "userId"]: userAlias.userId, - [`${TableName.OrgMembership}.orgId` as "id"]: orgId + [`${TableName.Membership}.actorUserId` as "actorUserId"]: userAlias.userId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }, { tx } ); if (!orgMembership) { const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole); - await orgDAL.createMembership( + const membership = await orgDAL.createMembership( { - userId: userAlias.userId, - orgId, - role, - roleId, + actorUserId: userAlias.userId, + scopeOrgId: orgId, + scope: AccessScope.Organization, status: OrgMembershipStatus.Accepted, isActive: true }, tx ); + await membershipRoleDAL.create( + { + membershipId: membership.id, + role, + customRoleId: roleId + }, + tx + ); } else if (orgMembership.status === OrgMembershipStatus.Invited) { await orgDAL.updateMembershipById( orgMembership.id, @@ -459,8 +467,9 @@ export const ldapConfigServiceFactory = ({ const [orgMembership] = await orgDAL.findMembership( { - [`${TableName.OrgMembership}.userId` as "userId"]: newUser.id, - [`${TableName.OrgMembership}.orgId` as "id"]: orgId + [`${TableName.Membership}.actorUserId` as "actorUserId"]: userAlias.userId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }, { tx } ); @@ -469,16 +478,22 @@ export const ldapConfigServiceFactory = ({ await throwOnPlanSeatLimitReached(licenseService, orgId, UserAliasType.LDAP); const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole); - - await orgMembershipDAL.create( + const membership = await orgDAL.createMembership( { - userId: newUser.id, - inviteEmail: email.toLowerCase(), - orgId, - role, - roleId, + actorUserId: newUser.id, + scopeOrgId: orgId, + scope: AccessScope.Organization, status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later - isActive: true + isActive: true, + inviteEmail: email.toLowerCase() + }, + tx + ); + await membershipRoleDAL.create( + { + membershipId: membership.id, + role, + customRoleId: roleId }, tx ); @@ -542,10 +557,10 @@ export const ldapConfigServiceFactory = ({ userDAL, userGroupMembershipDAL, orgDAL, - groupProjectDAL, projectKeyDAL, projectDAL, projectBotDAL, + membershipGroupDAL, tx }); } @@ -566,7 +581,7 @@ export const ldapConfigServiceFactory = ({ userIds: [newUser.id], userDAL, userGroupMembershipDAL, - groupProjectDAL, + membershipGroupDAL, projectKeyDAL, tx }); diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index 544eeae37..c2e67908f 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -488,7 +488,7 @@ export const licenseServiceFactory = ({ const getUsageMetrics = async (orgId: string) => { const [orgMembersUsed, identityUsed, projectCount] = await Promise.all([ orgDAL.countAllOrgMembers(orgId), - identityOrgMembershipDAL.countAllOrgIdentities({ orgId }), + identityOrgMembershipDAL.countAllOrgIdentities({ scopeOrgId: orgId }), projectDAL.countOfOrgProjects(orgId) ]); diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts index 3bfccf9aa..c2672a94e 100644 --- a/backend/src/ee/services/oidc/oidc-config-service.ts +++ b/backend/src/ee/services/oidc/oidc-config-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client"; -import { OrgMembershipStatus, TableName, TUsers } from "@app/db/schemas"; +import { AccessScope, OrgMembershipStatus, TableName, TUsers } from "@app/db/schemas"; import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs"; import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; @@ -19,12 +19,12 @@ import { OrgServiceActor } from "@app/lib/types"; import { ActorType, AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TokenType } from "@app/services/auth-token/auth-token-types"; -import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TMembershipRoleDALFactory } from "@app/services/membership/membership-role-dal"; +import { TMembershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns"; -import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal"; @@ -62,7 +62,8 @@ type TOidcConfigServiceFactoryDep = { TOrgDALFactory, "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" >; - orgMembershipDAL: Pick; + membershipGroupDAL: Pick; + membershipRoleDAL: Pick; licenseService: Pick; tokenService: Pick; smtpService: Pick; @@ -78,7 +79,6 @@ type TOidcConfigServiceFactoryDep = { | "delete" | "filterProjectsByUserMembership" >; - groupProjectDAL: Pick; projectKeyDAL: Pick; projectDAL: Pick; projectBotDAL: Pick; @@ -90,7 +90,6 @@ export type TOidcConfigServiceFactory = ReturnType; - permissionService: Pick; - accessApprovalRequestDAL: Pick; -}; - -const unpackPermissions = (permissions: unknown) => - UnpackedPermissionSchema.array().parse( - unpackRules((permissions || []) as PackRule>>[]) - ); - -export const projectUserAdditionalPrivilegeServiceFactory = ({ - projectUserAdditionalPrivilegeDAL, - projectMembershipDAL, - permissionService, - accessApprovalRequestDAL -}: TProjectUserAdditionalPrivilegeServiceFactoryDep): TProjectUserAdditionalPrivilegeServiceFactory => { - const create: TProjectUserAdditionalPrivilegeServiceFactory["create"] = async ({ - slug, - actor, - actorId, - permissions: customPermission, - actorOrgId, - actorAuthMethod, - projectMembershipId, - ...dto - }) => { - const projectMembership = await projectMembershipDAL.findById(projectMembershipId); - if (!projectMembership) - throw new NotFoundError({ message: `Project membership with ID ${projectMembershipId} found` }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: projectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); - const { permission: targetUserPermission, membership } = await permissionService.getProjectPermission({ - actor: ActorType.USER, - actorId: projectMembership.userId, - projectId: projectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - - // we need to validate that the privilege given is not higher than the assigning users permission - // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules - targetUserPermission.update(targetUserPermission.rules.concat(customPermission)); - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionMemberActions.GrantPrivileges, - ProjectPermissionSub.Member, - permission, - targetUserPermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to update more privileged user", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionMemberActions.GrantPrivileges, - ProjectPermissionSub.Member - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - - const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({ - slug, - projectId: projectMembership.projectId, - userId: projectMembership.userId - }); - if (existingSlug) - throw new BadRequestError({ message: `Additional privilege with provided slug ${slug} already exists` }); - - validateHandlebarTemplate("User Additional Privilege Create", JSON.stringify(customPermission || []), { - allowedExpressions: (val) => val.includes("identity.") - }); - - const packedPermission = JSON.stringify(packRules(customPermission)); - if (!dto.isTemporary) { - const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({ - userId: projectMembership.userId, - projectId: projectMembership.projectId, - slug, - permissions: packedPermission - }); - - await permissionService.invalidateProjectPermissionCache(projectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - } - - const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange); - const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({ - projectId: projectMembership.projectId, - userId: projectMembership.userId, - slug, - permissions: packedPermission, - isTemporary: true, - temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative, - temporaryRange: dto.temporaryRange, - temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), - temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) - }); - - await permissionService.invalidateProjectPermissionCache(projectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - }; - - const updateById: TProjectUserAdditionalPrivilegeServiceFactory["updateById"] = async ({ - privilegeId, - actorOrgId, - actor, - actorId, - actorAuthMethod, - ...dto - }) => { - const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId); - if (!userPrivilege) - throw new NotFoundError({ message: `User additional privilege with ID ${privilegeId} not found` }); - - const projectMembership = await projectMembershipDAL.findOne({ - userId: userPrivilege.userId, - projectId: userPrivilege.projectId - }); - - if (!projectMembership) - throw new NotFoundError({ - message: `Project membership for user with ID '${userPrivilege.userId}' not found in project with ID '${userPrivilege.projectId}'` - }); - - const { permission, membership } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: projectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); - const { permission: targetUserPermission } = await permissionService.getProjectPermission({ - actor: ActorType.USER, - actorId: projectMembership.userId, - projectId: projectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - - // we need to validate that the privilege given is not higher than the assigning users permission - // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules - targetUserPermission.update(targetUserPermission.rules.concat(dto.permissions || [])); - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionMemberActions.GrantPrivileges, - ProjectPermissionSub.Member, - permission, - targetUserPermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to update more privileged user", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionMemberActions.GrantPrivileges, - ProjectPermissionSub.Member - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - - if (dto?.slug) { - const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({ - slug: dto.slug, - userId: projectMembership.id, - projectId: projectMembership.projectId - }); - if (existingSlug && existingSlug.id !== userPrivilege.id) - throw new BadRequestError({ message: `Additional privilege with provided slug ${dto.slug} already exists` }); - } - - validateHandlebarTemplate("User Additional Privilege Update", JSON.stringify(dto.permissions || []), { - allowedExpressions: (val) => val.includes("identity.") - }); - - const isTemporary = typeof dto?.isTemporary !== "undefined" ? dto.isTemporary : userPrivilege.isTemporary; - - const packedPermission = dto.permissions && JSON.stringify(packRules(dto.permissions)); - if (isTemporary) { - const temporaryAccessStartTime = dto?.temporaryAccessStartTime || userPrivilege?.temporaryAccessStartTime; - const temporaryRange = dto?.temporaryRange || userPrivilege?.temporaryRange; - const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, { - slug: dto.slug, - permissions: packedPermission, - isTemporary: dto.isTemporary, - temporaryRange: dto.temporaryRange, - temporaryMode: dto.temporaryMode, - temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), - temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) - }); - - await permissionService.invalidateProjectPermissionCache(projectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - } - - const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, { - slug: dto.slug, - permissions: packedPermission, - isTemporary: false, - temporaryAccessStartTime: null, - temporaryAccessEndTime: null, - temporaryRange: null, - temporaryMode: null - }); - - await permissionService.invalidateProjectPermissionCache(projectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - }; - - const deleteById: TProjectUserAdditionalPrivilegeServiceFactory["deleteById"] = async ({ - actorId, - actor, - actorOrgId, - actorAuthMethod, - privilegeId - }) => { - const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId); - if (!userPrivilege) - throw new NotFoundError({ message: `User additional privilege with ID ${privilegeId} not found` }); - - const projectMembership = await projectMembershipDAL.findOne({ - userId: userPrivilege.userId, - projectId: userPrivilege.projectId - }); - if (!projectMembership) - throw new NotFoundError({ - message: `Project membership for user with ID '${userPrivilege.userId}' not found in project with ID '${userPrivilege.projectId}'` - }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: projectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); - - // TODO(simp): look into this in the new ap service - await accessApprovalRequestDAL.update( - { - privilegeId: userPrivilege.id - }, - { - privilegeDeletedAt: new Date(), - status: ApprovalStatus.REJECTED - } - ); - const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id); - - await permissionService.invalidateProjectPermissionCache(projectMembership.projectId); - - return { - ...deletedPrivilege, - permissions: unpackPermissions(deletedPrivilege.permissions) - }; - }; - - const getPrivilegeDetailsById: TProjectUserAdditionalPrivilegeServiceFactory["getPrivilegeDetailsById"] = async ({ - privilegeId, - actorOrgId, - actor, - actorId, - actorAuthMethod - }) => { - const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId); - if (!userPrivilege) - throw new NotFoundError({ message: `User additional privilege with ID ${privilegeId} not found` }); - - const projectMembership = await projectMembershipDAL.findOne({ - userId: userPrivilege.userId, - projectId: userPrivilege.projectId - }); - if (!projectMembership) - throw new NotFoundError({ - message: `Project membership for user with ID '${userPrivilege.userId}' not found in project with ID '${userPrivilege.projectId}'` - }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: projectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); - - return { - ...userPrivilege, - permissions: unpackPermissions(userPrivilege.permissions) - }; - }; - - const listPrivileges: TProjectUserAdditionalPrivilegeServiceFactory["listPrivileges"] = async ({ - projectMembershipId, - actorOrgId, - actor, - actorId, - actorAuthMethod - }) => { - const projectMembership = await projectMembershipDAL.findById(projectMembershipId); - if (!projectMembership) - throw new NotFoundError({ message: `Project membership with ID ${projectMembershipId} not found` }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: projectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); - - const userPrivileges = await projectUserAdditionalPrivilegeDAL.find( - { - userId: projectMembership.userId, - projectId: projectMembership.projectId - }, - { sort: [[`${TableName.ProjectUserAdditionalPrivilege}.slug` as "slug", "asc"]] } - ); - return userPrivileges; - }; - - return { - create, - updateById, - deleteById, - getPrivilegeDetailsById, - listPrivileges - }; -}; diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-types.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-types.ts deleted file mode 100644 index a700d997d..000000000 --- a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-types.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { TProjectUserAdditionalPrivilege } from "@app/db/schemas"; -import { TProjectPermission } from "@app/lib/types"; - -import { TProjectPermissionV2Schema } from "../permission/project-permission"; - -export enum ProjectUserAdditionalPrivilegeTemporaryMode { - Relative = "relative" -} - -export type TCreateUserPrivilegeDTO = ( - | { - permissions: TProjectPermissionV2Schema[]; - projectMembershipId: string; - slug: string; - isTemporary: false; - } - | { - permissions: TProjectPermissionV2Schema[]; - projectMembershipId: string; - slug: string; - isTemporary: true; - temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative; - temporaryRange: string; - temporaryAccessStartTime: string; - } -) & - Omit; - -export type TUpdateUserPrivilegeDTO = { privilegeId: string } & Omit & - Partial<{ - permissions: TProjectPermissionV2Schema[]; - slug: string; - isTemporary: boolean; - temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative; - temporaryRange: string; - temporaryAccessStartTime: string; - }>; - -export type TDeleteUserPrivilegeDTO = Omit & { privilegeId: string }; - -export type TGetUserPrivilegeDetailsDTO = Omit & { privilegeId: string }; - -export type TListUserPrivilegesDTO = Omit & { projectMembershipId: string }; - -interface TAdditionalPrivilege extends TProjectUserAdditionalPrivilege { - permissions: { - action: string[]; - subject?: string | undefined; - conditions?: unknown; - inverted?: boolean | undefined; - }[]; -} - -export type TProjectUserAdditionalPrivilegeServiceFactory = { - create: (arg: TCreateUserPrivilegeDTO) => Promise; - updateById: (arg: TUpdateUserPrivilegeDTO) => Promise; - deleteById: (arg: TDeleteUserPrivilegeDTO) => Promise; - getPrivilegeDetailsById: (arg: TGetUserPrivilegeDetailsDTO) => Promise; - listPrivileges: (arg: TListUserPrivilegesDTO) => Promise; -}; diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts index f1ee313e8..dd19fdd02 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -10,7 +10,8 @@ import { TGroups, TSamlConfigs, TSamlConfigsUpdate, - TUsers + TUsers, + AccessScope } from "@app/db/schemas"; import { throwOnPlanSeatLimitReached } from "@app/ee/services/license/license-fns"; import { getConfig } from "@app/lib/config/env"; @@ -23,12 +24,9 @@ import { TGroupProjectDALFactory } from "@app/services/group-project/group-proje import { TIdentityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TMembershipRoleDALFactory } from "@app/services/membership/membership-role-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns"; -import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; -import { TProjectDALFactory } from "@app/services/project/project-dal"; -import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; -import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { LoginMethod } from "@app/services/super-admin/super-admin-types"; @@ -68,16 +66,7 @@ type TSamlConfigServiceFactoryDep = { "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" >; identityMetadataDAL: Pick; - orgMembershipDAL: Pick; - groupDAL: Pick; - userGroupMembershipDAL: Pick< - TUserGroupMembershipDALFactory, - "find" | "delete" | "transaction" | "insertMany" | "filterProjectsByUserMembership" - >; - groupProjectDAL: Pick; - projectDAL: Pick; - projectBotDAL: Pick; - projectKeyDAL: Pick; + membershipRoleDAL: Pick; permissionService: Pick; licenseService: Pick; tokenService: Pick; @@ -88,7 +77,6 @@ type TSamlConfigServiceFactoryDep = { export const samlConfigServiceFactory = ({ samlConfigDAL, orgDAL, - orgMembershipDAL, userDAL, userAliasDAL, groupDAL, @@ -102,7 +90,8 @@ export const samlConfigServiceFactory = ({ tokenService, smtpService, identityMetadataDAL, - kmsService + kmsService, + membershipRoleDAL }: TSamlConfigServiceFactoryDep): TSamlConfigServiceFactory => { const parseSamlGroups = (groupsValue: string): string[] => { let samlGroups: string[] = []; @@ -506,26 +495,35 @@ export const samlConfigServiceFactory = ({ const foundUser = await userDAL.findById(userAlias.userId, tx); const [orgMembership] = await orgDAL.findMembership( { - [`${TableName.OrgMembership}.userId` as "userId"]: foundUser.id, - [`${TableName.OrgMembership}.orgId` as "id"]: orgId + [`${TableName.Membership}.actorUserId` as "actorUserId"]: userAlias.userId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }, { tx } ); + if (!orgMembership) { const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole); - await orgMembershipDAL.create( + const membership = await orgDAL.createMembership( { - userId: userAlias.userId, + actorUserId: userAlias.userId, inviteEmail: email, - orgId, - role, - roleId, - status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, + scopeOrgId: orgId, + scope: AccessScope.Organization, + status: OrgMembershipStatus.Accepted, isActive: true }, tx ); + await membershipRoleDAL.create( + { + membershipId: membership.id, + role, + customRoleId: roleId + }, + tx + ); // Only update the membership to Accepted if the user account is already completed. } else if (orgMembership.status === OrgMembershipStatus.Invited && foundUser.isAccepted) { await orgDAL.updateMembershipById( @@ -606,8 +604,9 @@ export const samlConfigServiceFactory = ({ const [orgMembership] = await orgDAL.findMembership( { - [`${TableName.OrgMembership}.userId` as "userId"]: newUser.id, - [`${TableName.OrgMembership}.orgId` as "id"]: orgId + [`${TableName.Membership}.actorUserId` as "actorUserId"]: userAlias.userId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }, { tx } ); @@ -617,15 +616,22 @@ export const samlConfigServiceFactory = ({ const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole); - await orgMembershipDAL.create( + const membership = await orgDAL.createMembership( { - userId: newUser.id, - inviteEmail: email, - orgId, - role, - roleId, + actorUserId: newUser.id, + scopeOrgId: orgId, + scope: AccessScope.Organization, status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later - isActive: true + isActive: true, + inviteEmail: email.toLowerCase() + }, + tx + ); + await membershipRoleDAL.create( + { + membershipId: membership.id, + role, + customRoleId: roleId }, tx ); diff --git a/backend/src/ee/services/scim/scim-service.ts b/backend/src/ee/services/scim/scim-service.ts index 9cc6e134d..0ec9e646e 100644 --- a/backend/src/ee/services/scim/scim-service.ts +++ b/backend/src/ee/services/scim/scim-service.ts @@ -2,7 +2,15 @@ import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; import { scimPatch } from "scim-patch"; -import { OrgMembershipRole, OrgMembershipStatus, TableName, TGroups, TOrgMemberships, TUsers } from "@app/db/schemas"; +import { + AccessScope, + OrgMembershipRole, + OrgMembershipStatus, + TableName, + TGroups, + TMemberships, + TUsers +} from "@app/db/schemas"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; @@ -13,12 +21,13 @@ import { BadRequestError, NotFoundError, ScimRequestError, UnauthorizedError } f import { alphaNumericNanoId } from "@app/lib/nanoid"; import { AuthTokenType } from "@app/services/auth/auth-type"; import { TExternalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal"; -import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; +import { TMembershipRoleDALFactory } from "@app/services/membership/membership-role-dal"; +import { TMembershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal"; +import { TMembershipUserDALFactory } from "@app/services/membership-user/membership-user-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { deleteOrgMembershipFn } from "@app/services/org/org-fns"; import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns"; import { OrgAuthMethod } from "@app/services/org/org-types"; -import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal"; @@ -33,7 +42,6 @@ import { UserAliasType } from "@app/services/user-alias/user-alias-types"; import { TLicenseServiceFactory } from "../license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { TPermissionServiceFactory } from "../permission/permission-service-types"; -import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal"; import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList, parseScimFilter } from "./scim-fns"; import { TScimGroup, TScimServiceFactory } from "./scim-types"; @@ -55,12 +63,9 @@ type TScimServiceFactoryDep = { | "updateMembershipById" | "findOrgById" >; - orgMembershipDAL: Pick< - TOrgMembershipDALFactory, - "find" | "findOne" | "create" | "updateById" | "findById" | "update" - >; + membershipUserDAL: TMembershipUserDALFactory; projectDAL: Pick; - projectMembershipDAL: Pick; + projectMembershipDAL: Pick; groupDAL: Pick< TGroupDALFactory, | "create" @@ -72,7 +77,8 @@ type TScimServiceFactoryDep = { | "updateById" | "update" >; - groupProjectDAL: Pick; + membershipGroupDAL: Pick; + membershipRoleDAL: Pick; userGroupMembershipDAL: Pick< TUserGroupMembershipDALFactory, | "find" @@ -88,7 +94,6 @@ type TScimServiceFactoryDep = { licenseService: Pick; permissionService: Pick; smtpService: Pick; - projectUserAdditionalPrivilegeDAL: Pick; externalGroupOrgRoleMappingDAL: TExternalGroupOrgRoleMappingDALFactory; }; @@ -98,18 +103,18 @@ export const scimServiceFactory = ({ userDAL, userAliasDAL, orgDAL, - orgMembershipDAL, projectDAL, projectMembershipDAL, groupDAL, - groupProjectDAL, userGroupMembershipDAL, projectKeyDAL, projectBotDAL, permissionService, - projectUserAdditionalPrivilegeDAL, smtpService, - externalGroupOrgRoleMappingDAL + externalGroupOrgRoleMappingDAL, + membershipGroupDAL, + membershipUserDAL, + membershipRoleDAL }: TScimServiceFactoryDep): TScimServiceFactory => { const createScimToken: TScimServiceFactory["createScimToken"] = async ({ actor, @@ -244,8 +249,9 @@ export const scimServiceFactory = ({ const getScimUser: TScimServiceFactory["getScimUser"] = async ({ orgMembershipId, orgId }) => { const [membership] = await orgDAL .findMembership({ - [`${TableName.OrgMembership}.id` as "id"]: orgMembershipId, - [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId + [`${TableName.Membership}.id` as "id"]: orgMembershipId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }) .catch(() => { throw new ScimRequestError({ @@ -322,13 +328,14 @@ export const scimServiceFactory = ({ const { user: createdUser, orgMembership: createdOrgMembership } = await userDAL.transaction(async (tx) => { let user: TUsers | undefined; - let orgMembership: TOrgMemberships; + let orgMembership: TMemberships; if (userAlias) { user = await userDAL.findById(userAlias.userId, tx); - orgMembership = await orgMembershipDAL.findOne( + orgMembership = await membershipUserDAL.findOne( { - userId: user.id, - orgId + actorUserId: user.id, + scope: AccessScope.Organization, + scopeOrgId: orgId }, tx ); @@ -336,20 +343,24 @@ export const scimServiceFactory = ({ if (!orgMembership) { const { role, roleId } = await getDefaultOrgMembershipRole(org.defaultMembershipRole); - orgMembership = await orgMembershipDAL.create( + orgMembership = await membershipUserDAL.create( { - userId: userAlias.userId, + actorUserId: userAlias.userId, inviteEmail: email.toLowerCase(), - orgId, - role, - roleId, + scopeOrgId: orgId, + scope: AccessScope.Organization, status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later isActive: true }, tx ); + await membershipRoleDAL.create({ + membershipId: orgMembership.id, + role, + customRoleId: roleId + }); } else if (orgMembership.status === OrgMembershipStatus.Invited && user.isAccepted) { - orgMembership = await orgMembershipDAL.updateById( + orgMembership = await membershipUserDAL.updateById( orgMembership.id, { status: OrgMembershipStatus.Accepted @@ -401,8 +412,9 @@ export const scimServiceFactory = ({ const [foundOrgMembership] = await orgDAL.findMembership( { - [`${TableName.OrgMembership}.userId` as "userId"]: user.id, - [`${TableName.OrgMembership}.orgId` as "id"]: orgId + [`${TableName.Membership}.actorUserId` as "actorUserId"]: user.id, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }, { tx } ); @@ -412,18 +424,25 @@ export const scimServiceFactory = ({ if (!orgMembership) { const { role, roleId } = await getDefaultOrgMembershipRole(org.defaultMembershipRole); - orgMembership = await orgMembershipDAL.create( + orgMembership = await membershipUserDAL.create( { - userId: user.id, + actorUserId: user.id, inviteEmail: email.toLowerCase(), - orgId, - role, - roleId, + scopeOrgId: orgId, + scope: AccessScope.Organization, status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later isActive: true }, tx ); + await membershipRoleDAL.create( + { + membershipId: orgMembership.id, + role, + customRoleId: roleId + }, + tx + ); // Only update the membership to Accepted if the user account is already completed. } else if (orgMembership.status === OrgMembershipStatus.Invited && user.isAccepted) { orgMembership = await orgDAL.updateMembershipById( @@ -475,8 +494,9 @@ export const scimServiceFactory = ({ const [membership] = await orgDAL .findMembership({ - [`${TableName.OrgMembership}.id` as "id"]: orgMembershipId, - [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId + [`${TableName.Membership}.id` as "id"]: orgMembershipId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }) .catch(() => { throw new ScimRequestError({ @@ -485,7 +505,7 @@ export const scimServiceFactory = ({ }); }); - if (!membership) + if (!membership || !membership.actorUserId) throw new ScimRequestError({ detail: "User not found", status: 404 @@ -514,7 +534,7 @@ export const scimServiceFactory = ({ org.orgAuthMethod === OrgAuthMethod.OIDC ? serverCfg.trustOidcEmails : serverCfg.trustSamlEmails; await userDAL.transaction(async (tx) => { - await orgMembershipDAL.updateById( + await membershipUserDAL.updateById( membership.id, { isActive: scimUser.active @@ -523,7 +543,7 @@ export const scimServiceFactory = ({ ); const hasEmailChanged = scimUser.emails[0].value !== membership.email; await userDAL.updateById( - membership.userId, + membership.actorUserId as string, { firstName: scimUser.name.givenName, email: scimUser.emails[0].value.toLowerCase(), @@ -556,8 +576,9 @@ export const scimServiceFactory = ({ const [membership] = await orgDAL .findMembership({ - [`${TableName.OrgMembership}.id` as "id"]: orgMembershipId, - [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId + [`${TableName.Membership}.id` as "id"]: orgMembershipId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }) .catch(() => { throw new ScimRequestError({ @@ -566,7 +587,7 @@ export const scimServiceFactory = ({ }); }); - if (!membership) + if (!membership || !membership.actorUserId) throw new ScimRequestError({ detail: "User not found", status: 404 @@ -587,7 +608,7 @@ export const scimServiceFactory = ({ { orgId, aliasType: org.orgAuthMethod === OrgAuthMethod.OIDC ? UserAliasType.OIDC : UserAliasType.SAML, - userId: membership.userId + userId: membership.actorUserId as string }, { externalId @@ -595,7 +616,7 @@ export const scimServiceFactory = ({ tx ); - await orgMembershipDAL.updateById( + await membershipUserDAL.updateById( membership.id, { isActive: active @@ -603,7 +624,7 @@ export const scimServiceFactory = ({ tx ); await userDAL.updateById( - membership.userId, + membership.actorUserId!, { firstName, email: email?.toLowerCase(), @@ -628,8 +649,9 @@ export const scimServiceFactory = ({ const deleteScimUser: TScimServiceFactory["deleteScimUser"] = async ({ orgMembershipId, orgId }) => { const [membership] = await orgDAL.findMembership({ - [`${TableName.OrgMembership}.id` as "id"]: orgMembershipId, - [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId + [`${TableName.Membership}.id` as "id"]: orgMembershipId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }); if (!membership) @@ -647,13 +669,13 @@ export const scimServiceFactory = ({ await deleteOrgMembershipFn({ orgMembershipId: membership.id, - orgId: membership.orgId, + orgId: membership.scopeOrgId, orgDAL, projectMembershipDAL, - projectUserAdditionalPrivilegeDAL, projectKeyDAL, userAliasDAL, - licenseService + licenseService, + membershipUserDAL }); return {}; // intentionally return empty object upon success @@ -750,8 +772,9 @@ export const scimServiceFactory = ({ if (!externalGroupMapping) return; // only get org memberships that are new (invites) - const newOrgMemberships = await orgMembershipDAL.find({ + const newOrgMemberships = await membershipUserDAL.find({ status: "invited", + scope: AccessScope.Organization, $in: { id: members.map((member) => member.value) } @@ -760,7 +783,7 @@ export const scimServiceFactory = ({ if (!newOrgMemberships.length) return; // set new membership roles to group mapping value - await orgMembershipDAL.update( + await membershipRoleDAL.update( { $in: { id: newOrgMemberships.map((membership) => membership.id) @@ -768,7 +791,7 @@ export const scimServiceFactory = ({ }, { role: externalGroupMapping.role, - roleId: externalGroupMapping.roleId + customRoleId: externalGroupMapping.roleId } ); }; @@ -822,7 +845,8 @@ export const scimServiceFactory = ({ ); if (members && members.length) { - const orgMemberships = await orgMembershipDAL.find({ + const orgMemberships = await membershipUserDAL.find({ + scope: AccessScope.Organization, $in: { id: members.map((member) => member.value) } @@ -830,14 +854,14 @@ export const scimServiceFactory = ({ const newMembers = await addUsersToGroupByUserIds({ group, - userIds: orgMemberships.map((membership) => membership.userId as string), + userIds: orgMemberships.map((membership) => membership.actorUserId as string), userDAL, userGroupMembershipDAL, orgDAL, - groupProjectDAL, projectKeyDAL, projectDAL, projectBotDAL, + membershipGroupDAL, tx }); @@ -850,9 +874,10 @@ export const scimServiceFactory = ({ }); const orgMemberships = await orgDAL.findMembership({ - [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization, $in: { - [`${TableName.OrgMembership}.userId` as "userId"]: newGroup.newMembers.map((member) => member.id) + [`${TableName.Membership}.actorUserId` as "actorUserId"]: newGroup.newMembers.map((member) => member.id) } }); @@ -895,9 +920,10 @@ export const scimServiceFactory = ({ .then((g) => g.members); const orgMemberships = await orgDAL.findMembership({ - [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization, $in: { - [`${TableName.OrgMembership}.userId` as "userId"]: users + [`${TableName.Membership}.actorUserId` as "actorUserId"]: users .filter((user) => user.isPartOfGroup) .map((user) => user.id) } @@ -933,10 +959,10 @@ export const scimServiceFactory = ({ } const updatedGroup = await groupDAL.transaction(async (tx) => { - if (group.name !== displayName) { + if (group?.name !== displayName) { await externalGroupOrgRoleMappingDAL.update( { - groupName: group.name, + groupName: group?.name, orgId }, { @@ -958,14 +984,16 @@ export const scimServiceFactory = ({ } const orgMemberships = members.length - ? await orgMembershipDAL.find({ + ? await membershipUserDAL.find({ + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization, $in: { id: members.map((member) => member.value) } }) : []; - const membersIdsSet = new Set(orgMemberships.map((orgMembership) => orgMembership.userId)); + const membersIdsSet = new Set(orgMemberships.map((orgMembership) => orgMembership.actorUserId as string)); const userGroupMembers = await userGroupMembershipDAL.find({ groupId: group.id }); @@ -978,20 +1006,20 @@ export const scimServiceFactory = ({ const allMembersUserIds = directMemberUserIds.concat(pendingGroupAdditionsUserIds); const allMembersUserIdsSet = new Set(allMembersUserIds); - const toAddUserIds = orgMemberships.filter((member) => !allMembersUserIdsSet.has(member.userId as string)); + const toAddUserIds = orgMemberships.filter((member) => !allMembersUserIdsSet.has(member.actorUserId as string)); const toRemoveUserIds = allMembersUserIds.filter((userId) => !membersIdsSet.has(userId)); if (toAddUserIds.length) { await addUsersToGroupByUserIds({ group, - userIds: toAddUserIds.map((member) => member.userId as string), + userIds: toAddUserIds.map((member) => member.actorUserId as string), userDAL, userGroupMembershipDAL, orgDAL, - groupProjectDAL, projectKeyDAL, projectDAL, projectBotDAL, + membershipGroupDAL, tx }); } @@ -1002,7 +1030,7 @@ export const scimServiceFactory = ({ userIds: toRemoveUserIds, userDAL, userGroupMembershipDAL, - groupProjectDAL, + membershipGroupDAL, projectKeyDAL, tx }); diff --git a/backend/src/server/routes/v1/group-project-router.ts b/backend/src/server/routes/v1/group-project-router.ts index d07e3bd8b..43192cd8a 100644 --- a/backend/src/server/routes/v1/group-project-router.ts +++ b/backend/src/server/routes/v1/group-project-router.ts @@ -1,10 +1,12 @@ import { z } from "zod"; import { + AccessScope, GroupProjectMembershipsSchema, GroupsSchema, ProjectMembershipRole, ProjectUserMembershipRolesSchema, + TemporaryPermissionMode, UsersSchema } from "@app/db/schemas"; import { EFilterReturnedUsers } from "@app/ee/services/group/group-types"; @@ -13,7 +15,6 @@ import { ms } from "@app/lib/ms"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; -import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types"; export const registerGroupProjectRouter = async (server: FastifyZodProvider) => { server.route({ @@ -54,7 +55,7 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => z.object({ role: z.string(), isTemporary: z.literal(true), - temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode), + temporaryMode: z.nativeEnum(TemporaryPermissionMode), temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), temporaryAccessStartTime: z.string().datetime() }) @@ -73,17 +74,27 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => } }, handler: async (req) => { - const groupMembership = await server.services.groupProject.addGroupToProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - roles: req.body.roles || [{ role: req.body.role }], - projectId: req.params.projectId, - groupIdOrName: req.params.groupIdOrName + // TODO(simp): name to id convertor + const { membership: groupMembership } = await server.services.membershipGroup.createMembership({ + permission: req.permission, + data: { + groupId: req.params.groupIdOrName, + roles: req.body.roles || [{ role: req.body.role, isTemporary: false }] + }, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { groupMembership }; + return { + groupMembership: { + ...groupMembership, + projectId: req.params.projectId, + groupId: groupMembership.actorGroupId as string + } + }; } }); @@ -115,7 +126,7 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => z.object({ role: z.string(), isTemporary: z.literal(true), - temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode), + temporaryMode: z.nativeEnum(TemporaryPermissionMode), temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), temporaryAccessStartTime: z.string().datetime() }) @@ -131,17 +142,22 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => } }, handler: async (req) => { - const roles = await server.services.groupProject.updateGroupInProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.projectId, - groupId: req.params.groupId, - roles: req.body.roles + const { membership: groupMembership } = await server.services.membershipGroup.updateMembership({ + permission: req.permission, + selector: { + groupId: req.params.groupId + }, + data: { + roles: req.body.roles + }, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { roles }; + return { roles: groupMembership.roles.map((el) => ({ ...el, projectMembershipId: groupMembership.id })) }; } }); @@ -172,16 +188,25 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => } }, handler: async (req) => { - const groupMembership = await server.services.groupProject.removeGroupFromProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - groupId: req.params.groupId, - projectId: req.params.projectId + const { membership: groupMembership } = await server.services.membershipGroup.deleteMembership({ + permission: req.permission, + selector: { + groupId: req.params.groupId + }, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { groupMembership }; + return { + groupMembership: { + ...groupMembership, + projectId: req.params.projectId, + groupId: groupMembership.actorGroupId as string + } + }; } }); @@ -233,15 +258,17 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => } }, handler: async (req) => { - const groupMemberships = await server.services.groupProject.listGroupsInProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.projectId + const { memberships: groupMemberships } = await server.services.membershipGroup.listMemberships({ + permission: req.permission, + data: {}, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { groupMemberships }; + return { groupMemberships: groupMemberships.map((el) => ({ ...el, groupId: el.actorGroupId as string })) }; } }); @@ -292,15 +319,25 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => } }, handler: async (req) => { - const groupMembership = await server.services.groupProject.getGroupInProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - ...req.params + const { membership: groupMembership } = await server.services.membershipGroup.getMembershipByGroupId({ + permission: req.permission, + selector: { + groupId: req.params.groupId + }, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { groupMembership }; + return { + groupMembership: { + ...groupMembership, + projectId: req.params.projectId, + groupId: groupMembership.actorGroupId as string + } + }; } }); diff --git a/backend/src/server/routes/v2/deprecated-group-project-router.ts b/backend/src/server/routes/v2/deprecated-group-project-router.ts index f0e4ee705..fad15e352 100644 --- a/backend/src/server/routes/v2/deprecated-group-project-router.ts +++ b/backend/src/server/routes/v2/deprecated-group-project-router.ts @@ -1,10 +1,12 @@ import { z } from "zod"; import { + AccessScope, GroupProjectMembershipsSchema, GroupsSchema, ProjectMembershipRole, ProjectUserMembershipRolesSchema, + TemporaryPermissionMode, UsersSchema } from "@app/db/schemas"; import { EFilterReturnedUsers } from "@app/ee/services/group/group-types"; @@ -13,7 +15,6 @@ import { ms } from "@app/lib/ms"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; -import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types"; export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodProvider) => { server.route({ @@ -54,7 +55,7 @@ export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodPro z.object({ role: z.string(), isTemporary: z.literal(true), - temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode), + temporaryMode: z.nativeEnum(TemporaryPermissionMode), temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), temporaryAccessStartTime: z.string().datetime() }) @@ -73,17 +74,26 @@ export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodPro } }, handler: async (req) => { - const groupMembership = await server.services.groupProject.addGroupToProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - roles: req.body.roles || [{ role: req.body.role }], - projectId: req.params.projectId, - groupIdOrName: req.params.groupIdOrName + const { membership: groupMembership } = await server.services.membershipGroup.createMembership({ + permission: req.permission, + data: { + groupId: req.params.groupIdOrName, + roles: req.body.roles || [{ role: req.body.role, isTemporary: false }] + }, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { groupMembership }; + return { + groupMembership: { + ...groupMembership, + projectId: req.params.projectId, + groupId: groupMembership.actorGroupId as string + } + }; } }); @@ -115,7 +125,7 @@ export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodPro z.object({ role: z.string(), isTemporary: z.literal(true), - temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode), + temporaryMode: z.nativeEnum(TemporaryPermissionMode), temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), temporaryAccessStartTime: z.string().datetime() }) @@ -131,17 +141,22 @@ export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodPro } }, handler: async (req) => { - const roles = await server.services.groupProject.updateGroupInProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.projectId, - groupId: req.params.groupId, - roles: req.body.roles + const { membership: groupMembership } = await server.services.membershipGroup.updateMembership({ + permission: req.permission, + selector: { + groupId: req.params.groupId + }, + data: { + roles: req.body.roles + }, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { roles }; + return { roles: groupMembership.roles.map((el) => ({ ...el, projectMembershipId: groupMembership.id })) }; } }); @@ -172,16 +187,25 @@ export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodPro } }, handler: async (req) => { - const groupMembership = await server.services.groupProject.removeGroupFromProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - groupId: req.params.groupId, - projectId: req.params.projectId + const { membership: groupMembership } = await server.services.membershipGroup.deleteMembership({ + permission: req.permission, + selector: { + groupId: req.params.groupId + }, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { groupMembership }; + return { + groupMembership: { + ...groupMembership, + projectId: req.params.projectId, + groupId: groupMembership.actorGroupId as string + } + }; } }); @@ -233,15 +257,17 @@ export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodPro } }, handler: async (req) => { - const groupMemberships = await server.services.groupProject.listGroupsInProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.projectId + const { memberships: groupMemberships } = await server.services.membershipGroup.listMemberships({ + permission: req.permission, + data: {}, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { groupMemberships }; + return { groupMemberships: groupMemberships.map((el) => ({ ...el, groupId: el.actorGroupId as string })) }; } }); @@ -292,15 +318,25 @@ export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodPro } }, handler: async (req) => { - const groupMembership = await server.services.groupProject.getGroupInProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - ...req.params + const { membership: groupMembership } = await server.services.membershipGroup.getMembershipByGroupId({ + permission: req.permission, + selector: { + groupId: req.params.groupId + }, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { groupMembership }; + return { + groupMembership: { + ...groupMembership, + projectId: req.params.projectId, + groupId: groupMembership.actorGroupId as string + } + }; } }); diff --git a/backend/src/services/auth-token/auth-token-service.ts b/backend/src/services/auth-token/auth-token-service.ts index 613aa0766..82df0dcb1 100644 --- a/backend/src/services/auth-token/auth-token-service.ts +++ b/backend/src/services/auth-token/auth-token-service.ts @@ -1,12 +1,12 @@ import { Knex } from "knex"; -import { TAuthTokens, TAuthTokenSessions } from "@app/db/schemas"; +import { AccessScope, TAuthTokens, TAuthTokenSessions } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; -import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { AuthModeJwtTokenPayload, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "../auth/auth-type"; +import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; import { TUserDALFactory } from "../user/user-dal"; import { TTokenDALFactory } from "./auth-token-dal"; import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenForUserDTO } from "./auth-token-types"; @@ -14,7 +14,7 @@ import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenFo type TAuthTokenServiceFactoryDep = { tokenDAL: TTokenDALFactory; userDAL: Pick; - orgMembershipDAL: Pick; + membershipUserDAL: Pick; }; export type TAuthTokenServiceFactory = ReturnType; @@ -80,7 +80,7 @@ export const getTokenConfig = (tokenType: TokenType) => { } }; -export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAuthTokenServiceFactoryDep) => { +export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL }: TAuthTokenServiceFactoryDep) => { const createTokenForUser = async ({ type, userId, orgId, aliasId, payload }: TCreateTokenForUserDTO) => { const { token, ...tkCfg } = getTokenConfig(type); const appCfg = getConfig(); @@ -208,9 +208,10 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu if (!user || !user.isAccepted) throw new NotFoundError({ message: `User with ID '${session.userId}' not found` }); if (token.organizationId) { - const orgMembership = await orgMembershipDAL.findOne({ - userId: user.id, - orgId: token.organizationId + const orgMembership = await membershipUserDAL.findOne({ + actorUserId: user.id, + scopeOrgId: token.organizationId, + scope: AccessScope.Organization }); if (!orgMembership) { diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index d69c836e9..31a9cc5a8 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -1,6 +1,13 @@ import { Knex } from "knex"; -import { OrgMembershipRole, OrgMembershipStatus, TableName, TUsers, UserDeviceSchema } from "@app/db/schemas"; +import { + AccessScope, + OrgMembershipRole, + OrgMembershipStatus, + TableName, + TUsers, + UserDeviceSchema +} from "@app/db/schemas"; import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { getConfig } from "@app/lib/config/env"; @@ -14,11 +21,12 @@ import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TokenType } from "../auth-token/auth-token-types"; +import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; +import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; import { TNotificationServiceFactory } from "../notification/notification-service"; import { NotificationType } from "../notification/notification-types"; import { TOrgDALFactory } from "../org/org-dal"; import { getDefaultOrgMembershipRole } from "../org/org-role-fns"; -import { TOrgMembershipDALFactory } from "../org-membership/org-membership-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { LoginMethod } from "../super-admin/super-admin-types"; import { TTotpServiceFactory } from "../totp/totp-service"; @@ -48,7 +56,8 @@ type TAuthLoginServiceFactoryDep = { smtpService: TSmtpService; totpService: Pick; auditLogService: Pick; - orgMembershipDAL: TOrgMembershipDALFactory; + membershipUserDAL: TMembershipUserDALFactory; + membershipRoleDAL: TMembershipRoleDALFactory; notificationService: Pick; }; @@ -58,10 +67,11 @@ export const authLoginServiceFactory = ({ tokenService, smtpService, orgDAL, - orgMembershipDAL, totpService, auditLogService, - notificationService + notificationService, + membershipUserDAL, + membershipRoleDAL }: TAuthLoginServiceFactoryDep) => { /* * Private @@ -163,8 +173,8 @@ export const authLoginServiceFactory = ({ if (organizationId) { const org = await orgDAL.findById(organizationId); if (org) { - await orgMembershipDAL.update( - { userId: user.id, orgId: org.id }, + await membershipUserDAL.update( + { actorUserId: user.id, scopeOrgId: org.id, scope: AccessScope.Organization }, { lastLoginAuthMethod: authMethod, lastLoginTime: new Date() } ); if (org.userTokenExpiration) { @@ -858,21 +868,34 @@ export const authLoginServiceFactory = ({ } orgId = defaultOrg.id; const [orgMembership] = await orgDAL.findMembership({ - [`${TableName.OrgMembership}.userId` as "userId"]: user.id, - [`${TableName.OrgMembership}.orgId` as "id"]: orgId + [`${TableName.Membership}.actorUserId` as "actorUserId"]: user.id, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }); if (!orgMembership) { const { role, roleId } = await getDefaultOrgMembershipRole(defaultOrg.defaultMembershipRole); - await orgMembershipDAL.create({ - userId: user.id, - inviteEmail: email, - orgId, - role, - roleId, - status: OrgMembershipStatus.Accepted, - isActive: true + await membershipUserDAL.transaction(async (tx) => { + const membership = await membershipUserDAL.create( + { + actorUserId: user?.id, + inviteEmail: email, + scopeOrgId: orgId, + scope: AccessScope.Organization, + status: OrgMembershipStatus.Accepted, + isActive: true + }, + tx + ); + await membershipRoleDAL.create( + { + membershipId: membership.id, + role, + customRoleId: roleId + }, + tx + ); }); } } @@ -895,10 +918,11 @@ export const authLoginServiceFactory = ({ if (org) { // checks for the membership and only sets the orgId / orgName if the user is a member of the specified org const orgMembership = await orgDAL.findMembership({ - [`${TableName.OrgMembership}.userId` as "userId"]: user.id, - [`${TableName.OrgMembership}.orgId` as "orgId"]: org.id, - [`${TableName.OrgMembership}.isActive` as "isActive"]: true, - [`${TableName.OrgMembership}.status` as "status"]: OrgMembershipStatus.Accepted + [`${TableName.Membership}.actorUserId` as "actorUserId"]: user.id, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: org.id, + [`${TableName.Membership}.isActive` as "isActive"]: true, + [`${TableName.Membership}.status` as "status"]: OrgMembershipStatus.Accepted, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }); if (orgMembership) { diff --git a/backend/src/services/auth/auth-signup-service.ts b/backend/src/services/auth/auth-signup-service.ts index 7bc3a5ef6..a2e426a2e 100644 --- a/backend/src/services/auth/auth-signup-service.ts +++ b/backend/src/services/auth/auth-signup-service.ts @@ -1,4 +1,4 @@ -import { OrgMembershipStatus, TableName } from "@app/db/schemas"; +import { AccessScope, OrgMembershipStatus, TableName } from "@app/db/schemas"; import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; @@ -8,17 +8,15 @@ import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { getMinExpiresIn } from "@app/lib/fn"; import { isDisposableEmail } from "@app/lib/validator"; -import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TokenType } from "../auth-token/auth-token-types"; +import { TMembershipGroupDALFactory } from "../membership-group/membership-group-dal"; import { TOrgDALFactory } from "../org/org-dal"; import { TOrgServiceFactory } from "../org/org-service"; -import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; -import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { getServerCfg } from "../super-admin/super-admin-service"; import { TUserDALFactory } from "../user/user-dal"; @@ -42,14 +40,12 @@ type TAuthSignupDep = { projectKeyDAL: Pick; projectDAL: Pick; projectBotDAL: Pick; - groupProjectDAL: Pick; orgService: Pick; orgDAL: TOrgDALFactory; tokenService: TAuthTokenServiceFactory; smtpService: TSmtpService; licenseService: Pick; - projectMembershipDAL: Pick; - projectUserMembershipRoleDAL: Pick; + membershipGroupDAL: TMembershipGroupDALFactory; }; export type TAuthSignupFactory = ReturnType; @@ -60,11 +56,11 @@ export const authSignupServiceFactory = ({ projectKeyDAL, projectDAL, projectBotDAL, - groupProjectDAL, tokenService, smtpService, orgService, orgDAL, + membershipGroupDAL, licenseService }: TAuthSignupDep) => { // first step of signup. create user and send email @@ -200,9 +196,10 @@ export const authSignupServiceFactory = ({ organizationId ) { const [pendingOrgMembership] = await orgDAL.findMembership({ - [`${TableName.OrgMembership}.userId` as "userId"]: user.id, + [`${TableName.Membership}.actorUserId` as "actorUserId"]: user.id, status: OrgMembershipStatus.Invited, - [`${TableName.OrgMembership}.orgId` as "orgId"]: organizationId + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: organizationId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }); if (pendingOrgMembership) { @@ -241,18 +238,18 @@ export const authSignupServiceFactory = ({ } const updatedMembersips = await orgDAL.updateMembership( - { inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited }, - { userId: user.id, status: OrgMembershipStatus.Accepted } + { inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited, scope: AccessScope.Organization }, + { actorUserId: user.id, status: OrgMembershipStatus.Accepted } ); - const uniqueOrgId = [...new Set(updatedMembersips.map(({ orgId }) => orgId))]; + const uniqueOrgId = [...new Set(updatedMembersips.map(({ scopeOrgId }) => scopeOrgId))]; await Promise.allSettled(uniqueOrgId.map((orgId) => licenseService.updateSubscriptionOrgMemberCount(orgId))); await convertPendingGroupAdditionsToGroupMemberships({ userIds: [user.id], userDAL, userGroupMembershipDAL, - groupProjectDAL, projectKeyDAL, + membershipGroupDAL, projectDAL, projectBotDAL }); @@ -351,21 +348,21 @@ export const authSignupServiceFactory = ({ ); const updatedMembersips = await orgDAL.updateMembership( - { inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited }, - { userId: us.id, status: OrgMembershipStatus.Accepted }, + { inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited, scope: AccessScope.Organization }, + { actorUserId: us.id, status: OrgMembershipStatus.Accepted }, tx ); - const uniqueOrgId = [...new Set(updatedMembersips.map(({ orgId }) => orgId))]; + const uniqueOrgId = [...new Set(updatedMembersips.map(({ scopeOrgId }) => scopeOrgId))]; await Promise.allSettled(uniqueOrgId.map((orgId) => licenseService.updateSubscriptionOrgMemberCount(orgId, tx))); await convertPendingGroupAdditionsToGroupMemberships({ userIds: [user.id], userDAL, userGroupMembershipDAL, - groupProjectDAL, projectKeyDAL, projectDAL, projectBotDAL, + membershipGroupDAL, tx }); diff --git a/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-fns.ts b/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-fns.ts index 552eed307..21b0b3c1f 100644 --- a/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-fns.ts +++ b/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-fns.ts @@ -1,19 +1,19 @@ -import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas"; +import { OrgMembershipRole, TRoles } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; -import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal"; import { isCustomOrgRole } from "@app/services/org/org-role-fns"; +import { TRoleDALFactory } from "../role/role-dal"; import { TExternalGroupOrgMembershipRoleMappingDTO } from "./external-group-org-role-mapping-types"; export const constructGroupOrgMembershipRoleMappings = async ({ mappingsDTO, orgId, - orgRoleDAL, + roleDAL, licenseService }: { mappingsDTO: TExternalGroupOrgMembershipRoleMappingDTO[]; - orgRoleDAL: TOrgRoleDALFactory; + roleDAL: TRoleDALFactory; licenseService: TLicenseServiceFactory; orgId: string; }) => { @@ -30,9 +30,9 @@ export const constructGroupOrgMembershipRoleMappings = async ({ .filter((mapping) => isCustomOrgRole(mapping.roleSlug)) .map((mapping) => mapping.roleSlug); - let customRolesMap: Map = new Map(); + let customRolesMap: Map = new Map(); if (customRoleSlugs.length > 0) { - const customRoles = await orgRoleDAL.find({ + const customRoles = await roleDAL.find({ orgId, $in: { slug: customRoleSlugs diff --git a/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-service.ts b/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-service.ts index de293609b..a072544ca 100644 --- a/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-service.ts +++ b/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-service.ts @@ -6,15 +6,15 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { OrgServiceActor } from "@app/lib/types"; import { constructGroupOrgMembershipRoleMappings } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-fns"; import { TSyncExternalGroupOrgMembershipRoleMappingsDTO } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-types"; -import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal"; +import { TRoleDALFactory } from "../role/role-dal"; import { TExternalGroupOrgRoleMappingDALFactory } from "./external-group-org-role-mapping-dal"; type TExternalGroupOrgRoleMappingServiceFactoryDep = { externalGroupOrgRoleMappingDAL: TExternalGroupOrgRoleMappingDALFactory; permissionService: TPermissionServiceFactory; licenseService: TLicenseServiceFactory; - orgRoleDAL: TOrgRoleDALFactory; + roleDAL: TRoleDALFactory; }; export type TExternalGroupOrgRoleMappingServiceFactory = ReturnType; @@ -23,7 +23,7 @@ export const externalGroupOrgRoleMappingServiceFactory = ({ externalGroupOrgRoleMappingDAL, licenseService, permissionService, - orgRoleDAL + roleDAL }: TExternalGroupOrgRoleMappingServiceFactoryDep) => { const listExternalGroupOrgRoleMappings = async (actor: OrgServiceActor) => { const { permission } = await permissionService.getOrgPermission( @@ -61,7 +61,7 @@ export const externalGroupOrgRoleMappingServiceFactory = ({ const mappings = await constructGroupOrgMembershipRoleMappings({ mappingsDTO: dto.mappings, - orgRoleDAL, + roleDAL, licenseService, orgId: actor.orgId }); diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts index 1e41c919c..4a4a61496 100644 --- a/backend/src/services/group-project/group-project-service.ts +++ b/backend/src/services/group-project/group-project-service.ts @@ -1,504 +1,27 @@ import { ForbiddenError } from "@casl/ability"; -import { ActionProjectType, ProjectMembershipRole, ProjectVersion, SecretKeyEncoding, TGroups } from "@app/db/schemas"; +import { ActionProjectType } from "@app/db/schemas"; import { TListProjectGroupUsersDTO } from "@app/ee/services/group/group-types"; -import { - constructPermissionErrorMessage, - validatePrivilegeChangeOperation -} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionGroupActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { crypto } from "@app/lib/crypto/cryptography"; -import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; -import { groupBy } from "@app/lib/fn"; -import { ms } from "@app/lib/ms"; -import { isUuidV4 } from "@app/lib/validator"; +import { NotFoundError } from "@app/lib/errors"; import { TGroupDALFactory } from "../../ee/services/group/group-dal"; -import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal"; import { TProjectDALFactory } from "../project/project-dal"; -import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; -import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; -import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types"; -import { TProjectRoleDALFactory } from "../project-role/project-role-dal"; -import { TGroupProjectDALFactory } from "./group-project-dal"; -import { TGroupProjectMembershipRoleDALFactory } from "./group-project-membership-role-dal"; -import { - TCreateProjectGroupDTO, - TDeleteProjectGroupDTO, - TGetGroupInProjectDTO, - TListProjectGroupDTO, - TUpdateProjectGroupDTO -} from "./group-project-types"; type TGroupProjectServiceFactoryDep = { - groupProjectDAL: Pick; - groupProjectMembershipRoleDAL: Pick< - TGroupProjectMembershipRoleDALFactory, - "create" | "transaction" | "insertMany" | "delete" - >; - userGroupMembershipDAL: Pick; - projectDAL: Pick; - projectKeyDAL: Pick; - projectRoleDAL: Pick; - projectBotDAL: TProjectBotDALFactory; groupDAL: Pick; - permissionService: Pick; + projectDAL: Pick; + permissionService: Pick; }; export type TGroupProjectServiceFactory = ReturnType; export const groupProjectServiceFactory = ({ groupDAL, - groupProjectDAL, - groupProjectMembershipRoleDAL, - userGroupMembershipDAL, projectDAL, - projectKeyDAL, - projectBotDAL, - projectRoleDAL, permissionService }: TGroupProjectServiceFactoryDep) => { - const addGroupToProject = async ({ - actor, - actorId, - actorOrgId, - actorAuthMethod, - roles, - projectId, - groupIdOrName - }: TCreateProjectGroupDTO) => { - const project = await projectDAL.findById(projectId); - - if (!project) throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` }); - if (project.version < 2) throw new BadRequestError({ message: `Failed to add group to E2EE project` }); - - const { permission, membership } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Create, ProjectPermissionSub.Groups); - - let group: TGroups | null = null; - if (isUuidV4(groupIdOrName)) { - group = await groupDAL.findOne({ orgId: actorOrgId, id: groupIdOrName }); - } - if (!group) { - group = await groupDAL.findOne({ orgId: actorOrgId, name: groupIdOrName }); - } - - if (!group) throw new NotFoundError({ message: `Failed to find group with ID or name ${groupIdOrName}` }); - - const existingGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id }); - if (existingGroup) - throw new BadRequestError({ - message: `Group with ID ${group.id} already exists in project with id ${project.id}` - }); - - for await (const { role: requestedRoleChange } of roles) { - const { permission: rolePermission } = await permissionService.getProjectPermissionByRole( - requestedRoleChange, - project.id - ); - - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionGroupActions.GrantPrivileges, - ProjectPermissionSub.Groups, - permission, - rolePermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to assign group to role", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionGroupActions.GrantPrivileges, - ProjectPermissionSub.Groups - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - } - - // validate custom roles input - const customInputRoles = roles.filter( - ({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole) - ); - const hasCustomRole = Boolean(customInputRoles.length); - const customRoles = hasCustomRole - ? await projectRoleDAL.find({ - projectId: project.id, - $in: { slug: customInputRoles.map(({ role }) => role) } - }) - : []; - - if (customRoles.length !== customInputRoles.length) { - const customRoleSlugs = customRoles.map((customRole) => customRole.slug); - const missingInputRoles = customInputRoles - .filter((inputRole) => !customRoleSlugs.includes(inputRole.role)) - .map((role) => role.role); - - throw new NotFoundError({ - message: `Custom role/s not found: ${missingInputRoles.join(", ")}` - }); - } - const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); - - const projectGroup = await groupProjectDAL.transaction(async (tx) => { - const groupProjectMembership = await groupProjectDAL.create( - { - groupId: group!.id, - projectId: project.id - }, - tx - ); - - const sanitizedProjectMembershipRoles = roles.map((inputRole) => { - const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]); - if (!inputRole.isTemporary) { - return { - projectMembershipId: groupProjectMembership.id, - role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role, - customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null - }; - } - - // check cron or relative here later for now its just relative - const relativeTimeInMs = ms(inputRole.temporaryRange); - return { - projectMembershipId: groupProjectMembership.id, - role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role, - customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null, - isTemporary: true, - temporaryMode: ProjectUserMembershipTemporaryMode.Relative, - temporaryRange: inputRole.temporaryRange, - temporaryAccessStartTime: new Date(inputRole.temporaryAccessStartTime), - temporaryAccessEndTime: new Date(new Date(inputRole.temporaryAccessStartTime).getTime() + relativeTimeInMs) - }; - }); - - await groupProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); - - // share project key with users in group that have not - // individually been added to the project and that are not part of - // other groups that are in the project - const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group!.id, project.id, tx); - - if (groupMembers.length && (project.version === ProjectVersion.V1 || project.version === ProjectVersion.V2)) { - const ghostUser = await projectDAL.findProjectGhostUser(project.id, tx); - - if (!ghostUser) { - throw new NotFoundError({ - message: `Failed to find project owner of project with name ${project.name}` - }); - } - - const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, project.id, tx); - - if (!ghostUserLatestKey) { - throw new NotFoundError({ - message: `Failed to find project owner's latest key in project with name ${project.name}` - }); - } - - if (!ghostUserLatestKey.sender.publicKey) { - throw new NotFoundError({ - message: `Failed to find project owner's latest key in project with name ${project.name}` - }); - } - - const bot = await projectBotDAL.findOne({ projectId: project.id }, tx); - - if (!bot) { - throw new NotFoundError({ - message: `Failed to find project bot in project with name ${project.name}` - }); - } - - const botPrivateKey = crypto - .encryption() - .symmetric() - .decryptWithRootEncryptionKey({ - keyEncoding: bot.keyEncoding as SecretKeyEncoding, - iv: bot.iv, - tag: bot.tag, - ciphertext: bot.encryptedPrivateKey - }); - - const plaintextProjectKey = crypto.encryption().asymmetric().decrypt({ - ciphertext: ghostUserLatestKey.encryptedKey, - nonce: ghostUserLatestKey.nonce, - publicKey: ghostUserLatestKey.sender.publicKey, - privateKey: botPrivateKey - }); - - const projectKeyData = groupMembers.map(({ user: { publicKey, id } }) => { - if (!publicKey) { - throw new NotFoundError({ - message: `Failed to find user's public key in project with name ${project.name}` - }); - } - - const { ciphertext: encryptedKey, nonce } = crypto - .encryption() - .asymmetric() - .encrypt(plaintextProjectKey, publicKey, botPrivateKey); - - return { - encryptedKey, - nonce, - senderId: ghostUser.id, - receiverId: id, - projectId: project.id - }; - }); - - await projectKeyDAL.insertMany(projectKeyData, tx); - } - - return groupProjectMembership; - }); - - await permissionService.invalidateProjectPermissionCache(projectId); - - return projectGroup; - }; - - const updateGroupInProject = async ({ - projectId, - groupId, - roles, - actor, - actorId, - actorAuthMethod, - actorOrgId - }: TUpdateProjectGroupDTO) => { - const project = await projectDAL.findById(projectId); - - if (!project) throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` }); - - const { permission, membership } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Edit, ProjectPermissionSub.Groups); - - const group = await groupDAL.findOne({ orgId: actorOrgId, id: groupId }); - if (!group) throw new NotFoundError({ message: `Failed to find group with ID ${groupId}` }); - - const projectGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id }); - if (!projectGroup) throw new NotFoundError({ message: `Failed to find group with ID ${groupId}` }); - - for await (const { role: requestedRoleChange } of roles) { - const { permission: rolePermission } = await permissionService.getProjectPermissionByRole( - requestedRoleChange, - project.id - ); - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionGroupActions.GrantPrivileges, - ProjectPermissionSub.Groups, - permission, - rolePermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to assign group to role", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionGroupActions.GrantPrivileges, - ProjectPermissionSub.Groups - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - } - - // validate custom roles input - const customInputRoles = roles.filter( - ({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole) - ); - const hasCustomRole = Boolean(customInputRoles.length); - const customRoles = hasCustomRole - ? await projectRoleDAL.find({ - projectId: project.id, - $in: { slug: customInputRoles.map(({ role }) => role) } - }) - : []; - if (customRoles.length !== customInputRoles.length) { - const customRoleSlugs = customRoles.map((customRole) => customRole.slug); - const missingInputRoles = customInputRoles - .filter((inputRole) => !customRoleSlugs.includes(inputRole.role)) - .map((role) => role.role); - - throw new NotFoundError({ - message: `Custom role/s not found: ${missingInputRoles.join(", ")}` - }); - } - - const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); - - const sanitizedProjectMembershipRoles = roles.map((inputRole) => { - const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]); - if (!inputRole.isTemporary) { - return { - projectMembershipId: projectGroup.id, - role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role, - customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null - }; - } - - // check cron or relative here later for now its just relative - const relativeTimeInMs = ms(inputRole.temporaryRange); - return { - projectMembershipId: projectGroup.id, - role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role, - customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null, - isTemporary: true, - temporaryMode: ProjectUserMembershipTemporaryMode.Relative, - temporaryRange: inputRole.temporaryRange, - temporaryAccessStartTime: new Date(inputRole.temporaryAccessStartTime), - temporaryAccessEndTime: new Date(new Date(inputRole.temporaryAccessStartTime).getTime() + relativeTimeInMs) - }; - }); - - const updatedRoles = await groupProjectMembershipRoleDAL.transaction(async (tx) => { - await groupProjectMembershipRoleDAL.delete({ projectMembershipId: projectGroup.id }, tx); - return groupProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); - }); - - await permissionService.invalidateProjectPermissionCache(projectId); - - return updatedRoles; - }; - - const removeGroupFromProject = async ({ - projectId, - groupId, - actorId, - actor, - actorOrgId, - actorAuthMethod - }: TDeleteProjectGroupDTO) => { - const project = await projectDAL.findById(projectId); - - if (!project) throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` }); - - const group = await groupDAL.findOne({ orgId: actorOrgId, id: groupId }); - if (!group) throw new NotFoundError({ message: `Failed to find group with ID ${groupId}` }); - - const groupProjectMembership = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id }); - if (!groupProjectMembership) throw new NotFoundError({ message: `Failed to find group with ID ${groupId}` }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Delete, ProjectPermissionSub.Groups); - - const deletedProjectGroup = await groupProjectDAL.transaction(async (tx) => { - const groupMembersNotInProject = await userGroupMembershipDAL.findGroupMembersNotInProject( - group.id, - project.id, - tx - ); - - if (groupMembersNotInProject.length) { - await projectKeyDAL.delete( - { - projectId: project.id, - $in: { - receiverId: groupMembersNotInProject.map(({ user: { id } }) => id) - } - }, - tx - ); - } - - const [projectGroup] = await groupProjectDAL.delete({ groupId: group.id, projectId: project.id }, tx); - return projectGroup; - }); - - await permissionService.invalidateProjectPermissionCache(projectId); - - return deletedProjectGroup; - }; - - const listGroupsInProject = async ({ - projectId, - actor, - actorId, - actorAuthMethod, - actorOrgId - }: TListProjectGroupDTO) => { - const project = await projectDAL.findById(projectId); - - if (!project) { - throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` }); - } - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups); - - const groupMemberships = await groupProjectDAL.findByProjectId(project.id); - return groupMemberships; - }; - - const getGroupInProject = async ({ - actor, - actorId, - actorAuthMethod, - actorOrgId, - groupId, - projectId - }: TGetGroupInProjectDTO) => { - const project = await projectDAL.findById(projectId); - - if (!project) { - throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` }); - } - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups); - - const [groupMembership] = await groupProjectDAL.findByProjectId(project.id, { - groupId - }); - - if (!groupMembership) { - throw new NotFoundError({ - message: `Group membership with ID ${groupId} not found in project with ID ${projectId}` - }); - } - - return groupMembership; - }; - const listProjectGroupUsers = async ({ id, projectId, @@ -542,11 +65,6 @@ export const groupProjectServiceFactory = ({ }; return { - addGroupToProject, - updateGroupInProject, - removeGroupFromProject, - listGroupsInProject, - getGroupInProject, listProjectGroupUsers }; }; diff --git a/backend/src/services/membership-group/membership-group-service.ts b/backend/src/services/membership-group/membership-group-service.ts index ac3bd708a..ba32d9950 100644 --- a/backend/src/services/membership-group/membership-group-service.ts +++ b/backend/src/services/membership-group/membership-group-service.ts @@ -217,8 +217,8 @@ export const membershipGroupServiceFactory = ({ }, tx ); - await membershipRoleDAL.insertMany(roleDocs, tx); - return doc; + const roles = await membershipRoleDAL.insertMany(roleDocs, tx); + return { ...doc, roles }; }); return { membership: membershipDoc }; @@ -276,7 +276,7 @@ export const membershipGroupServiceFactory = ({ : undefined } }); - return memberships; + return { memberships: memberships.data, totalCount: memberships.totalCount }; }; const getMembershipByGroupId = async (dto: TGetMembershipGroupByGroupIdDTO) => { @@ -290,7 +290,7 @@ export const membershipGroupServiceFactory = ({ }); if (!membership) throw new NotFoundError({ message: `Group membership not found` }); - return membership; + return { membership }; }; return { diff --git a/backend/src/services/membership-group/membership-group-types.ts b/backend/src/services/membership-group/membership-group-types.ts index 8b2c852cf..19c374458 100644 --- a/backend/src/services/membership-group/membership-group-types.ts +++ b/backend/src/services/membership-group/membership-group-types.ts @@ -54,14 +54,11 @@ export type TUpdateMembershipGroupDTO = { export type TListMembershipGroupDTO = { permission: OrgServiceActor; scopeData: AccessScopeData; - selector: { - groupId: string; - }; data: { limit?: number; offset?: number; groupName?: string; - roles: string[]; + roles?: string[]; }; }; diff --git a/backend/src/services/membership-group/project/project-membership-group-factory.ts b/backend/src/services/membership-group/project/project-membership-group-factory.ts index 6900e3755..54394a697 100644 --- a/backend/src/services/membership-group/project/project-membership-group-factory.ts +++ b/backend/src/services/membership-group/project/project-membership-group-factory.ts @@ -23,6 +23,7 @@ type TProjectMembershipGroupScopeFactoryDep = { membershipGroupDAL: Pick; }; +// TODO(simp): check for group in org export const newProjectMembershipGroupFactory = ({ permissionService, orgDAL, diff --git a/backend/src/services/org/org-fns.ts b/backend/src/services/org/org-fns.ts index 30e67cca4..fd998d43b 100644 --- a/backend/src/services/org/org-fns.ts +++ b/backend/src/services/org/org-fns.ts @@ -1,20 +1,22 @@ +import { AccessScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal"; 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 { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; + type TDeleteOrgMembership = { orgMembershipId: string; orgId: string; orgDAL: Pick; - projectMembershipDAL: Pick; + projectMembershipDAL: Pick; + membershipUserDAL: Pick; projectKeyDAL: Pick; userAliasDAL: Pick; licenseService: Pick; - projectUserAdditionalPrivilegeDAL: Pick; userId?: string; }; @@ -22,11 +24,11 @@ type TDeleteOrgMemberships = { orgMembershipIds: string[]; orgId: string; orgDAL: Pick; - projectMembershipDAL: Pick; + projectMembershipDAL: Pick; + membershipUserDAL: Pick; projectKeyDAL: Pick; userAliasDAL: Pick; licenseService: Pick; - projectUserAdditionalPrivilegeDAL: Pick; userId?: string; }; @@ -35,46 +37,41 @@ export const deleteOrgMembershipFn = async ({ orgId, orgDAL, projectMembershipDAL, - projectUserAdditionalPrivilegeDAL, projectKeyDAL, userAliasDAL, licenseService, - userId + userId, + membershipUserDAL }: TDeleteOrgMembership) => { const deletedMembership = await orgDAL.transaction(async (tx) => { const orgMembership = await orgDAL.deleteMembershipById(orgMembershipId, orgId, tx); - if (userId && orgMembership.userId === userId) { + 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" }); } - if (!orgMembership.userId) { + const deletedUserId = orgMembership.actorUserId; + if (!deletedUserId) { await licenseService.updateSubscriptionOrgMemberCount(orgId); return orgMembership; } await userAliasDAL.delete( { - userId: orgMembership.userId, + userId: deletedUserId, orgId }, tx ); - await projectUserAdditionalPrivilegeDAL.delete( - { - userId: orgMembership.userId - }, - tx - ); - // Get all the project memberships of the user in the organization - const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserId(orgId, orgMembership.userId); + const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserId(orgId, deletedUserId); // Delete all the project memberships of the user in the organization - await projectMembershipDAL.delete( + await membershipUserDAL.delete( { + scope: AccessScope.Project, $in: { id: projectMemberships.map((membership) => membership.id) } @@ -87,7 +84,7 @@ export const deleteOrgMembershipFn = async ({ $in: { projectId: projectMemberships.map((membership) => membership.projectId) }, - receiverId: orgMembership.userId + receiverId: deletedUserId }); // Delete all the project keys of the user in the organization @@ -112,18 +109,18 @@ export const deleteOrgMembershipsFn = async ({ orgId, orgDAL, projectMembershipDAL, - projectUserAdditionalPrivilegeDAL, projectKeyDAL, userAliasDAL, licenseService, - userId + userId, + membershipUserDAL }: TDeleteOrgMemberships) => { const deletedMemberships = await orgDAL.transaction(async (tx) => { const orgMemberships = await orgDAL.deleteMembershipsById(orgMembershipIds, orgId, tx); const membershipUserIds = orgMemberships - .filter((member) => Boolean(member.userId)) - .map((member) => member.userId) as string[]; + .filter((member) => Boolean(member.actorUserId)) + .map((member) => member.actorUserId) as string[]; if (userId && membershipUserIds.includes(userId)) { // scott: this is temporary, we will add a leave org endpoint with proper handling to ensure org isn't abandoned/broken @@ -145,21 +142,13 @@ export const deleteOrgMembershipsFn = async ({ tx ); - await projectUserAdditionalPrivilegeDAL.delete( - { - $in: { - userId: membershipUserIds - } - }, - tx - ); - // 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 projectMembershipDAL.delete( + await membershipUserDAL.delete( { + scope: AccessScope.Project, $in: { id: projectMemberships.map((membership) => membership.id) } diff --git a/backend/src/services/project-membership/project-user-membership-role-dal.ts b/backend/src/services/project-membership/project-user-membership-role-dal.ts deleted file mode 100644 index b1cb55b9b..000000000 --- a/backend/src/services/project-membership/project-user-membership-role-dal.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; - -export type TProjectUserMembershipRoleDALFactory = ReturnType; - -export const projectUserMembershipRoleDALFactory = (db: TDbClient) => { - const orm = ormify(db, TableName.ProjectUserMembershipRole); - return orm; -};