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">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
permissionService: Pick<
TPermissionServiceFactory,
| "getOrgPermission"
| "getOrgPermissionByRole"
| "invalidateUserProjectPermissionCache"
| "invalidateProjectPermissionCache"
>;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne">;
};
@@ -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];
};
@@ -28,7 +28,10 @@ type TIdentityProjectAdditionalPrivilegeV2ServiceFactoryDep = {
identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeV2DALFactory;
identityProjectDAL: Pick<TIdentityProjectDALFactory, "findOne" | "findById">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
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)
@@ -31,7 +31,10 @@ type TIdentityProjectAdditionalPrivilegeServiceFactoryDep = {
identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeDALFactory;
identityProjectDAL: Pick<TIdentityProjectDALFactory, "findOne" | "findById">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
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)
};
};
@@ -283,4 +283,7 @@ export type TPermissionServiceFactory = {
projectId: string;
checkPermissions: ProjectPermissionSet;
}) => Promise<boolean>;
invalidateProjectPermissionCache: (projectId: string) => Promise<void>;
invalidateUserProjectPermissionCache: (userId: string) => Promise<void>;
invalidateIdentityProjectPermissionCache: (identityId: string) => Promise<void>;
};
@@ -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<TServiceTokenDALFactory, "findById">;
projectDAL: Pick<TProjectDALFactory, "findById">;
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<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) {
case ActorType.USER:
return getUserProjectPermission({
result = (await getUserProjectPermission({
userId: actorId,
projectId,
authMethod: actorAuthMethod,
userOrgId: actorOrgId,
actionProjectType
}) as Promise<TProjectPermissionRT<T>>;
case ActorType.SERVICE:
return getServiceTokenProjectPermission({
serviceTokenId: actorId,
projectId,
actorOrgId,
actionProjectType
}) as Promise<TProjectPermissionRT<T>>;
})) as TProjectPermissionRT<T>;
break;
case ActorType.IDENTITY:
return getIdentityProjectPermission({
result = (await getIdentityProjectPermission({
identityId: actorId,
projectId,
identityOrgId: actorOrgId,
actionProjectType
}) as Promise<TProjectPermissionRT<T>>;
})) as TProjectPermissionRT<T>;
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
};
};
@@ -27,7 +27,10 @@ import {
type TProjectUserAdditionalPrivilegeServiceFactoryDep = {
projectUserAdditionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById" | "findOne">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
permissionService: Pick<
TPermissionServiceFactory,
"getProjectPermission" | "invalidateUserProjectPermissionCache" | "invalidateProjectPermissionCache"
>;
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "update">;
};
@@ -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)