From 4eee40c2a3489e32d339db0c25ebb2b26991faf7 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Tue, 23 Sep 2025 17:15:54 -0300 Subject: [PATCH] Add project permission cache --- .../src/ee/services/group/group-service.ts | 39 ++++++- ...project-additional-privilege-v2-service.ts | 34 +++++- ...ty-project-additional-privilege-service.ts | 36 ++++++- .../permission/permission-service-types.ts | 3 + .../services/permission/permission-service.ts | 102 +++++++++++++++--- ...oject-user-additional-privilege-service.ts | 34 +++++- backend/src/keystore/keystore.ts | 13 ++- backend/src/server/routes/index.ts | 3 +- .../group-project/group-project-service.ts | 41 +++++-- .../identity-project-service.ts | 25 ++++- .../project-membership-service.ts | 36 ++++++- .../project-role/project-role-service.ts | 9 +- 12 files changed, 345 insertions(+), 30 deletions(-) diff --git a/backend/src/ee/services/group/group-service.ts b/backend/src/ee/services/group/group-service.ts index 46c9831b0..792c2824d 100644 --- a/backend/src/ee/services/group/group-service.ts +++ b/backend/src/ee/services/group/group-service.ts @@ -44,7 +44,13 @@ type TGroupServiceFactoryDep = { projectDAL: Pick; projectBotDAL: Pick; projectKeyDAL: Pick; - permissionService: Pick; + permissionService: Pick< + TPermissionServiceFactory, + | "getOrgPermission" + | "getOrgPermissionByRole" + | "invalidateUserProjectPermissionCache" + | "invalidateProjectPermissionCache" + >; licenseService: Pick; oidcConfigDAL: Pick; }; @@ -225,6 +231,17 @@ export const groupServiceFactory = ({ return updated; }); + if (role) { + const groupMembers = await userGroupMembershipDAL.find({ groupId: group.id }); + const groupProjects = await groupProjectDAL.find({ groupId: group.id }); + await Promise.allSettled([ + ...groupMembers.map((member) => permissionService.invalidateUserProjectPermissionCache(member.userId)), + ...groupProjects.map((groupProject) => + permissionService.invalidateProjectPermissionCache(groupProject.projectId) + ) + ]); + } + return updatedGroup; }; @@ -247,11 +264,19 @@ export const groupServiceFactory = ({ message: "Failed to delete group due to plan restriction. Upgrade plan to delete group." }); + const groupMembers = await userGroupMembershipDAL.find({ groupId: id }); + const groupProjects = await groupProjectDAL.find({ groupId: id }); + const [group] = await groupDAL.delete({ id, orgId: actorOrgId }); + await Promise.allSettled([ + ...groupMembers.map((member) => permissionService.invalidateUserProjectPermissionCache(member.userId)), + ...groupProjects.map((groupProject) => permissionService.invalidateProjectPermissionCache(groupProject.projectId)) + ]); + return group; }; @@ -398,6 +423,12 @@ export const groupServiceFactory = ({ projectBotDAL }); + const groupProjects = await groupProjectDAL.find({ groupId: group.id }); + await Promise.allSettled([ + permissionService.invalidateUserProjectPermissionCache(user.id), + ...groupProjects.map((groupProject) => permissionService.invalidateProjectPermissionCache(groupProject.projectId)) + ]); + return users[0]; }; @@ -479,6 +510,12 @@ export const groupServiceFactory = ({ projectKeyDAL }); + const groupProjects = await groupProjectDAL.find({ groupId: group.id }); + await Promise.allSettled([ + permissionService.invalidateUserProjectPermissionCache(user.id), + ...groupProjects.map((groupProject) => permissionService.invalidateProjectPermissionCache(groupProject.projectId)) + ]); + return users[0]; }; 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 index 64da588f8..8f8c05c60 100644 --- 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 @@ -28,7 +28,10 @@ type TIdentityProjectAdditionalPrivilegeV2ServiceFactoryDep = { identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeV2DALFactory; identityProjectDAL: Pick; projectDAL: Pick; - permissionService: Pick; + permissionService: Pick< + TPermissionServiceFactory, + "getProjectPermission" | "invalidateProjectPermissionCache" | "invalidateIdentityProjectPermissionCache" + >; }; export type TIdentityProjectAdditionalPrivilegeV2ServiceFactory = ReturnType< @@ -115,6 +118,11 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ permissions: packedPermission }); + await Promise.allSettled([ + permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId), + permissionService.invalidateIdentityProjectPermissionCache(identityProjectMembership.identityId) + ]); + return { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) @@ -132,6 +140,12 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) }); + + await Promise.allSettled([ + permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId), + permissionService.invalidateIdentityProjectPermissionCache(identityProjectMembership.identityId) + ]); + return { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) @@ -224,6 +238,12 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) }); + + await Promise.allSettled([ + permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId), + permissionService.invalidateIdentityProjectPermissionCache(identityProjectMembership.identityId) + ]); + return { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) @@ -239,6 +259,12 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ temporaryRange: null, temporaryMode: null }); + + await Promise.allSettled([ + permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId), + permissionService.invalidateIdentityProjectPermissionCache(identityProjectMembership.identityId) + ]); + return { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) @@ -294,6 +320,12 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ }); const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id); + + await Promise.allSettled([ + permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId), + permissionService.invalidateIdentityProjectPermissionCache(identityProjectMembership.identityId) + ]); + return { ...deletedPrivilege, permissions: unpackPermissions(deletedPrivilege.permissions) 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 index 828cf43a3..7a8f1a047 100644 --- 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 @@ -31,7 +31,10 @@ type TIdentityProjectAdditionalPrivilegeServiceFactoryDep = { identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeDALFactory; identityProjectDAL: Pick; projectDAL: Pick; - permissionService: Pick; + permissionService: Pick< + TPermissionServiceFactory, + "getProjectPermission" | "invalidateProjectPermissionCache" | "invalidateIdentityProjectPermissionCache" + >; }; export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType< @@ -129,6 +132,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ slug, permissions: packedPermission }); + + await Promise.allSettled([ + permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId), + permissionService.invalidateIdentityProjectPermissionCache(identityId) + ]); + return { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) @@ -146,6 +155,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) }); + + await Promise.allSettled([ + permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId), + permissionService.invalidateIdentityProjectPermissionCache(identityId) + ]); + return { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) @@ -250,6 +265,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) }); + + await Promise.allSettled([ + permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId), + permissionService.invalidateIdentityProjectPermissionCache(identityId) + ]); + return { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) @@ -265,6 +286,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ temporaryRange: null, temporaryMode: null }); + + await Promise.allSettled([ + permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId), + permissionService.invalidateIdentityProjectPermissionCache(identityId) + ]); + return { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) @@ -338,9 +365,14 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ } const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id); + + await Promise.allSettled([ + permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId), + permissionService.invalidateIdentityProjectPermissionCache(identityId) + ]); + return { ...deletedPrivilege, - permissions: unpackPermissions(deletedPrivilege.permissions) }; }; diff --git a/backend/src/ee/services/permission/permission-service-types.ts b/backend/src/ee/services/permission/permission-service-types.ts index 5e71c65d9..e56851061 100644 --- a/backend/src/ee/services/permission/permission-service-types.ts +++ b/backend/src/ee/services/permission/permission-service-types.ts @@ -283,4 +283,7 @@ export type TPermissionServiceFactory = { projectId: string; checkPermissions: ProjectPermissionSet; }) => Promise; + invalidateProjectPermissionCache: (projectId: string) => Promise; + invalidateUserProjectPermissionCache: (userId: string) => Promise; + invalidateIdentityProjectPermissionCache: (identityId: string) => Promise; }; diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index 461ceef46..993da1978 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -20,6 +20,7 @@ import { projectViewerPermission, sshHostBootstrapPermissions } from "@app/ee/services/permission/default-roles"; +import { KeyStorePrefixes, KeyStoreTtls, TKeyStoreFactory } from "@app/keystore/keystore"; import { conditionsMatcher } from "@app/lib/casl"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { objectify } from "@app/lib/fn"; @@ -49,6 +50,7 @@ type TPermissionServiceFactoryDep = { serviceTokenDAL: Pick; projectDAL: Pick; permissionDAL: TPermissionDALFactory; + keyStore: TKeyStoreFactory; }; export const permissionServiceFactory = ({ @@ -56,7 +58,8 @@ export const permissionServiceFactory = ({ orgRoleDAL, projectRoleDAL, serviceTokenDAL, - projectDAL + projectDAL, + keyStore }: TPermissionServiceFactoryDep): TPermissionServiceFactory => { const buildOrgPermission = (orgUserRoles: TBuildOrgPermissionDTO) => { const rules = orgUserRoles @@ -83,6 +86,21 @@ export const permissionServiceFactory = ({ }); }; + const invalidateProjectPermissionCache = async (projectId: string) => { + const pattern = KeyStorePrefixes.ProjectPermissionPattern(projectId); + await keyStore.deleteItems({ pattern }); + }; + + const invalidateUserProjectPermissionCache = async (userId: string) => { + const pattern = KeyStorePrefixes.UserProjectPermissionPattern(userId); + await keyStore.deleteItems({ pattern }); + }; + + const invalidateIdentityProjectPermissionCache = async (identityId: string) => { + const pattern = KeyStorePrefixes.IdentityProjectPermissionPattern(identityId); + await keyStore.deleteItems({ pattern }); + }; + const buildProjectPermissionRules = (projectUserRoles: TBuildProjectPermissionDTO) => { const rules = projectUserRoles .map(({ role, permissions }) => { @@ -577,35 +595,90 @@ export const permissionServiceFactory = ({ actorId = assumedPrivilegeDetailsCtx.actorId; } + const cacheKey = KeyStorePrefixes.ProjectPermission( + projectId, + actor, + actorId, + actionProjectType || ActionProjectType.Any + ); + + if (actor === ActorType.SERVICE) { + return getServiceTokenProjectPermission({ + serviceTokenId: actorId, + projectId, + actorOrgId, + actionProjectType + }) as Promise>; + } + + try { + const cachedData = await keyStore.getItem(cacheKey); + if (cachedData) { + const parsed = JSON.parse(cachedData) as { + rules: RawRuleOf>[]; + membership: { + roles?: Array<{ role: string; customRoleSlug?: string }>; + [key: string]: unknown; + }; + }; + // Reconstruct the permission object from cached rules + const permission = createMongoAbility(parsed.rules, { + conditionsMatcher + }); + + return { + permission, + membership: parsed.membership, + hasRole: (role: string) => + parsed.membership.roles?.findIndex( + ({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug + ) !== -1 + } as TProjectPermissionRT; + } + } catch (error) {} + + let result: TProjectPermissionRT; + switch (actor) { case ActorType.USER: - return getUserProjectPermission({ + result = (await getUserProjectPermission({ userId: actorId, projectId, authMethod: actorAuthMethod, userOrgId: actorOrgId, actionProjectType - }) as Promise>; - case ActorType.SERVICE: - return getServiceTokenProjectPermission({ - serviceTokenId: actorId, - projectId, - actorOrgId, - actionProjectType - }) as Promise>; + })) as TProjectPermissionRT; + break; case ActorType.IDENTITY: - return getIdentityProjectPermission({ + result = (await getIdentityProjectPermission({ identityId: actorId, projectId, identityOrgId: actorOrgId, actionProjectType - }) as Promise>; + })) as TProjectPermissionRT; + break; default: throw new BadRequestError({ message: "Invalid actor provided", name: "Get project permission" }); } + + try { + const cacheData = { + rules: result.permission.rules, + membership: result.membership + }; + + await keyStore.setItemWithExpiry( + cacheKey, + KeyStoreTtls.ProjectPermissionCacheInSeconds, + JSON.stringify(cacheData) + ); + } catch (error) { + } + + return result; }; const getProjectPermissionByRole: TPermissionServiceFactory["getProjectPermissionByRole"] = async ( @@ -668,6 +741,9 @@ export const permissionServiceFactory = ({ getProjectPermissionByRole, buildOrgPermission, buildProjectPermissionRules, - checkGroupProjectPermission + checkGroupProjectPermission, + invalidateProjectPermissionCache, + invalidateUserProjectPermissionCache, + invalidateIdentityProjectPermissionCache }; }; diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts index 944775156..1127ac6b4 100644 --- a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts +++ b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts @@ -27,7 +27,10 @@ import { type TProjectUserAdditionalPrivilegeServiceFactoryDep = { projectUserAdditionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory; projectMembershipDAL: Pick; - permissionService: Pick; + permissionService: Pick< + TPermissionServiceFactory, + "getProjectPermission" | "invalidateUserProjectPermissionCache" | "invalidateProjectPermissionCache" + >; accessApprovalRequestDAL: Pick; }; @@ -115,6 +118,12 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ slug, permissions: packedPermission }); + + await Promise.allSettled([ + permissionService.invalidateProjectPermissionCache(projectMembership.projectId), + permissionService.invalidateUserProjectPermissionCache(projectMembership.userId) + ]); + return { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) @@ -133,6 +142,12 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) }); + + await Promise.allSettled([ + permissionService.invalidateProjectPermissionCache(projectMembership.projectId), + permissionService.invalidateUserProjectPermissionCache(projectMembership.userId) + ]); + return { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) @@ -230,6 +245,11 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) }); + await Promise.allSettled([ + permissionService.invalidateProjectPermissionCache(projectMembership.projectId), + permissionService.invalidateUserProjectPermissionCache(projectMembership.userId) + ]); + return { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) @@ -245,6 +265,12 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ temporaryRange: null, temporaryMode: null }); + + await Promise.allSettled([ + permissionService.invalidateProjectPermissionCache(projectMembership.projectId), + permissionService.invalidateUserProjectPermissionCache(projectMembership.userId) + ]); + return { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) @@ -291,6 +317,12 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ } ); const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id); + + await Promise.allSettled([ + permissionService.invalidateProjectPermissionCache(projectMembership.projectId), + permissionService.invalidateUserProjectPermissionCache(projectMembership.userId) + ]); + return { ...deletedPrivilege, permissions: unpackPermissions(deletedPrivilege.permissions) diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index 04ba8428b..9da4db673 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -63,13 +63,22 @@ export const KeyStorePrefixes = { ActiveSSEConnectionsSet: (projectId: string, identityId: string) => `sse-connections:${projectId}:${identityId}` as const, ActiveSSEConnections: (projectId: string, identityId: string, connectionId: string) => - `sse-connections:${projectId}:${identityId}:${connectionId}` as const + `sse-connections:${projectId}:${identityId}:${connectionId}` as const, + + ProjectPermission: (projectId: string, actorType: string, actorId: string, actionProjectType: string) => + `project-permission:${projectId}:${actorType}:${actorId}:${actionProjectType}` as const, + ProjectPermissionPattern: (projectId: string) => `project-permission:${projectId}:*` as const, + UserProjectPermissionPattern: (userId: string) => `project-permission:*:USER:${userId}:*` as const, + IdentityProjectPermissionPattern: (identityId: string) => `project-permission:*:IDENTITY:${identityId}:*` as const, + GroupMemberProjectPermissionPattern: (projectId: string, groupId: string) => + `group-member-project-permission:${projectId}:${groupId}:*` as const }; export const KeyStoreTtls = { SetSyncSecretIntegrationLastRunTimestampInSeconds: 60, SetSecretSyncLastRunTimestampInSeconds: 60, - AccessTokenStatusUpdateInSeconds: 120 + AccessTokenStatusUpdateInSeconds: 120, + ProjectPermissionCacheInSeconds: 300 // 5 minutes }; type TDeleteItems = { diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 2fb7e9ff7..3171919a9 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -530,7 +530,8 @@ export const registerRoutes = async ( orgRoleDAL, projectRoleDAL, serviceTokenDAL, - projectDAL + projectDAL, + keyStore }); const assumePrivilegeService = assumePrivilegeServiceFactory({ projectDAL, diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts index 838d89437..b7ff13ee0 100644 --- a/backend/src/services/group-project/group-project-service.ts +++ b/backend/src/services/group-project/group-project-service.ts @@ -37,13 +37,19 @@ type TGroupProjectServiceFactoryDep = { TGroupProjectMembershipRoleDALFactory, "create" | "transaction" | "insertMany" | "delete" >; - userGroupMembershipDAL: Pick; + userGroupMembershipDAL: Pick; projectDAL: Pick; projectKeyDAL: Pick; projectRoleDAL: Pick; projectBotDAL: TProjectBotDALFactory; groupDAL: Pick; - permissionService: Pick; + permissionService: Pick< + TPermissionServiceFactory, + | "getProjectPermission" + | "getProjectPermissionByRole" + | "invalidateProjectPermissionCache" + | "invalidateUserProjectPermissionCache" + >; }; export type TGroupProjectServiceFactory = ReturnType; @@ -263,6 +269,12 @@ export const groupProjectServiceFactory = ({ return groupProjectMembership; }); + const groupMembers = await userGroupMembershipDAL.find({ groupId: group.id }); + await Promise.allSettled([ + permissionService.invalidateProjectPermissionCache(projectId), + ...groupMembers.map((member) => permissionService.invalidateUserProjectPermissionCache(member.userId)) + ]); + return projectGroup; }; @@ -372,6 +384,12 @@ export const groupProjectServiceFactory = ({ return groupProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); }); + const groupMembers = await userGroupMembershipDAL.find({ groupId }); + await Promise.allSettled([ + permissionService.invalidateProjectPermissionCache(projectId), + ...groupMembers.map((member) => permissionService.invalidateUserProjectPermissionCache(member.userId)) + ]); + return updatedRoles; }; @@ -403,15 +421,21 @@ export const groupProjectServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Delete, ProjectPermissionSub.Groups); - const deletedProjectGroup = await groupProjectDAL.transaction(async (tx) => { - const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, project.id, tx); + const groupMembers = await userGroupMembershipDAL.find({ groupId: group.id }); - if (groupMembers.length) { + 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: groupMembers.map(({ user: { id } }) => id) + receiverId: groupMembersNotInProject.map(({ user: { id } }) => id) } }, tx @@ -422,6 +446,11 @@ export const groupProjectServiceFactory = ({ return projectGroup; }); + await Promise.allSettled([ + permissionService.invalidateProjectPermissionCache(projectId), + ...groupMembers.map((member) => permissionService.invalidateUserProjectPermissionCache(member.userId)) + ]); + return deletedProjectGroup; }; diff --git a/backend/src/services/identity-project/identity-project-service.ts b/backend/src/services/identity-project/identity-project-service.ts index 4f0964f42..8333f2ed8 100644 --- a/backend/src/services/identity-project/identity-project-service.ts +++ b/backend/src/services/identity-project/identity-project-service.ts @@ -35,7 +35,13 @@ type TIdentityProjectServiceFactoryDep = { projectDAL: Pick; projectRoleDAL: Pick; identityOrgMembershipDAL: Pick; - permissionService: Pick; + permissionService: Pick< + TPermissionServiceFactory, + | "getProjectPermission" + | "getProjectPermissionByRole" + | "invalidateIdentityProjectPermissionCache" + | "invalidateProjectPermissionCache" + >; }; export type TIdentityProjectServiceFactory = ReturnType; @@ -165,6 +171,12 @@ export const identityProjectServiceFactory = ({ const identityRoles = await identityProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); return { ...identityProjectMembership, roles: identityRoles }; }); + + await Promise.allSettled([ + permissionService.invalidateProjectPermissionCache(projectId), + permissionService.invalidateIdentityProjectPermissionCache(identityId) + ]); + return projectIdentity; }; @@ -272,6 +284,11 @@ export const identityProjectServiceFactory = ({ return identityProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); }); + await Promise.allSettled([ + permissionService.invalidateProjectPermissionCache(projectId), + permissionService.invalidateIdentityProjectPermissionCache(identityId) + ]); + return updatedRoles; }; @@ -302,6 +319,12 @@ export const identityProjectServiceFactory = ({ ); const [deletedIdentity] = await identityProjectDAL.delete({ identityId, projectId }); + + await Promise.allSettled([ + permissionService.invalidateProjectPermissionCache(projectId), + permissionService.invalidateIdentityProjectPermissionCache(identityId) + ]); + return deletedIdentity; }; diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts index 991bf65d1..bb291c1cc 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -43,7 +43,13 @@ import { import { TProjectUserMembershipRoleDALFactory } from "./project-user-membership-role-dal"; type TProjectMembershipServiceFactoryDep = { - permissionService: Pick; + permissionService: Pick< + TPermissionServiceFactory, + | "getProjectPermission" + | "getProjectPermissionByRole" + | "invalidateUserProjectPermissionCache" + | "invalidateProjectPermissionCache" + >; smtpService: TSmtpService; projectBotDAL: TProjectBotDALFactory; projectMembershipDAL: TProjectMembershipDALFactory; @@ -239,6 +245,13 @@ export const projectMembershipServiceFactory = ({ ); }); + await Promise.allSettled([ + permissionService.invalidateProjectPermissionCache(projectId), + ...orgMembers.map(({ userId }) => + userId ? permissionService.invalidateUserProjectPermissionCache(userId) : Promise.resolve() + ) + ]); + if (sendEmails) { await notificationService.createUserNotifications( orgMembers.map((member) => ({ @@ -371,6 +384,13 @@ export const projectMembershipServiceFactory = ({ return projectUserMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); }); + await Promise.allSettled([ + permissionService.invalidateProjectPermissionCache(projectId), + membershipUser?.userId + ? permissionService.invalidateUserProjectPermissionCache(membershipUser.userId) + : Promise.resolve() + ]); + return updatedRoles; }; @@ -414,6 +434,12 @@ export const projectMembershipServiceFactory = ({ ); return deletedMembership; }); + + await Promise.allSettled([ + permissionService.invalidateProjectPermissionCache(projectId), + membership.userId ? permissionService.invalidateUserProjectPermissionCache(membership.userId) : Promise.resolve() + ]); + return membership; }; @@ -515,6 +541,14 @@ export const projectMembershipServiceFactory = ({ return deletedMemberships; }); + + await Promise.allSettled([ + permissionService.invalidateProjectPermissionCache(projectId), + ...memberships.map(({ userId }) => + userId ? permissionService.invalidateUserProjectPermissionCache(userId) : Promise.resolve() + ) + ]); + return memberships; }; diff --git a/backend/src/services/project-role/project-role-service.ts b/backend/src/services/project-role/project-role-service.ts index dd0eecc68..f30da21f5 100644 --- a/backend/src/services/project-role/project-role-service.ts +++ b/backend/src/services/project-role/project-role-service.ts @@ -35,7 +35,10 @@ type TProjectRoleServiceFactoryDep = { identityDAL: Pick; userDAL: Pick; projectDAL: Pick; - permissionService: Pick; + permissionService: Pick< + TPermissionServiceFactory, + "getProjectPermission" | "getUserProjectPermission" | "invalidateProjectPermissionCache" + >; identityProjectMembershipRoleDAL: TIdentityProjectMembershipRoleDALFactory; projectUserMembershipRoleDAL: TProjectUserMembershipRoleDALFactory; }; @@ -162,6 +165,8 @@ export const projectRoleServiceFactory = ({ }); if (!updatedRole) throw new NotFoundError({ message: "Project role not found", name: "Update role" }); + await permissionService.invalidateProjectPermissionCache(projectRole.projectId); + return { ...updatedRole, permissions: unpackPermissions(updatedRole.permissions) }; }; @@ -197,6 +202,8 @@ export const projectRoleServiceFactory = ({ const deletedRole = await projectRoleDAL.deleteById(roleId); if (!deletedRole) throw new NotFoundError({ message: "Project role not found", name: "Delete role" }); + await permissionService.invalidateProjectPermissionCache(projectRole.projectId); + return { ...deletedRole, permissions: unpackPermissions(deletedRole.permissions) }; };