Skip to content

Comments

fix: remove page permission check for /insights#23945

Merged
eunjae-lee merged 2 commits intomainfrom
fix/remove-page-permission-check-for-insights
Sep 19, 2025
Merged

fix: remove page permission check for /insights#23945
eunjae-lee merged 2 commits intomainfrom
fix/remove-page-permission-check-for-insights

Conversation

@eunjae-lee
Copy link
Contributor

What does this PR do?

We've recently added a permission check for /insights/xxx pages. And this PR reverts some of them.

It's because a normal member without access to insights.read should still be able to access insights page for their own personal scoped data like this

Screenshot 2025-09-19 at 15 22 57

Mandatory Tasks (DO NOT REMOVE)

  • I have self-reviewed the code (A decent size PR without self-review might be rejected).
  • N/A - I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. If N/A, write N/A here and check the checkbox.
  • I confirm automated tests are in place that prove my fix is effective or that my feature works.

How should this be tested?

Impersonate pbac-analytics and visit /insights. It should open.

(on current main, it gets redirected to /)

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 19, 2025

Walkthrough

Removed the per-user Insights permission check and its module. The page-level guard now enforces only the global "insights" feature flag (redirect to "/") and authentication (redirect to "/auth/login"); it returns the session otherwise. The deleted helper was packages/features/insights/server/hasInsightsPermission.ts. In the TRPC router, all endpoints once using the PBAC-based insightsPbacProcedure were switched to userBelongsToTeamProcedure with a new UserBelongsToTeamInput; endpoint business logic and error handling remain unchanged.

Possibly related PRs

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title "fix: remove page permission check for /insights" succinctly and accurately summarizes the primary change in this PR—the removal of the page-level permission check for the /insights pages. It aligns with the provided diffs (removal of hasInsightsPermission, changes to checkInsightsPagePermission, and related router/test updates) and is concise and specific.
Description Check ✅ Passed The PR description directly describes reverting the recently added permission checks for /insights, gives the rationale (members without insights.read should still access personal-scoped insights), includes testing instructions and a screenshot, and matches the changes shown in the summary, so it is related and sufficiently informative for reviewers.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/remove-page-permission-check-for-insights

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@graphite-app graphite-app bot requested a review from a team September 19, 2025 13:30
@keithwillcode keithwillcode added consumer core area: core, team members only labels Sep 19, 2025
@eunjae-lee eunjae-lee marked this pull request as draft September 19, 2025 13:30
@dosubot dosubot bot added insights area: insights, analytics 🐛 bug Something isn't working labels Sep 19, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/features/insights/server/trpc-router.ts (2)

29-33: Critical: input shape mismatch breaks authorization (team/org scope bypass).

UserBelongsToTeamInput only accepts teamId/isAll, while most endpoints pass selectedTeamId/scope. As a result, userBelongsToTeamProcedure does not validate team membership nor org-scope, enabling unauthorized access when clients pass selectedTeamId or scope="org". Normalize both shapes.

Apply this diff to accept both forms and derive normalized fields:

-const UserBelongsToTeamInput = z.object({
-  teamId: z.coerce.number().optional().nullable(),
-  isAll: z.boolean().optional(),
-});
+const UserBelongsToTeamInput = z
+  .object({
+    teamId: z.coerce.number().optional().nullable(),
+    selectedTeamId: z.coerce.number().optional().nullable(),
+    scope: z.union([z.literal("user"), z.literal("team"), z.literal("org")]).optional(),
+    isAll: z.boolean().optional(),
+  })
+  .transform((data) => ({
+    teamId: data.teamId ?? data.selectedTeamId ?? null,
+    isAll: data.isAll ?? data.scope === "org",
+  }));

176-240: Critical: close auth bypass in userBelongsToTeamProcedure

The middleware currently permits requests when a team id is supplied and the user is neither a member nor part of the org — and "isAll" (org-scope) is not enforced to require OWNER/ADMIN. Require membership OR org OWNER/ADMIN for team scope, and require org OWNER/ADMIN for isAll.

Apply this diff (also handle selectedTeamId callers):

@@
-  const membershipWhereConditional: Prisma.MembershipWhereInput = {
+  const membershipWhereConditional: Prisma.MembershipWhereInput = {
     userId: ctx.user.id,
     accepted: true,
   };
-
-  if (parse.data.teamId) {
-    membershipWhereConditional["teamId"] = parse.data.teamId;
-  }
+  const effectiveTeamId = parse.data.teamId ?? parse.data.selectedTeamId ?? undefined;
+  if (effectiveTeamId) {
+    membershipWhereConditional.teamId = effectiveTeamId;
+  }
@@
-  // Probably we couldn't find a membership because the user is not a direct member of the team
-  // So that would mean ctx.user.organization is present
-  if ((parse.data.isAll && ctx.user.organizationId) || (!membership && ctx.user.organizationId)) {
-    //Look for membership type in organizationId
-    if (!membership && ctx.user.organizationId && parse.data.teamId) {
-      const isChildTeamOfOrg = await ctx.insightsDb.team.findFirst({
-        where: {
-          id: parse.data.teamId,
-          parentId: ctx.user.organizationId,
-        },
-      });
-      if (!isChildTeamOfOrg) {
-        throw new TRPCError({ code: "UNAUTHORIZED" });
-      }
-    }
-
-    const membershipOrg = await ctx.insightsDb.membership.findFirst({
-      where: {
-        userId: ctx.user.id,
-        teamId: ctx.user.organizationId,
-        accepted: true,
-        role: {
-          in: ["OWNER", "ADMIN"],
-        },
-      },
-    });
-    if (!membershipOrg) {
-      throw new TRPCError({ code: "UNAUTHORIZED" });
-    }
-    isOwnerAdminOfParentTeam = true;
-  }
+  // Org-scope (isAll) always requires OWNER/ADMIN on parent org.
+  if (parse.data.isAll) {
+    if (!ctx.user.organizationId) throw new TRPCError({ code: "UNAUTHORIZED" });
+    const membershipOrg = await ctx.insightsDb.membership.findFirst({
+      where: {
+        userId: ctx.user.id,
+        teamId: ctx.user.organizationId,
+        accepted: true,
+        role: { in: ["OWNER", "ADMIN"] },
+      },
+    });
+    if (!membershipOrg) throw new TRPCError({ code: "UNAUTHORIZED" });
+    isOwnerAdminOfParentTeam = true;
+  }
+
+  // Team-scope with a specific team requires membership OR org OWNER/ADMIN over that child team.
+  if (effectiveTeamId && !membership) {
+    if (!ctx.user.organizationId) throw new TRPCError({ code: "UNAUTHORIZED" });
+    const isChildTeamOfOrg = await ctx.insightsDb.team.findFirst({
+      where: { id: effectiveTeamId, parentId: ctx.user.organizationId },
+    });
+    if (!isChildTeamOfOrg) throw new TRPCError({ code: "UNAUTHORIZED" });
+    const membershipOrg = await ctx.insightsDb.membership.findFirst({
+      where: {
+        userId: ctx.user.id,
+        teamId: ctx.user.organizationId,
+        accepted: true,
+        role: { in: ["OWNER", "ADMIN"] },
+      },
+    });
+    if (!membershipOrg) throw new TRPCError({ code: "UNAUTHORIZED" });
+    isOwnerAdminOfParentTeam = true;
+  }

Precise pointers: middleware in packages/features/insights/server/trpc-router.ts (~lines 176–240). Callsites pass selectedTeamId at ~lines 306, 312, 327, 871 — ensure selectedTeamId -> teamId mapping is respected.

🧹 Nitpick comments (3)
packages/features/insights/server/trpc-router.ts (3)

445-470: Handle missing/invalid date range as BAD_REQUEST.

extractDateRangeFromColumnFilters throws if filters are absent; this happens before try/catch and returns a generic 500. Validate earlier and return a clear TRPC BAD_REQUEST.

-  const { startDate, endDate } = extractDateRangeFromColumnFilters(columnFilters);
+  try {
+    const { startDate, endDate } = extractDateRangeFromColumnFilters(columnFilters);
+    // ... keep existing logic below
+  } catch {
+    throw new TRPCError({ code: "BAD_REQUEST", message: "Missing or invalid date range filters" });
+  }

482-542: Push average computation to DB to avoid N+dayjs on large sets.

Looping through allBookings and formatting with dayjs per row may be heavy. Prefer an aggregate grouped by period at the DB layer.

Would you like a follow-up patch using Prisma raw SQL/groupBy to compute averages per period?


817-837: Endpoint depends on fixed guard; add a hard cap server-side.

rawData sets default limit 100, but clients can request up to 100; consider enforcing a strict upper bound server-side irrespective of input to protect memory.

-        return await insightsBookingService.getCsvData({
-          limit: limit ?? 100,
+        return await insightsBookingService.getCsvData({
+          limit: Math.min(limit ?? 100, 100),
           offset: offset ?? 0,
         });
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 76332a7 and a922fe5.

📒 Files selected for processing (3)
  • apps/web/app/(use-page-wrapper)/insights/checkInsightsPagePermission.ts (0 hunks)
  • packages/features/insights/server/hasInsightsPermission.ts (0 hunks)
  • packages/features/insights/server/trpc-router.ts (22 hunks)
💤 Files with no reviewable changes (2)
  • apps/web/app/(use-page-wrapper)/insights/checkInsightsPagePermission.ts
  • packages/features/insights/server/hasInsightsPermission.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • packages/features/insights/server/trpc-router.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • packages/features/insights/server/trpc-router.ts
**/*.{ts,tsx,js,jsx}

⚙️ CodeRabbit configuration file

Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.

Files:

  • packages/features/insights/server/trpc-router.ts
🧠 Learnings (4)
📓 Common learnings
Learnt from: SinghaAnirban005
PR: calcom/cal.com#23343
File: packages/features/insights/server/trpc-router.ts:1080-1101
Timestamp: 2025-08-26T08:08:23.395Z
Learning: In packages/features/insights/server/trpc-router.ts, when filtering personal event types (userId provided, no teamId, not isAll), the query correctly uses user.id (authenticated user) instead of the input userId parameter for security reasons. This prevents users from accessing other users' personal event types by passing arbitrary user IDs.
📚 Learning: 2025-08-26T08:08:23.395Z
Learnt from: SinghaAnirban005
PR: calcom/cal.com#23343
File: packages/features/insights/server/trpc-router.ts:1080-1101
Timestamp: 2025-08-26T08:08:23.395Z
Learning: In packages/features/insights/server/trpc-router.ts, when filtering personal event types (userId provided, no teamId, not isAll), the query correctly uses user.id (authenticated user) instead of the input userId parameter for security reasons. This prevents users from accessing other users' personal event types by passing arbitrary user IDs.

Applied to files:

  • packages/features/insights/server/trpc-router.ts
📚 Learning: 2025-08-17T22:00:16.329Z
Learnt from: Udit-takkar
PR: calcom/cal.com#22995
File: packages/trpc/server/routers/viewer/aiVoiceAgent/_router.ts:117-126
Timestamp: 2025-08-17T22:00:16.329Z
Learning: In calcom/cal.com PR #22995, packages/trpc/server/routers/viewer/aiVoiceAgent/_router.ts, the enabled input parameter in the update endpoint is intentionally not forwarded to aiService.updateAgentConfiguration() as the enabled/disabled agent functionality is not required at the moment (per maintainer Udit-takkar). Future reviews should not flag this as missing functionality unless requirements change.

Applied to files:

  • packages/features/insights/server/trpc-router.ts
📚 Learning: 2025-07-15T12:59:34.389Z
Learnt from: eunjae-lee
PR: calcom/cal.com#22106
File: packages/features/insights/components/FailedBookingsByField.tsx:65-71
Timestamp: 2025-07-15T12:59:34.389Z
Learning: In the FailedBookingsByField component (packages/features/insights/components/FailedBookingsByField.tsx), although routingFormId is typed as optional in useInsightsParameters, the system automatically enforces a routing form filter, so routingFormId is always present in practice. This means the data always contains only one entry, making the single-entry destructuring approach safe.

Applied to files:

  • packages/features/insights/server/trpc-router.ts
🧬 Code graph analysis (1)
packages/features/insights/server/trpc-router.ts (3)
packages/features/insights/server/raw-data.schema.ts (2)
  • bookingRepositoryBaseInputSchema (103-108)
  • insightsRoutingServiceInputSchema (69-75)
packages/features/insights/lib/bookingUtils.ts (1)
  • extractDateRangeFromColumnFilters (5-25)
packages/features/insights/server/insightsDateUtils.ts (2)
  • getTimeView (20-31)
  • getDateRanges (33-117)
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Install dependencies / Yarn install & cache
🔇 Additional comments (24)
packages/features/insights/server/trpc-router.ts (24)

340-444: Endpoint depends on fixed guard; otherwise OK.

bookingKPIStats is fine, but it passes selectedTeamId via bookingRepositoryBaseInputSchema; ensure the guard normalization above is in place.


471-481: Endpoint depends on fixed guard; otherwise OK.

popularEvents uses bookingRepositoryBaseInputSchema; relies on guard to validate selectedTeamId/scope.


543-556: Endpoint depends on fixed guard; otherwise OK.

membersWithMostCancelledBookings relies on guard to scope to user/team/org.


557-571: Endpoint depends on fixed guard; otherwise OK.

membersWithMostCompletedBookings is fine post-guard fix.


572-586: Endpoint depends on fixed guard; otherwise OK.

membersWithLeastCompletedBookings is fine post-guard fix.


587-597: Endpoint depends on fixed guard; otherwise OK.

membersWithMostBookings is fine post-guard fix.


598-608: Endpoint depends on fixed guard; otherwise OK.

membersWithLeastBookings is fine post-guard fix.


762-788: Good: ignores input userId for personal scope, uses authenticated user.

Passing ctx.user into getEventTypeList enforces the learned pattern to prevent userId spoofing. LGTM.


788-793: Endpoint depends on fixed guard; otherwise OK.

recentRatings is fine post-guard fix.


794-804: Endpoint depends on fixed guard; otherwise OK.

membersWithMostNoShow is fine post-guard fix.


805-810: Endpoint depends on fixed guard; otherwise OK.

membersWithHighestRatings is fine post-guard fix.


811-816: Endpoint depends on fixed guard; otherwise OK.

membersWithLowestRatings is fine post-guard fix.


839-849: LGTM (with guard fix).

getRoutingFormsForFilters correctly overrides userId with ctx.user.id. Relies on guard fix; otherwise fine.


850-856: Endpoint depends on fixed guard; otherwise OK.

routingFormsByStatus is fine post-guard fix.


857-865: Endpoint depends on fixed guard; otherwise OK.

routingFormResponses is fine post-guard fix.


866-891: Endpoint depends on fixed guard; CSV generation is OK.

Ensure guard normalization first; CSV escaping looks correct.


891-907: LGTM (with guard fix).

getRoutingFormFieldOptions properly forces userId from context.


908-917: Endpoint depends on fixed guard; otherwise OK.

failedBookingsByField is fine post-guard fix.


918-937: LGTM (with guard fix).

routingFormResponsesHeaders uses ctx.user.id/orgId; good.


938-953: Endpoint depends on fixed guard; otherwise OK.

routedToPerPeriod is fine post-guard fix.


954-975: Endpoint depends on fixed guard; otherwise OK.

routedToPerPeriodCsv is fine post-guard fix.


987-1004: Endpoint depends on fixed guard; otherwise OK.

getRoutingFunnelData is fine post-guard fix.


1005-1018: Endpoint depends on fixed guard; otherwise OK.

bookingsByHourStats is fine post-guard fix.


1019-1029: Endpoint depends on fixed guard; otherwise OK.

recentNoShowGuests is fine post-guard fix.

devin-ai-integration bot added a commit that referenced this pull request Sep 19, 2025
- Add helper functions for PBAC setup in e2e fixtures
- Add test verifying users with custom roles lacking insights.read can access /insights
- Test validates that PR #23945 correctly removes page-level permission checks
- Fix existing lint violations in users.ts fixture file

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>
@vercel
Copy link

vercel bot commented Sep 19, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Preview Comments Updated (UTC)
cal Ignored Ignored Sep 19, 2025 2:59pm
cal-eu Ignored Ignored Sep 19, 2025 2:59pm

@eunjae-lee eunjae-lee marked this pull request as ready for review September 19, 2025 15:00
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
apps/web/playwright/lib/test-helpers/pbac.ts (1)

5-18: Skips the internal _resource metadata field.

The function correctly filters out the "*" resource and processes all other resources. However, it also iterates over the internal _resource metadata field present in each resource config, which should be skipped.

Apply this diff to skip the _resource metadata field:

 export const createAllPermissionsArray = () => {
   const allPermissions: { resource: string; action: string }[] = [];

   Object.entries(PERMISSION_REGISTRY).forEach(([resource, resourceConfig]) => {
     if (resource === "*") {
       return;
     }
     Object.entries(resourceConfig).forEach(([action, _details]) => {
+      if (action === "_resource") {
+        return;
+      }
       allPermissions.push({ resource, action });
     });
   });

   return allPermissions;
 };
apps/web/playwright/insights.e2e.ts (1)

300-302: Consider using a more explicit filter condition for clarity.

While the current filter correctly excludes the insights.read permission, consider using a more explicit condition for better readability.

Apply this diff for improved clarity:

 const permissions = createAllPermissionsArray().filter(
-  ({ resource, action }) => !(resource === "insights" && action === "read")
+  (permission) => permission.resource !== "insights" || permission.action !== "read"
 );
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a922fe5 and 7398a69.

📒 Files selected for processing (2)
  • apps/web/playwright/insights.e2e.ts (2 hunks)
  • apps/web/playwright/lib/test-helpers/pbac.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • apps/web/playwright/lib/test-helpers/pbac.ts
  • apps/web/playwright/insights.e2e.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • apps/web/playwright/lib/test-helpers/pbac.ts
  • apps/web/playwright/insights.e2e.ts
**/*.{ts,tsx,js,jsx}

⚙️ CodeRabbit configuration file

Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.

Files:

  • apps/web/playwright/lib/test-helpers/pbac.ts
  • apps/web/playwright/insights.e2e.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: SinghaAnirban005
PR: calcom/cal.com#23343
File: packages/features/insights/server/trpc-router.ts:1080-1101
Timestamp: 2025-08-26T08:08:23.395Z
Learning: In packages/features/insights/server/trpc-router.ts, when filtering personal event types (userId provided, no teamId, not isAll), the query correctly uses user.id (authenticated user) instead of the input userId parameter for security reasons. This prevents users from accessing other users' personal event types by passing arbitrary user IDs.
🧬 Code graph analysis (2)
apps/web/playwright/lib/test-helpers/pbac.ts (1)
packages/features/pbac/domain/types/permission-registry.ts (1)
  • PERMISSION_REGISTRY (154-552)
apps/web/playwright/insights.e2e.ts (2)
apps/web/playwright/lib/test-helpers/pbac.ts (2)
  • enablePBACForTeam (20-29)
  • createAllPermissionsArray (5-18)
packages/features/pbac/services/permission-check.service.ts (2)
  • PermissionCheckService (19-290)
  • hasPermission (191-209)
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Tests / Unit
  • GitHub Check: Type check / check-types
  • GitHub Check: Linters / lint
🔇 Additional comments (2)
apps/web/playwright/lib/test-helpers/pbac.ts (1)

20-29: LGTM!

The function correctly creates a PBAC feature record for the specified team.

apps/web/playwright/insights.e2e.ts (1)

279-349: Test verifies the intended behavior of allowing access without insights.read permission.

The test successfully validates that users without the insights.read permission can still access the insights page when viewing their personal data, which aligns with the PR's objective of reverting the permission check. The test creates a robust scenario with PBAC enabled and confirms the lack of permission before verifying page access.

@eunjae-lee eunjae-lee enabled auto-merge (squash) September 19, 2025 15:31
@github-actions
Copy link
Contributor

E2E results are ready!

@eunjae-lee eunjae-lee merged commit 4e19204 into main Sep 19, 2025
63 of 64 checks passed
@eunjae-lee eunjae-lee deleted the fix/remove-page-permission-check-for-insights branch September 19, 2025 16:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐛 bug Something isn't working consumer core area: core, team members only insights area: insights, analytics ready-for-e2e size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants