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 da87978fd3..d69c6da790 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 46c9831b08..d4d30b5ae9 100644 --- a/backend/src/ee/services/group/group-service.ts +++ b/backend/src/ee/services/group/group-service.ts @@ -44,7 +44,10 @@ type TGroupServiceFactoryDep = { projectDAL: Pick; projectBotDAL: Pick; projectKeyDAL: Pick; - permissionService: Pick; + permissionService: Pick< + TPermissionServiceFactory, + "getOrgPermission" | "getOrgPermissionByRole" | "invalidateProjectPermissionCache" + >; licenseService: Pick; oidcConfigDAL: Pick; }; @@ -225,6 +228,15 @@ export const groupServiceFactory = ({ return updated; }); + if (role) { + const groupProjects = await groupProjectDAL.find({ groupId: group.id }); + await Promise.allSettled([ + ...groupProjects.map((groupProject) => + permissionService.invalidateProjectPermissionCache(groupProject.projectId) + ) + ]); + } + return updatedGroup; }; @@ -247,11 +259,17 @@ export const groupServiceFactory = ({ message: "Failed to delete group due to plan restriction. Upgrade plan to delete group." }); + const groupProjects = await groupProjectDAL.find({ groupId: id }); + const [group] = await groupDAL.delete({ id, orgId: actorOrgId }); + await Promise.allSettled([ + ...groupProjects.map((groupProject) => permissionService.invalidateProjectPermissionCache(groupProject.projectId)) + ]); + return group; }; @@ -398,6 +416,11 @@ export const groupServiceFactory = ({ projectBotDAL }); + const groupProjects = await groupProjectDAL.find({ groupId: group.id }); + await Promise.allSettled([ + ...groupProjects.map((groupProject) => permissionService.invalidateProjectPermissionCache(groupProject.projectId)) + ]); + return users[0]; }; @@ -479,6 +502,11 @@ export const groupServiceFactory = ({ projectKeyDAL }); + const groupProjects = await groupProjectDAL.find({ groupId: group.id }); + await Promise.allSettled([ + ...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 64da588f8d..27b67367e0 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,7 @@ type TIdentityProjectAdditionalPrivilegeV2ServiceFactoryDep = { identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeV2DALFactory; identityProjectDAL: Pick; projectDAL: Pick; - permissionService: Pick; + permissionService: Pick; }; export type TIdentityProjectAdditionalPrivilegeV2ServiceFactory = ReturnType< @@ -115,6 +115,8 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ permissions: packedPermission }); + await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); + return { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) @@ -132,6 +134,9 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) }); + + await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); + return { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) @@ -224,6 +229,9 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) }); + + await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); + return { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) @@ -239,6 +247,9 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ temporaryRange: null, temporaryMode: null }); + + await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); + return { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) @@ -294,6 +305,9 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ }); const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id); + + await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); + 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 828cf43a37..ddba76920a 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,7 @@ type TIdentityProjectAdditionalPrivilegeServiceFactoryDep = { identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeDALFactory; identityProjectDAL: Pick; projectDAL: Pick; - permissionService: Pick; + permissionService: Pick; }; export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType< @@ -129,6 +129,9 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ slug, permissions: packedPermission }); + + await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); + return { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) @@ -146,6 +149,9 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) }); + + await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); + return { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) @@ -250,6 +256,9 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) }); + + await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); + return { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) @@ -265,6 +274,9 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ temporaryRange: null, temporaryMode: null }); + + await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); + return { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) @@ -338,9 +350,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ } const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id); + + await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); + 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 5e71c65d94..f25b84bd59 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,4 +284,5 @@ export type TPermissionServiceFactory = { projectId: string; checkPermissions: ProjectPermissionSet; }) => Promise; + invalidateProjectPermissionCache: (projectId: string, tx?: Knex) => Promise; }; diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index 461ceef461..9e94e14158 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, @@ -20,9 +21,11 @@ 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"; +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"; @@ -49,6 +52,7 @@ type TPermissionServiceFactoryDep = { serviceTokenDAL: Pick; projectDAL: Pick; permissionDAL: TPermissionDALFactory; + keyStore: TKeyStoreFactory; }; export const permissionServiceFactory = ({ @@ -56,7 +60,8 @@ export const permissionServiceFactory = ({ orgRoleDAL, projectRoleDAL, serviceTokenDAL, - projectDAL + projectDAL, + keyStore }: TPermissionServiceFactoryDep): TPermissionServiceFactory => { const buildOrgPermission = (orgUserRoles: TBuildOrgPermissionDTO) => { const rules = orgUserRoles @@ -83,6 +88,68 @@ export const permissionServiceFactory = ({ }); }; + const invalidateProjectPermissionCache = async (projectId: string, tx?: Knex) => { + const projectPermissionDalVersionKey = KeyStorePrefixes.ProjectPermissionDalVersion(projectId); + await keyStore.pgIncrementBy(projectPermissionDalVersionKey, { + incr: 1, + tx, + expiry: KeyStoreTtls.ProjectPermissionDalVersionTtl + }); + }; + + 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 }) => { @@ -577,35 +644,94 @@ export const permissionServiceFactory = ({ actorId = assumedPrivilegeDetailsCtx.actorId; } + if (actor === ActorType.SERVICE) { + return getServiceTokenProjectPermission({ + serviceTokenId: actorId, + projectId, + actorOrgId, + actionProjectType + }) 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) { + const parsed = JSON.parse(cachedData) as { + rules: RawRuleOf>[]; + membership: { + roles?: Array<{ role: string; customRoleSlug?: string }>; + [key: string]: unknown; + }; + }; + 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) { + logger.error(error, "Failed to get project permission"); + } + + 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 + }; + + const ttl = calculateProjectPermissionTtl(result.membership); + await keyStore.setItemWithExpiry(cacheKey, ttl, JSON.stringify(cacheData)); + } catch (error) { + logger.error(error, "Failed to cache project permission"); + } + + return result; }; const getProjectPermissionByRole: TPermissionServiceFactory["getProjectPermissionByRole"] = async ( @@ -668,6 +794,7 @@ export const permissionServiceFactory = ({ getProjectPermissionByRole, buildOrgPermission, buildProjectPermissionRules, - checkGroupProjectPermission + checkGroupProjectPermission, + 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 944775156e..c2876572f1 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,7 @@ import { type TProjectUserAdditionalPrivilegeServiceFactoryDep = { projectUserAdditionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory; projectMembershipDAL: Pick; - permissionService: Pick; + permissionService: Pick; accessApprovalRequestDAL: Pick; }; @@ -115,6 +115,9 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ slug, permissions: packedPermission }); + + await permissionService.invalidateProjectPermissionCache(projectMembership.projectId); + return { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) @@ -133,6 +136,9 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) }); + + await permissionService.invalidateProjectPermissionCache(projectMembership.projectId); + return { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) @@ -230,6 +236,8 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) }); + await permissionService.invalidateProjectPermissionCache(projectMembership.projectId); + return { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) @@ -245,6 +253,9 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ temporaryRange: null, temporaryMode: null }); + + await permissionService.invalidateProjectPermissionCache(projectMembership.projectId); + return { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) @@ -291,6 +302,9 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ } ); const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id); + + await permissionService.invalidateProjectPermissionCache(projectMembership.projectId); + return { ...deletedPrivilege, permissions: unpackPermissions(deletedPrivilege.permissions) diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index 04ba8428b5..17f4aa4937 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -63,13 +63,28 @@ 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, + 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 }; export const KeyStoreTtls = { SetSyncSecretIntegrationLastRunTimestampInSeconds: 60, SetSecretSyncLastRunTimestampInSeconds: 60, - AccessTokenStatusUpdateInSeconds: 120 + AccessTokenStatusUpdateInSeconds: 120, + ProjectPermissionCacheInSeconds: 300, // 5 minutes + ProjectPermissionDalVersionTtl: "15m" // Project permission DAL version TTL }; type TDeleteItems = { diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 2fb7e9ff72..3171919a96 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 838d894375..913aca2f34 100644 --- a/backend/src/services/group-project/group-project-service.ts +++ b/backend/src/services/group-project/group-project-service.ts @@ -37,13 +37,16 @@ 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" + >; }; export type TGroupProjectServiceFactory = ReturnType; @@ -263,6 +266,8 @@ export const groupProjectServiceFactory = ({ return groupProjectMembership; }); + await permissionService.invalidateProjectPermissionCache(projectId); + return projectGroup; }; @@ -372,6 +377,8 @@ export const groupProjectServiceFactory = ({ return groupProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); }); + await permissionService.invalidateProjectPermissionCache(projectId); + return updatedRoles; }; @@ -404,14 +411,18 @@ 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 groupMembersNotInProject = await userGroupMembershipDAL.findGroupMembersNotInProject( + group.id, + project.id, + tx + ); - if (groupMembers.length) { + 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 +433,8 @@ export const groupProjectServiceFactory = ({ return projectGroup; }); + 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 4f0964f42d..08fd2cf0be 100644 --- a/backend/src/services/identity-project/identity-project-service.ts +++ b/backend/src/services/identity-project/identity-project-service.ts @@ -35,7 +35,10 @@ type TIdentityProjectServiceFactoryDep = { projectDAL: Pick; projectRoleDAL: Pick; identityOrgMembershipDAL: Pick; - permissionService: Pick; + permissionService: Pick< + TPermissionServiceFactory, + "getProjectPermission" | "getProjectPermissionByRole" | "invalidateProjectPermissionCache" + >; }; export type TIdentityProjectServiceFactory = ReturnType; @@ -165,6 +168,9 @@ export const identityProjectServiceFactory = ({ const identityRoles = await identityProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); return { ...identityProjectMembership, roles: identityRoles }; }); + + await permissionService.invalidateProjectPermissionCache(projectId); + return projectIdentity; }; @@ -272,6 +278,8 @@ export const identityProjectServiceFactory = ({ return identityProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); }); + await permissionService.invalidateProjectPermissionCache(projectId); + return updatedRoles; }; @@ -302,6 +310,9 @@ export const identityProjectServiceFactory = ({ ); const [deletedIdentity] = await identityProjectDAL.delete({ identityId, projectId }); + + 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 991bf65d16..301ce1bc04 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -43,7 +43,10 @@ import { import { TProjectUserMembershipRoleDALFactory } from "./project-user-membership-role-dal"; type TProjectMembershipServiceFactoryDep = { - permissionService: Pick; + permissionService: Pick< + TPermissionServiceFactory, + "getProjectPermission" | "getProjectPermissionByRole" | "invalidateProjectPermissionCache" + >; smtpService: TSmtpService; projectBotDAL: TProjectBotDALFactory; projectMembershipDAL: TProjectMembershipDALFactory; @@ -239,6 +242,8 @@ export const projectMembershipServiceFactory = ({ ); }); + await permissionService.invalidateProjectPermissionCache(projectId); + if (sendEmails) { await notificationService.createUserNotifications( orgMembers.map((member) => ({ @@ -371,6 +376,8 @@ export const projectMembershipServiceFactory = ({ return projectUserMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); }); + await permissionService.invalidateProjectPermissionCache(projectId); + return updatedRoles; }; @@ -414,6 +421,9 @@ export const projectMembershipServiceFactory = ({ ); return deletedMembership; }); + + await permissionService.invalidateProjectPermissionCache(projectId); + return membership; }; @@ -515,6 +525,9 @@ export const projectMembershipServiceFactory = ({ return deletedMemberships; }); + + await permissionService.invalidateProjectPermissionCache(projectId); + 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 dd0eecc68e..f30da21f53 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) }; };