From 4eee40c2a3489e32d339db0c25ebb2b26991faf7 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Tue, 23 Sep 2025 17:15:54 -0300 Subject: [PATCH 1/4] 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) }; }; From 98896d41181d56d63a5a14458521fedec2309c32 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Tue, 23 Sep 2025 20:49:48 -0300 Subject: [PATCH 2/4] Fix lint errors --- backend/src/ee/services/permission/permission-service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index 993da1978..a2581779f 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -24,6 +24,7 @@ import { KeyStorePrefixes, KeyStoreTtls, TKeyStoreFactory } from "@app/keystore/ import { conditionsMatcher } from "@app/lib/casl"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { objectify } from "@app/lib/fn"; +import { logger } from "@app/lib/logger"; import { ActorType } from "@app/services/auth/auth-type"; import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; @@ -635,7 +636,9 @@ export const permissionServiceFactory = ({ ) !== -1 } as TProjectPermissionRT; } - } catch (error) {} + } catch (error) { + logger.error(error, "Failed to get project permission"); + } let result: TProjectPermissionRT; @@ -676,6 +679,7 @@ export const permissionServiceFactory = ({ JSON.stringify(cacheData) ); } catch (error) { + logger.error(error, "Failed to cache project permission"); } return result; From a0dc051f6b3dde6068357fe27c6c61eb4e847561 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Fri, 26 Sep 2025 11:12:46 -0300 Subject: [PATCH 3/4] Add temporal permissions cache handling --- .../permission/permission-service-types.ts | 3 +- .../services/permission/permission-service.ts | 91 +++++++++++++++---- backend/src/keystore/keystore.ts | 18 ++-- 3 files changed, 89 insertions(+), 23 deletions(-) diff --git a/backend/src/ee/services/permission/permission-service-types.ts b/backend/src/ee/services/permission/permission-service-types.ts index e56851061..9e5d9700b 100644 --- a/backend/src/ee/services/permission/permission-service-types.ts +++ b/backend/src/ee/services/permission/permission-service-types.ts @@ -1,5 +1,6 @@ import { MongoAbility, RawRuleOf } from "@casl/ability"; import { MongoQuery } from "@ucast/mongo2js"; +import { Knex } from "knex"; import { ActionProjectType } from "@app/db/schemas"; import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; @@ -283,7 +284,7 @@ export type TPermissionServiceFactory = { projectId: string; checkPermissions: ProjectPermissionSet; }) => Promise; - invalidateProjectPermissionCache: (projectId: string) => Promise; + invalidateProjectPermissionCache: (projectId: string, tx?: Knex) => 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 a2581779f..b428acce5 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -3,6 +3,7 @@ import { PackRule, unpackRules } from "@casl/ability/extra"; import { requestContext } from "@fastify/request-context"; import { MongoQuery } from "@ucast/mongo2js"; import handlebars from "handlebars"; +import { Knex } from "knex"; import { ActionProjectType, @@ -87,9 +88,13 @@ export const permissionServiceFactory = ({ }); }; - const invalidateProjectPermissionCache = async (projectId: string) => { - const pattern = KeyStorePrefixes.ProjectPermissionPattern(projectId); - await keyStore.deleteItems({ pattern }); + const invalidateProjectPermissionCache = async (projectId: string, tx?: Knex) => { + const projectPermissionDalVersionKey = KeyStorePrefixes.ProjectPermissionDalVersion(projectId); + await keyStore.pgIncrementBy(projectPermissionDalVersionKey, { + incr: 1, + tx, + expiry: KeyStoreTtls.ProjectPermissionDalVersionTtl + }); }; const invalidateUserProjectPermissionCache = async (userId: string) => { @@ -102,6 +107,59 @@ export const permissionServiceFactory = ({ await keyStore.deleteItems({ pattern }); }; + const calculateProjectPermissionTtl = (membership: unknown): number => { + const now = new Date(); + let minTtl = KeyStoreTtls.ProjectPermissionCacheInSeconds; + + const getMinEndTime = (items: Array<{ temporaryAccessEndTime?: Date | null; isTemporary?: boolean }>) => { + return items + .filter((item) => item.isTemporary && item.temporaryAccessEndTime) + .map((item) => item.temporaryAccessEndTime!) + .filter((endTime) => endTime > now) + .reduce((min, endTime) => (!min || endTime < min ? endTime : min), null as Date | null); + }; + + const roleTimes: Date[] = []; + const additionalPrivilegeTimes: Date[] = []; + + if ( + membership && + typeof membership === "object" && + "roles" in membership && + Array.isArray((membership as Record).roles) + ) { + const roles = (membership as Record).roles as Array<{ + temporaryAccessEndTime?: Date | null; + isTemporary?: boolean; + }>; + const minRoleEndTime = getMinEndTime(roles); + if (minRoleEndTime) roleTimes.push(minRoleEndTime); + } + + if ( + membership && + typeof membership === "object" && + "additionalPrivileges" in membership && + Array.isArray((membership as Record).additionalPrivileges) + ) { + const additionalPrivileges = (membership as Record).additionalPrivileges as Array<{ + temporaryAccessEndTime?: Date | null; + isTemporary?: boolean; + }>; + const minAdditionalEndTime = getMinEndTime(additionalPrivileges); + if (minAdditionalEndTime) additionalPrivilegeTimes.push(minAdditionalEndTime); + } + + const allEndTimes = [...roleTimes, ...additionalPrivilegeTimes]; + if (allEndTimes.length > 0) { + const nearestEndTime = allEndTimes.reduce((min, endTime) => (!min || endTime < min ? endTime : min)); + const timeUntilExpiry = Math.floor((nearestEndTime.getTime() - now.getTime()) / 1000); + minTtl = Math.min(minTtl, Math.max(1, timeUntilExpiry)); + } + + return minTtl; + }; + const buildProjectPermissionRules = (projectUserRoles: TBuildProjectPermissionDTO) => { const rules = projectUserRoles .map(({ role, permissions }) => { @@ -596,13 +654,6 @@ export const permissionServiceFactory = ({ actorId = assumedPrivilegeDetailsCtx.actorId; } - const cacheKey = KeyStorePrefixes.ProjectPermission( - projectId, - actor, - actorId, - actionProjectType || ActionProjectType.Any - ); - if (actor === ActorType.SERVICE) { return getServiceTokenProjectPermission({ serviceTokenId: actorId, @@ -612,6 +663,18 @@ export const permissionServiceFactory = ({ }) as Promise>; } + const cachedProjectPermissionVersion = await keyStore.pgGetIntItem( + KeyStorePrefixes.ProjectPermissionDalVersion(projectId) + ); + const projectPermissionVersion = Number(cachedProjectPermissionVersion || 0); + const cacheKey = KeyStorePrefixes.ProjectPermission( + projectId, + projectPermissionVersion, + actor, + actorId, + actionProjectType || ActionProjectType.Any + ); + try { const cachedData = await keyStore.getItem(cacheKey); if (cachedData) { @@ -622,7 +685,6 @@ export const permissionServiceFactory = ({ [key: string]: unknown; }; }; - // Reconstruct the permission object from cached rules const permission = createMongoAbility(parsed.rules, { conditionsMatcher }); @@ -673,11 +735,8 @@ export const permissionServiceFactory = ({ membership: result.membership }; - await keyStore.setItemWithExpiry( - cacheKey, - KeyStoreTtls.ProjectPermissionCacheInSeconds, - JSON.stringify(cacheData) - ); + const ttl = calculateProjectPermissionTtl(result.membership); + await keyStore.setItemWithExpiry(cacheKey, ttl, JSON.stringify(cacheData)); } catch (error) { logger.error(error, "Failed to cache project permission"); } diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index 9da4db673..17f4aa493 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -65,11 +65,16 @@ export const KeyStorePrefixes = { ActiveSSEConnections: (projectId: string, identityId: string, connectionId: string) => `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, + ProjectPermission: ( + projectId: string, + version: number, + actorType: string, + actorId: string, + actionProjectType: string + ) => `project-permission:${projectId}:${version}:${actorType}:${actorId}:${actionProjectType}` as const, + ProjectPermissionDalVersion: (projectId: string) => `project-permission:${projectId}:dal-version` 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 }; @@ -78,7 +83,8 @@ export const KeyStoreTtls = { SetSyncSecretIntegrationLastRunTimestampInSeconds: 60, SetSecretSyncLastRunTimestampInSeconds: 60, AccessTokenStatusUpdateInSeconds: 120, - ProjectPermissionCacheInSeconds: 300 // 5 minutes + ProjectPermissionCacheInSeconds: 300, // 5 minutes + ProjectPermissionDalVersionTtl: "15m" // Project permission DAL version TTL }; type TDeleteItems = { From 2374182f75d6d5da1e74c646c430d0e0c815f490 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Fri, 26 Sep 2025 17:50:14 -0300 Subject: [PATCH 4/4] Add access approval requests to project permission cache logic --- .../access-approval-request-service.ts | 4 ++- .../src/ee/services/group/group-service.ts | 11 +------ ...project-additional-privilege-v2-service.ts | 30 ++++-------------- ...ty-project-additional-privilege-service.ts | 30 ++++-------------- .../permission/permission-service-types.ts | 2 -- .../services/permission/permission-service.ts | 14 +-------- ...oject-user-additional-privilege-service.ts | 30 ++++-------------- .../group-project/group-project-service.ts | 24 +++----------- .../identity-project-service.ts | 20 +++--------- .../project-membership-service.ts | 31 +++---------------- 10 files changed, 36 insertions(+), 160 deletions(-) diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index da87978fd..d69c6da79 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -35,7 +35,7 @@ import { ApprovalStatus, TAccessApprovalRequestServiceFactory } from "./access-a type TSecretApprovalRequestServiceFactoryDep = { additionalPrivilegeDAL: Pick; - permissionService: Pick; + permissionService: Pick; accessApprovalPolicyApproverDAL: Pick; projectEnvDAL: Pick; projectDAL: Pick< @@ -758,6 +758,8 @@ export const accessApprovalRequestServiceFactory = ({ { privilegeId: privilegeIdToSet, status: ApprovalStatus.APPROVED }, tx ); + + await permissionService.invalidateProjectPermissionCache(accessApprovalRequest.projectId, tx); } } diff --git a/backend/src/ee/services/group/group-service.ts b/backend/src/ee/services/group/group-service.ts index 792c2824d..d4d30b5ae 100644 --- a/backend/src/ee/services/group/group-service.ts +++ b/backend/src/ee/services/group/group-service.ts @@ -46,10 +46,7 @@ type TGroupServiceFactoryDep = { projectKeyDAL: Pick; permissionService: Pick< TPermissionServiceFactory, - | "getOrgPermission" - | "getOrgPermissionByRole" - | "invalidateUserProjectPermissionCache" - | "invalidateProjectPermissionCache" + "getOrgPermission" | "getOrgPermissionByRole" | "invalidateProjectPermissionCache" >; licenseService: Pick; oidcConfigDAL: Pick; @@ -232,10 +229,8 @@ export const groupServiceFactory = ({ }); 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) ) @@ -264,7 +259,6 @@ 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({ @@ -273,7 +267,6 @@ export const groupServiceFactory = ({ }); await Promise.allSettled([ - ...groupMembers.map((member) => permissionService.invalidateUserProjectPermissionCache(member.userId)), ...groupProjects.map((groupProject) => permissionService.invalidateProjectPermissionCache(groupProject.projectId)) ]); @@ -425,7 +418,6 @@ export const groupServiceFactory = ({ const groupProjects = await groupProjectDAL.find({ groupId: group.id }); await Promise.allSettled([ - permissionService.invalidateUserProjectPermissionCache(user.id), ...groupProjects.map((groupProject) => permissionService.invalidateProjectPermissionCache(groupProject.projectId)) ]); @@ -512,7 +504,6 @@ export const groupServiceFactory = ({ const groupProjects = await groupProjectDAL.find({ groupId: group.id }); await Promise.allSettled([ - permissionService.invalidateUserProjectPermissionCache(user.id), ...groupProjects.map((groupProject) => permissionService.invalidateProjectPermissionCache(groupProject.projectId)) ]); 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 8f8c05c60..27b67367e 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,10 +28,7 @@ type TIdentityProjectAdditionalPrivilegeV2ServiceFactoryDep = { identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeV2DALFactory; identityProjectDAL: Pick; projectDAL: Pick; - permissionService: Pick< - TPermissionServiceFactory, - "getProjectPermission" | "invalidateProjectPermissionCache" | "invalidateIdentityProjectPermissionCache" - >; + permissionService: Pick; }; export type TIdentityProjectAdditionalPrivilegeV2ServiceFactory = ReturnType< @@ -118,10 +115,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ permissions: packedPermission }); - await Promise.allSettled([ - permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId), - permissionService.invalidateIdentityProjectPermissionCache(identityProjectMembership.identityId) - ]); + await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); return { ...additionalPrivilege, @@ -141,10 +135,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) }); - await Promise.allSettled([ - permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId), - permissionService.invalidateIdentityProjectPermissionCache(identityProjectMembership.identityId) - ]); + await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); return { ...additionalPrivilege, @@ -239,10 +230,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) }); - await Promise.allSettled([ - permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId), - permissionService.invalidateIdentityProjectPermissionCache(identityProjectMembership.identityId) - ]); + await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); return { ...additionalPrivilege, @@ -260,10 +248,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ temporaryMode: null }); - await Promise.allSettled([ - permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId), - permissionService.invalidateIdentityProjectPermissionCache(identityProjectMembership.identityId) - ]); + await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); return { ...additionalPrivilege, @@ -321,10 +306,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id); - await Promise.allSettled([ - permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId), - permissionService.invalidateIdentityProjectPermissionCache(identityProjectMembership.identityId) - ]); + await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); return { ...deletedPrivilege, 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 7a8f1a047..ddba76920 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,10 +31,7 @@ type TIdentityProjectAdditionalPrivilegeServiceFactoryDep = { identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeDALFactory; identityProjectDAL: Pick; projectDAL: Pick; - permissionService: Pick< - TPermissionServiceFactory, - "getProjectPermission" | "invalidateProjectPermissionCache" | "invalidateIdentityProjectPermissionCache" - >; + permissionService: Pick; }; export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType< @@ -133,10 +130,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ permissions: packedPermission }); - await Promise.allSettled([ - permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId), - permissionService.invalidateIdentityProjectPermissionCache(identityId) - ]); + await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); return { ...additionalPrivilege, @@ -156,10 +150,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) }); - await Promise.allSettled([ - permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId), - permissionService.invalidateIdentityProjectPermissionCache(identityId) - ]); + await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); return { ...additionalPrivilege, @@ -266,10 +257,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) }); - await Promise.allSettled([ - permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId), - permissionService.invalidateIdentityProjectPermissionCache(identityId) - ]); + await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); return { ...additionalPrivilege, @@ -287,10 +275,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ temporaryMode: null }); - await Promise.allSettled([ - permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId), - permissionService.invalidateIdentityProjectPermissionCache(identityId) - ]); + await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); return { ...additionalPrivilege, @@ -366,10 +351,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id); - await Promise.allSettled([ - permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId), - permissionService.invalidateIdentityProjectPermissionCache(identityId) - ]); + await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); return { ...deletedPrivilege, diff --git a/backend/src/ee/services/permission/permission-service-types.ts b/backend/src/ee/services/permission/permission-service-types.ts index 9e5d9700b..f25b84bd5 100644 --- a/backend/src/ee/services/permission/permission-service-types.ts +++ b/backend/src/ee/services/permission/permission-service-types.ts @@ -285,6 +285,4 @@ export type TPermissionServiceFactory = { checkPermissions: ProjectPermissionSet; }) => Promise; invalidateProjectPermissionCache: (projectId: string, tx?: Knex) => 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 b428acce5..9e94e1415 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -97,16 +97,6 @@ export const permissionServiceFactory = ({ }); }; - 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 calculateProjectPermissionTtl = (membership: unknown): number => { const now = new Date(); let minTtl = KeyStoreTtls.ProjectPermissionCacheInSeconds; @@ -805,8 +795,6 @@ export const permissionServiceFactory = ({ buildOrgPermission, buildProjectPermissionRules, checkGroupProjectPermission, - invalidateProjectPermissionCache, - invalidateUserProjectPermissionCache, - invalidateIdentityProjectPermissionCache + invalidateProjectPermissionCache }; }; 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 1127ac6b4..c2876572f 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,10 +27,7 @@ import { type TProjectUserAdditionalPrivilegeServiceFactoryDep = { projectUserAdditionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory; projectMembershipDAL: Pick; - permissionService: Pick< - TPermissionServiceFactory, - "getProjectPermission" | "invalidateUserProjectPermissionCache" | "invalidateProjectPermissionCache" - >; + permissionService: Pick; accessApprovalRequestDAL: Pick; }; @@ -119,10 +116,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ permissions: packedPermission }); - await Promise.allSettled([ - permissionService.invalidateProjectPermissionCache(projectMembership.projectId), - permissionService.invalidateUserProjectPermissionCache(projectMembership.userId) - ]); + await permissionService.invalidateProjectPermissionCache(projectMembership.projectId); return { ...additionalPrivilege, @@ -143,10 +137,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) }); - await Promise.allSettled([ - permissionService.invalidateProjectPermissionCache(projectMembership.projectId), - permissionService.invalidateUserProjectPermissionCache(projectMembership.userId) - ]); + await permissionService.invalidateProjectPermissionCache(projectMembership.projectId); return { ...additionalPrivilege, @@ -245,10 +236,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) }); - await Promise.allSettled([ - permissionService.invalidateProjectPermissionCache(projectMembership.projectId), - permissionService.invalidateUserProjectPermissionCache(projectMembership.userId) - ]); + await permissionService.invalidateProjectPermissionCache(projectMembership.projectId); return { ...additionalPrivilege, @@ -266,10 +254,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ temporaryMode: null }); - await Promise.allSettled([ - permissionService.invalidateProjectPermissionCache(projectMembership.projectId), - permissionService.invalidateUserProjectPermissionCache(projectMembership.userId) - ]); + await permissionService.invalidateProjectPermissionCache(projectMembership.projectId); return { ...additionalPrivilege, @@ -318,10 +303,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ ); const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id); - await Promise.allSettled([ - permissionService.invalidateProjectPermissionCache(projectMembership.projectId), - permissionService.invalidateUserProjectPermissionCache(projectMembership.userId) - ]); + await permissionService.invalidateProjectPermissionCache(projectMembership.projectId); return { ...deletedPrivilege, diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts index b7ff13ee0..913aca2f3 100644 --- a/backend/src/services/group-project/group-project-service.ts +++ b/backend/src/services/group-project/group-project-service.ts @@ -45,10 +45,7 @@ type TGroupProjectServiceFactoryDep = { groupDAL: Pick; permissionService: Pick< TPermissionServiceFactory, - | "getProjectPermission" - | "getProjectPermissionByRole" - | "invalidateProjectPermissionCache" - | "invalidateUserProjectPermissionCache" + "getProjectPermission" | "getProjectPermissionByRole" | "invalidateProjectPermissionCache" >; }; @@ -269,11 +266,7 @@ 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)) - ]); + await permissionService.invalidateProjectPermissionCache(projectId); return projectGroup; }; @@ -384,11 +377,7 @@ 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)) - ]); + await permissionService.invalidateProjectPermissionCache(projectId); return updatedRoles; }; @@ -421,8 +410,6 @@ export const groupProjectServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Delete, ProjectPermissionSub.Groups); - const groupMembers = await userGroupMembershipDAL.find({ groupId: group.id }); - const deletedProjectGroup = await groupProjectDAL.transaction(async (tx) => { const groupMembersNotInProject = await userGroupMembershipDAL.findGroupMembersNotInProject( group.id, @@ -446,10 +433,7 @@ export const groupProjectServiceFactory = ({ return projectGroup; }); - await Promise.allSettled([ - permissionService.invalidateProjectPermissionCache(projectId), - ...groupMembers.map((member) => permissionService.invalidateUserProjectPermissionCache(member.userId)) - ]); + await permissionService.invalidateProjectPermissionCache(projectId); 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 8333f2ed8..08fd2cf0b 100644 --- a/backend/src/services/identity-project/identity-project-service.ts +++ b/backend/src/services/identity-project/identity-project-service.ts @@ -37,10 +37,7 @@ type TIdentityProjectServiceFactoryDep = { identityOrgMembershipDAL: Pick; permissionService: Pick< TPermissionServiceFactory, - | "getProjectPermission" - | "getProjectPermissionByRole" - | "invalidateIdentityProjectPermissionCache" - | "invalidateProjectPermissionCache" + "getProjectPermission" | "getProjectPermissionByRole" | "invalidateProjectPermissionCache" >; }; @@ -172,10 +169,7 @@ export const identityProjectServiceFactory = ({ return { ...identityProjectMembership, roles: identityRoles }; }); - await Promise.allSettled([ - permissionService.invalidateProjectPermissionCache(projectId), - permissionService.invalidateIdentityProjectPermissionCache(identityId) - ]); + await permissionService.invalidateProjectPermissionCache(projectId); return projectIdentity; }; @@ -284,10 +278,7 @@ export const identityProjectServiceFactory = ({ return identityProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); }); - await Promise.allSettled([ - permissionService.invalidateProjectPermissionCache(projectId), - permissionService.invalidateIdentityProjectPermissionCache(identityId) - ]); + await permissionService.invalidateProjectPermissionCache(projectId); return updatedRoles; }; @@ -320,10 +311,7 @@ export const identityProjectServiceFactory = ({ const [deletedIdentity] = await identityProjectDAL.delete({ identityId, projectId }); - await Promise.allSettled([ - permissionService.invalidateProjectPermissionCache(projectId), - permissionService.invalidateIdentityProjectPermissionCache(identityId) - ]); + await permissionService.invalidateProjectPermissionCache(projectId); 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 bb291c1cc..301ce1bc0 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -45,10 +45,7 @@ import { TProjectUserMembershipRoleDALFactory } from "./project-user-membership- type TProjectMembershipServiceFactoryDep = { permissionService: Pick< TPermissionServiceFactory, - | "getProjectPermission" - | "getProjectPermissionByRole" - | "invalidateUserProjectPermissionCache" - | "invalidateProjectPermissionCache" + "getProjectPermission" | "getProjectPermissionByRole" | "invalidateProjectPermissionCache" >; smtpService: TSmtpService; projectBotDAL: TProjectBotDALFactory; @@ -245,12 +242,7 @@ export const projectMembershipServiceFactory = ({ ); }); - await Promise.allSettled([ - permissionService.invalidateProjectPermissionCache(projectId), - ...orgMembers.map(({ userId }) => - userId ? permissionService.invalidateUserProjectPermissionCache(userId) : Promise.resolve() - ) - ]); + await permissionService.invalidateProjectPermissionCache(projectId); if (sendEmails) { await notificationService.createUserNotifications( @@ -384,12 +376,7 @@ export const projectMembershipServiceFactory = ({ return projectUserMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); }); - await Promise.allSettled([ - permissionService.invalidateProjectPermissionCache(projectId), - membershipUser?.userId - ? permissionService.invalidateUserProjectPermissionCache(membershipUser.userId) - : Promise.resolve() - ]); + await permissionService.invalidateProjectPermissionCache(projectId); return updatedRoles; }; @@ -435,10 +422,7 @@ export const projectMembershipServiceFactory = ({ return deletedMembership; }); - await Promise.allSettled([ - permissionService.invalidateProjectPermissionCache(projectId), - membership.userId ? permissionService.invalidateUserProjectPermissionCache(membership.userId) : Promise.resolve() - ]); + await permissionService.invalidateProjectPermissionCache(projectId); return membership; }; @@ -542,12 +526,7 @@ export const projectMembershipServiceFactory = ({ return deletedMemberships; }); - await Promise.allSettled([ - permissionService.invalidateProjectPermissionCache(projectId), - ...memberships.map(({ userId }) => - userId ? permissionService.invalidateUserProjectPermissionCache(userId) : Promise.resolve() - ) - ]); + await permissionService.invalidateProjectPermissionCache(projectId); return memberships; };