Add project permission cache

This commit is contained in:
Carlos Monastyrski
2025-09-23 17:15:54 -03:00
parent 362abc44d6
commit 4eee40c2a3
12 changed files with 345 additions and 30 deletions
+38 -1
View File
@@ -44,7 +44,13 @@ type TGroupServiceFactoryDep = {
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">; permissionService: Pick<
TPermissionServiceFactory,
| "getOrgPermission"
| "getOrgPermissionByRole"
| "invalidateUserProjectPermissionCache"
| "invalidateProjectPermissionCache"
>;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne">; oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne">;
}; };
@@ -225,6 +231,17 @@ export const groupServiceFactory = ({
return updated; 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; return updatedGroup;
}; };
@@ -247,11 +264,19 @@ export const groupServiceFactory = ({
message: "Failed to delete group due to plan restriction. Upgrade plan to delete group." 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({ const [group] = await groupDAL.delete({
id, id,
orgId: actorOrgId orgId: actorOrgId
}); });
await Promise.allSettled([
...groupMembers.map((member) => permissionService.invalidateUserProjectPermissionCache(member.userId)),
...groupProjects.map((groupProject) => permissionService.invalidateProjectPermissionCache(groupProject.projectId))
]);
return group; return group;
}; };
@@ -398,6 +423,12 @@ export const groupServiceFactory = ({
projectBotDAL 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]; return users[0];
}; };
@@ -479,6 +510,12 @@ export const groupServiceFactory = ({
projectKeyDAL 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]; return users[0];
}; };
@@ -28,7 +28,10 @@ type TIdentityProjectAdditionalPrivilegeV2ServiceFactoryDep = {
identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeV2DALFactory; identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeV2DALFactory;
identityProjectDAL: Pick<TIdentityProjectDALFactory, "findOne" | "findById">; identityProjectDAL: Pick<TIdentityProjectDALFactory, "findOne" | "findById">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">; projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<
TPermissionServiceFactory,
"getProjectPermission" | "invalidateProjectPermissionCache" | "invalidateIdentityProjectPermissionCache"
>;
}; };
export type TIdentityProjectAdditionalPrivilegeV2ServiceFactory = ReturnType< export type TIdentityProjectAdditionalPrivilegeV2ServiceFactory = ReturnType<
@@ -115,6 +118,11 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
permissions: packedPermission permissions: packedPermission
}); });
await Promise.allSettled([
permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId),
permissionService.invalidateIdentityProjectPermissionCache(identityProjectMembership.identityId)
]);
return { return {
...additionalPrivilege, ...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions) permissions: unpackPermissions(additionalPrivilege.permissions)
@@ -132,6 +140,12 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime),
temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs)
}); });
await Promise.allSettled([
permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId),
permissionService.invalidateIdentityProjectPermissionCache(identityProjectMembership.identityId)
]);
return { return {
...additionalPrivilege, ...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions) permissions: unpackPermissions(additionalPrivilege.permissions)
@@ -224,6 +238,12 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""),
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
}); });
await Promise.allSettled([
permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId),
permissionService.invalidateIdentityProjectPermissionCache(identityProjectMembership.identityId)
]);
return { return {
...additionalPrivilege, ...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions) permissions: unpackPermissions(additionalPrivilege.permissions)
@@ -239,6 +259,12 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
temporaryRange: null, temporaryRange: null,
temporaryMode: null temporaryMode: null
}); });
await Promise.allSettled([
permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId),
permissionService.invalidateIdentityProjectPermissionCache(identityProjectMembership.identityId)
]);
return { return {
...additionalPrivilege, ...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions) permissions: unpackPermissions(additionalPrivilege.permissions)
@@ -294,6 +320,12 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
}); });
const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id); const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id);
await Promise.allSettled([
permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId),
permissionService.invalidateIdentityProjectPermissionCache(identityProjectMembership.identityId)
]);
return { return {
...deletedPrivilege, ...deletedPrivilege,
permissions: unpackPermissions(deletedPrivilege.permissions) permissions: unpackPermissions(deletedPrivilege.permissions)
@@ -31,7 +31,10 @@ type TIdentityProjectAdditionalPrivilegeServiceFactoryDep = {
identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeDALFactory; identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeDALFactory;
identityProjectDAL: Pick<TIdentityProjectDALFactory, "findOne" | "findById">; identityProjectDAL: Pick<TIdentityProjectDALFactory, "findOne" | "findById">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">; projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<
TPermissionServiceFactory,
"getProjectPermission" | "invalidateProjectPermissionCache" | "invalidateIdentityProjectPermissionCache"
>;
}; };
export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType< export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType<
@@ -129,6 +132,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
slug, slug,
permissions: packedPermission permissions: packedPermission
}); });
await Promise.allSettled([
permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId),
permissionService.invalidateIdentityProjectPermissionCache(identityId)
]);
return { return {
...additionalPrivilege, ...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions) permissions: unpackPermissions(additionalPrivilege.permissions)
@@ -146,6 +155,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime),
temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs)
}); });
await Promise.allSettled([
permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId),
permissionService.invalidateIdentityProjectPermissionCache(identityId)
]);
return { return {
...additionalPrivilege, ...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions) permissions: unpackPermissions(additionalPrivilege.permissions)
@@ -250,6 +265,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""),
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
}); });
await Promise.allSettled([
permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId),
permissionService.invalidateIdentityProjectPermissionCache(identityId)
]);
return { return {
...additionalPrivilege, ...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions) permissions: unpackPermissions(additionalPrivilege.permissions)
@@ -265,6 +286,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
temporaryRange: null, temporaryRange: null,
temporaryMode: null temporaryMode: null
}); });
await Promise.allSettled([
permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId),
permissionService.invalidateIdentityProjectPermissionCache(identityId)
]);
return { return {
...additionalPrivilege, ...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions) permissions: unpackPermissions(additionalPrivilege.permissions)
@@ -338,9 +365,14 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
} }
const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id); const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id);
await Promise.allSettled([
permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId),
permissionService.invalidateIdentityProjectPermissionCache(identityId)
]);
return { return {
...deletedPrivilege, ...deletedPrivilege,
permissions: unpackPermissions(deletedPrivilege.permissions) permissions: unpackPermissions(deletedPrivilege.permissions)
}; };
}; };
@@ -283,4 +283,7 @@ export type TPermissionServiceFactory = {
projectId: string; projectId: string;
checkPermissions: ProjectPermissionSet; checkPermissions: ProjectPermissionSet;
}) => Promise<boolean>; }) => Promise<boolean>;
invalidateProjectPermissionCache: (projectId: string) => Promise<void>;
invalidateUserProjectPermissionCache: (userId: string) => Promise<void>;
invalidateIdentityProjectPermissionCache: (identityId: string) => Promise<void>;
}; };
@@ -20,6 +20,7 @@ import {
projectViewerPermission, projectViewerPermission,
sshHostBootstrapPermissions sshHostBootstrapPermissions
} from "@app/ee/services/permission/default-roles"; } from "@app/ee/services/permission/default-roles";
import { KeyStorePrefixes, KeyStoreTtls, TKeyStoreFactory } from "@app/keystore/keystore";
import { conditionsMatcher } from "@app/lib/casl"; import { conditionsMatcher } from "@app/lib/casl";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { objectify } from "@app/lib/fn"; import { objectify } from "@app/lib/fn";
@@ -49,6 +50,7 @@ type TPermissionServiceFactoryDep = {
serviceTokenDAL: Pick<TServiceTokenDALFactory, "findById">; serviceTokenDAL: Pick<TServiceTokenDALFactory, "findById">;
projectDAL: Pick<TProjectDALFactory, "findById">; projectDAL: Pick<TProjectDALFactory, "findById">;
permissionDAL: TPermissionDALFactory; permissionDAL: TPermissionDALFactory;
keyStore: TKeyStoreFactory;
}; };
export const permissionServiceFactory = ({ export const permissionServiceFactory = ({
@@ -56,7 +58,8 @@ export const permissionServiceFactory = ({
orgRoleDAL, orgRoleDAL,
projectRoleDAL, projectRoleDAL,
serviceTokenDAL, serviceTokenDAL,
projectDAL projectDAL,
keyStore
}: TPermissionServiceFactoryDep): TPermissionServiceFactory => { }: TPermissionServiceFactoryDep): TPermissionServiceFactory => {
const buildOrgPermission = (orgUserRoles: TBuildOrgPermissionDTO) => { const buildOrgPermission = (orgUserRoles: TBuildOrgPermissionDTO) => {
const rules = orgUserRoles 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 buildProjectPermissionRules = (projectUserRoles: TBuildProjectPermissionDTO) => {
const rules = projectUserRoles const rules = projectUserRoles
.map(({ role, permissions }) => { .map(({ role, permissions }) => {
@@ -577,35 +595,90 @@ export const permissionServiceFactory = ({
actorId = assumedPrivilegeDetailsCtx.actorId; 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<TProjectPermissionRT<T>>;
}
try {
const cachedData = await keyStore.getItem(cacheKey);
if (cachedData) {
const parsed = JSON.parse(cachedData) as {
rules: RawRuleOf<MongoAbility<ProjectPermissionSet>>[];
membership: {
roles?: Array<{ role: string; customRoleSlug?: string }>;
[key: string]: unknown;
};
};
// Reconstruct the permission object from cached rules
const permission = createMongoAbility<ProjectPermissionSet>(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<T>;
}
} catch (error) {}
let result: TProjectPermissionRT<T>;
switch (actor) { switch (actor) {
case ActorType.USER: case ActorType.USER:
return getUserProjectPermission({ result = (await getUserProjectPermission({
userId: actorId, userId: actorId,
projectId, projectId,
authMethod: actorAuthMethod, authMethod: actorAuthMethod,
userOrgId: actorOrgId, userOrgId: actorOrgId,
actionProjectType actionProjectType
}) as Promise<TProjectPermissionRT<T>>; })) as TProjectPermissionRT<T>;
case ActorType.SERVICE: break;
return getServiceTokenProjectPermission({
serviceTokenId: actorId,
projectId,
actorOrgId,
actionProjectType
}) as Promise<TProjectPermissionRT<T>>;
case ActorType.IDENTITY: case ActorType.IDENTITY:
return getIdentityProjectPermission({ result = (await getIdentityProjectPermission({
identityId: actorId, identityId: actorId,
projectId, projectId,
identityOrgId: actorOrgId, identityOrgId: actorOrgId,
actionProjectType actionProjectType
}) as Promise<TProjectPermissionRT<T>>; })) as TProjectPermissionRT<T>;
break;
default: default:
throw new BadRequestError({ throw new BadRequestError({
message: "Invalid actor provided", message: "Invalid actor provided",
name: "Get project permission" 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 ( const getProjectPermissionByRole: TPermissionServiceFactory["getProjectPermissionByRole"] = async (
@@ -668,6 +741,9 @@ export const permissionServiceFactory = ({
getProjectPermissionByRole, getProjectPermissionByRole,
buildOrgPermission, buildOrgPermission,
buildProjectPermissionRules, buildProjectPermissionRules,
checkGroupProjectPermission checkGroupProjectPermission,
invalidateProjectPermissionCache,
invalidateUserProjectPermissionCache,
invalidateIdentityProjectPermissionCache
}; };
}; };
@@ -27,7 +27,10 @@ import {
type TProjectUserAdditionalPrivilegeServiceFactoryDep = { type TProjectUserAdditionalPrivilegeServiceFactoryDep = {
projectUserAdditionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory; projectUserAdditionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById" | "findOne">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById" | "findOne">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<
TPermissionServiceFactory,
"getProjectPermission" | "invalidateUserProjectPermissionCache" | "invalidateProjectPermissionCache"
>;
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "update">; accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "update">;
}; };
@@ -115,6 +118,12 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
slug, slug,
permissions: packedPermission permissions: packedPermission
}); });
await Promise.allSettled([
permissionService.invalidateProjectPermissionCache(projectMembership.projectId),
permissionService.invalidateUserProjectPermissionCache(projectMembership.userId)
]);
return { return {
...additionalPrivilege, ...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions) permissions: unpackPermissions(additionalPrivilege.permissions)
@@ -133,6 +142,12 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime),
temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs)
}); });
await Promise.allSettled([
permissionService.invalidateProjectPermissionCache(projectMembership.projectId),
permissionService.invalidateUserProjectPermissionCache(projectMembership.userId)
]);
return { return {
...additionalPrivilege, ...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions) permissions: unpackPermissions(additionalPrivilege.permissions)
@@ -230,6 +245,11 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
}); });
await Promise.allSettled([
permissionService.invalidateProjectPermissionCache(projectMembership.projectId),
permissionService.invalidateUserProjectPermissionCache(projectMembership.userId)
]);
return { return {
...additionalPrivilege, ...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions) permissions: unpackPermissions(additionalPrivilege.permissions)
@@ -245,6 +265,12 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
temporaryRange: null, temporaryRange: null,
temporaryMode: null temporaryMode: null
}); });
await Promise.allSettled([
permissionService.invalidateProjectPermissionCache(projectMembership.projectId),
permissionService.invalidateUserProjectPermissionCache(projectMembership.userId)
]);
return { return {
...additionalPrivilege, ...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions) permissions: unpackPermissions(additionalPrivilege.permissions)
@@ -291,6 +317,12 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
} }
); );
const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id); const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id);
await Promise.allSettled([
permissionService.invalidateProjectPermissionCache(projectMembership.projectId),
permissionService.invalidateUserProjectPermissionCache(projectMembership.userId)
]);
return { return {
...deletedPrivilege, ...deletedPrivilege,
permissions: unpackPermissions(deletedPrivilege.permissions) permissions: unpackPermissions(deletedPrivilege.permissions)
+11 -2
View File
@@ -63,13 +63,22 @@ export const KeyStorePrefixes = {
ActiveSSEConnectionsSet: (projectId: string, identityId: string) => ActiveSSEConnectionsSet: (projectId: string, identityId: string) =>
`sse-connections:${projectId}:${identityId}` as const, `sse-connections:${projectId}:${identityId}` as const,
ActiveSSEConnections: (projectId: string, identityId: string, connectionId: string) => 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 = { export const KeyStoreTtls = {
SetSyncSecretIntegrationLastRunTimestampInSeconds: 60, SetSyncSecretIntegrationLastRunTimestampInSeconds: 60,
SetSecretSyncLastRunTimestampInSeconds: 60, SetSecretSyncLastRunTimestampInSeconds: 60,
AccessTokenStatusUpdateInSeconds: 120 AccessTokenStatusUpdateInSeconds: 120,
ProjectPermissionCacheInSeconds: 300 // 5 minutes
}; };
type TDeleteItems = { type TDeleteItems = {
+2 -1
View File
@@ -530,7 +530,8 @@ export const registerRoutes = async (
orgRoleDAL, orgRoleDAL,
projectRoleDAL, projectRoleDAL,
serviceTokenDAL, serviceTokenDAL,
projectDAL projectDAL,
keyStore
}); });
const assumePrivilegeService = assumePrivilegeServiceFactory({ const assumePrivilegeService = assumePrivilegeServiceFactory({
projectDAL, projectDAL,
@@ -37,13 +37,19 @@ type TGroupProjectServiceFactoryDep = {
TGroupProjectMembershipRoleDALFactory, TGroupProjectMembershipRoleDALFactory,
"create" | "transaction" | "insertMany" | "delete" "create" | "transaction" | "insertMany" | "delete"
>; >;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findGroupMembersNotInProject">; userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findGroupMembersNotInProject" | "find">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "findProjectGhostUser" | "findById">; projectDAL: Pick<TProjectDALFactory, "findOne" | "findProjectGhostUser" | "findById">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany" | "transaction">; projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany" | "transaction">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">; projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
projectBotDAL: TProjectBotDALFactory; projectBotDAL: TProjectBotDALFactory;
groupDAL: Pick<TGroupDALFactory, "findOne" | "findAllGroupPossibleMembers">; groupDAL: Pick<TGroupDALFactory, "findOne" | "findAllGroupPossibleMembers">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getProjectPermissionByRole">; permissionService: Pick<
TPermissionServiceFactory,
| "getProjectPermission"
| "getProjectPermissionByRole"
| "invalidateProjectPermissionCache"
| "invalidateUserProjectPermissionCache"
>;
}; };
export type TGroupProjectServiceFactory = ReturnType<typeof groupProjectServiceFactory>; export type TGroupProjectServiceFactory = ReturnType<typeof groupProjectServiceFactory>;
@@ -263,6 +269,12 @@ export const groupProjectServiceFactory = ({
return groupProjectMembership; 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; return projectGroup;
}; };
@@ -372,6 +384,12 @@ export const groupProjectServiceFactory = ({
return groupProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); 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; return updatedRoles;
}; };
@@ -403,15 +421,21 @@ export const groupProjectServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Delete, ProjectPermissionSub.Groups); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Delete, ProjectPermissionSub.Groups);
const deletedProjectGroup = await groupProjectDAL.transaction(async (tx) => { const groupMembers = await userGroupMembershipDAL.find({ groupId: group.id });
const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, project.id, tx);
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( await projectKeyDAL.delete(
{ {
projectId: project.id, projectId: project.id,
$in: { $in: {
receiverId: groupMembers.map(({ user: { id } }) => id) receiverId: groupMembersNotInProject.map(({ user: { id } }) => id)
} }
}, },
tx tx
@@ -422,6 +446,11 @@ export const groupProjectServiceFactory = ({
return projectGroup; return projectGroup;
}); });
await Promise.allSettled([
permissionService.invalidateProjectPermissionCache(projectId),
...groupMembers.map((member) => permissionService.invalidateUserProjectPermissionCache(member.userId))
]);
return deletedProjectGroup; return deletedProjectGroup;
}; };
@@ -35,7 +35,13 @@ type TIdentityProjectServiceFactoryDep = {
projectDAL: Pick<TProjectDALFactory, "findById">; projectDAL: Pick<TProjectDALFactory, "findById">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">; projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">; identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getProjectPermissionByRole">; permissionService: Pick<
TPermissionServiceFactory,
| "getProjectPermission"
| "getProjectPermissionByRole"
| "invalidateIdentityProjectPermissionCache"
| "invalidateProjectPermissionCache"
>;
}; };
export type TIdentityProjectServiceFactory = ReturnType<typeof identityProjectServiceFactory>; export type TIdentityProjectServiceFactory = ReturnType<typeof identityProjectServiceFactory>;
@@ -165,6 +171,12 @@ export const identityProjectServiceFactory = ({
const identityRoles = await identityProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); const identityRoles = await identityProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
return { ...identityProjectMembership, roles: identityRoles }; return { ...identityProjectMembership, roles: identityRoles };
}); });
await Promise.allSettled([
permissionService.invalidateProjectPermissionCache(projectId),
permissionService.invalidateIdentityProjectPermissionCache(identityId)
]);
return projectIdentity; return projectIdentity;
}; };
@@ -272,6 +284,11 @@ export const identityProjectServiceFactory = ({
return identityProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); return identityProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
}); });
await Promise.allSettled([
permissionService.invalidateProjectPermissionCache(projectId),
permissionService.invalidateIdentityProjectPermissionCache(identityId)
]);
return updatedRoles; return updatedRoles;
}; };
@@ -302,6 +319,12 @@ export const identityProjectServiceFactory = ({
); );
const [deletedIdentity] = await identityProjectDAL.delete({ identityId, projectId }); const [deletedIdentity] = await identityProjectDAL.delete({ identityId, projectId });
await Promise.allSettled([
permissionService.invalidateProjectPermissionCache(projectId),
permissionService.invalidateIdentityProjectPermissionCache(identityId)
]);
return deletedIdentity; return deletedIdentity;
}; };
@@ -43,7 +43,13 @@ import {
import { TProjectUserMembershipRoleDALFactory } from "./project-user-membership-role-dal"; import { TProjectUserMembershipRoleDALFactory } from "./project-user-membership-role-dal";
type TProjectMembershipServiceFactoryDep = { type TProjectMembershipServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getProjectPermissionByRole">; permissionService: Pick<
TPermissionServiceFactory,
| "getProjectPermission"
| "getProjectPermissionByRole"
| "invalidateUserProjectPermissionCache"
| "invalidateProjectPermissionCache"
>;
smtpService: TSmtpService; smtpService: TSmtpService;
projectBotDAL: TProjectBotDALFactory; projectBotDAL: TProjectBotDALFactory;
projectMembershipDAL: TProjectMembershipDALFactory; 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) { if (sendEmails) {
await notificationService.createUserNotifications( await notificationService.createUserNotifications(
orgMembers.map((member) => ({ orgMembers.map((member) => ({
@@ -371,6 +384,13 @@ export const projectMembershipServiceFactory = ({
return projectUserMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); return projectUserMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
}); });
await Promise.allSettled([
permissionService.invalidateProjectPermissionCache(projectId),
membershipUser?.userId
? permissionService.invalidateUserProjectPermissionCache(membershipUser.userId)
: Promise.resolve()
]);
return updatedRoles; return updatedRoles;
}; };
@@ -414,6 +434,12 @@ export const projectMembershipServiceFactory = ({
); );
return deletedMembership; return deletedMembership;
}); });
await Promise.allSettled([
permissionService.invalidateProjectPermissionCache(projectId),
membership.userId ? permissionService.invalidateUserProjectPermissionCache(membership.userId) : Promise.resolve()
]);
return membership; return membership;
}; };
@@ -515,6 +541,14 @@ export const projectMembershipServiceFactory = ({
return deletedMemberships; return deletedMemberships;
}); });
await Promise.allSettled([
permissionService.invalidateProjectPermissionCache(projectId),
...memberships.map(({ userId }) =>
userId ? permissionService.invalidateUserProjectPermissionCache(userId) : Promise.resolve()
)
]);
return memberships; return memberships;
}; };
@@ -35,7 +35,10 @@ type TProjectRoleServiceFactoryDep = {
identityDAL: Pick<TIdentityDALFactory, "findById">; identityDAL: Pick<TIdentityDALFactory, "findById">;
userDAL: Pick<TUserDALFactory, "findById">; userDAL: Pick<TUserDALFactory, "findById">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findProjectById">; projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findProjectById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getUserProjectPermission">; permissionService: Pick<
TPermissionServiceFactory,
"getProjectPermission" | "getUserProjectPermission" | "invalidateProjectPermissionCache"
>;
identityProjectMembershipRoleDAL: TIdentityProjectMembershipRoleDALFactory; identityProjectMembershipRoleDAL: TIdentityProjectMembershipRoleDALFactory;
projectUserMembershipRoleDAL: TProjectUserMembershipRoleDALFactory; projectUserMembershipRoleDAL: TProjectUserMembershipRoleDALFactory;
}; };
@@ -162,6 +165,8 @@ export const projectRoleServiceFactory = ({
}); });
if (!updatedRole) throw new NotFoundError({ message: "Project role not found", name: "Update role" }); if (!updatedRole) throw new NotFoundError({ message: "Project role not found", name: "Update role" });
await permissionService.invalidateProjectPermissionCache(projectRole.projectId);
return { ...updatedRole, permissions: unpackPermissions(updatedRole.permissions) }; return { ...updatedRole, permissions: unpackPermissions(updatedRole.permissions) };
}; };
@@ -197,6 +202,8 @@ export const projectRoleServiceFactory = ({
const deletedRole = await projectRoleDAL.deleteById(roleId); const deletedRole = await projectRoleDAL.deleteById(roleId);
if (!deletedRole) throw new NotFoundError({ message: "Project role not found", name: "Delete role" }); if (!deletedRole) throw new NotFoundError({ message: "Project role not found", name: "Delete role" });
await permissionService.invalidateProjectPermissionCache(projectRole.projectId);
return { ...deletedRole, permissions: unpackPermissions(deletedRole.permissions) }; return { ...deletedRole, permissions: unpackPermissions(deletedRole.permissions) };
}; };