fix: Add PBAC permission checks for insights access (#25381)
* fix: Add PBAC permission checks for insights access
- Add checkInsightsPermission() helper that properly checks insights.read permission with PBAC support
- Update userBelongsToTeamProcedure to use PBAC-aware permission check for org-level access
- Update teamListForUser query to filter teams based on insights.read permission instead of only checking base ADMIN/OWNER roles
- Maintain backward compatibility with fallback to traditional role checks (ADMIN/OWNER) when PBAC is disabled
- Org admins (base role ADMIN/OWNER) continue to have automatic insights access as a privileged position
- Team-level admins with custom roles now properly checked for insights.read permission
Fixes issue where users with custom PBAC roles couldn't access insights even if they had insights.read permission.
Related: CAL-XXXX
* perf: Optimize team permission checks to avoid N+1 queries
Replace individual permission checks per team with bulk query using getTeamIdsWithPermission().
This reduces database queries from N (one per team) to a single optimized query.
- Use PermissionCheckService.getTeamIdsWithPermission() for bulk permission checking
- Filter teams based on returned team IDs instead of individual checks
- Maintains same functionality with significantly better performance for users with many teams
* perf: Fetch only teams with insights access instead of filtering after
Move permission check before team query to filter at database level.
Previously fetched all teams then filtered in JavaScript.
Now only fetches teams the user has insights access to.
- Check permissions first using getTeamIdsWithPermission()
- Add teamId filter to membership query (teamId: { in: teamIdsWithAccess })
- Remove JavaScript filter step (done at DB level)
- Reduces data transfer and improves query efficiency
This commit is contained in:
1 parent
f136b7145f
commit
724204843a
1 file changed
+31
-27
@@ -15,8 +15,10 @@ import {
|
||||
routedToPerPeriodCsvInputSchema,
|
||||
bookingRepositoryBaseInputSchema,
|
||||
} from "@calcom/features/insights/server/raw-data.schema";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import authedProcedure from "@calcom/trpc/server/procedures/authedProcedure";
|
||||
import { router } from "@calcom/trpc/server/trpc";
|
||||
|
||||
@@ -213,17 +215,8 @@ const userBelongsToTeamProcedure = authedProcedure.use(async ({ ctx, next, getRa
|
||||
}
|
||||
}
|
||||
|
||||
const membershipOrg = await ctx.insightsDb.membership.findFirst({
|
||||
where: {
|
||||
userId: ctx.user.id,
|
||||
teamId: ctx.user.organizationId,
|
||||
accepted: true,
|
||||
role: {
|
||||
in: ["OWNER", "ADMIN"],
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!membershipOrg) {
|
||||
const hasOrgAccess = await checkInsightsPermission(ctx.user.id, ctx.user.organizationId);
|
||||
if (!hasOrgAccess) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
isOwnerAdminOfParentTeam = true;
|
||||
@@ -247,6 +240,16 @@ const userSelect = {
|
||||
avatarUrl: true,
|
||||
};
|
||||
|
||||
async function checkInsightsPermission(userId: number, teamId: number): Promise<boolean> {
|
||||
const permissionCheckService = new PermissionCheckService();
|
||||
return await permissionCheckService.checkPermission({
|
||||
userId,
|
||||
teamId,
|
||||
permission: "insights.read",
|
||||
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||
});
|
||||
}
|
||||
|
||||
const emptyResponseBookingKPIStats = {
|
||||
empty: true,
|
||||
created: {
|
||||
@@ -651,7 +654,21 @@ export const insightsRouter = router({
|
||||
return result;
|
||||
}
|
||||
|
||||
// Look if user it's admin/owner in multiple teams
|
||||
// Get all team IDs where user has insights.read permission in a single optimized query
|
||||
// This properly handles both PBAC permissions and traditional role-based access
|
||||
const permissionCheckService = new PermissionCheckService();
|
||||
const teamIdsWithAccess = await permissionCheckService.getTeamIdsWithPermission({
|
||||
userId: user.id,
|
||||
permission: "insights.read",
|
||||
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||
});
|
||||
|
||||
if (teamIdsWithAccess.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Fetch only teams where user has both membership AND insights access
|
||||
// This avoids fetching unnecessary team data by filtering at the database level
|
||||
const belongsToTeams = await ctx.insightsDb.membership.findMany({
|
||||
where: {
|
||||
team: {
|
||||
@@ -659,14 +676,7 @@ export const insightsRouter = router({
|
||||
},
|
||||
accepted: true,
|
||||
userId: user.id,
|
||||
OR: [
|
||||
{
|
||||
role: "ADMIN",
|
||||
},
|
||||
{
|
||||
role: "OWNER",
|
||||
},
|
||||
],
|
||||
teamId: { in: teamIdsWithAccess },
|
||||
},
|
||||
include: {
|
||||
team: {
|
||||
@@ -681,13 +691,7 @@ export const insightsRouter = router({
|
||||
},
|
||||
});
|
||||
|
||||
if (belongsToTeams.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result: IResultTeamList[] = belongsToTeams.map((membership) => {
|
||||
return { ...membership.team };
|
||||
});
|
||||
const result: IResultTeamList[] = belongsToTeams.map((membership) => ({ ...membership.team }));
|
||||
|
||||
return result;
|
||||
}),
|
||||
|
||||
Reference in new issue
Block a user