Skip to content

Conversation

devkiran
Copy link
Collaborator

@devkiran devkiran commented Oct 3, 2025

Summary by CodeRabbit

  • New Features

    • SAML enforcement now uses a timestamp to drive the SAML badge and toggle while still displaying the configured SSO email domain.
    • Workspace data now exposes SSO domain and enforcement status for UI use.
  • Bug Fixes

    • More consistent SSO domain checks during sign-in and locale-aware domain handling.
    • Duplicate/conflict messages now clearly reference the workspace slug.
  • Chores

    • Added a migration helper to backfill SSO domains for existing workspaces.

Copy link
Contributor

vercel bot commented Oct 3, 2025

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

Project Deployment Preview Updated (UTC)
dub Ready Ready Preview Oct 3, 2025 11:12pm

Copy link
Contributor

coderabbitai bot commented Oct 3, 2025

Warning

Rate limit exceeded

@steven-tey has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 5 minutes and 52 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 7c8ed1d and 49afc34.

📒 Files selected for processing (2)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/security/saml.tsx (4 hunks)
  • apps/web/scripts/migrations/backfill-saml-sso.ts (1 hunks)

Walkthrough

Replaces per-domain SSO enforcement with a timestamp field (ssoEnforcedAt) while continuing to persist ssoEmailDomain on SAML connection create; updates API routes, UI, auth checks, Prisma schema, Zod workspace schema, and a backfill script accordingly.

Changes

Cohort / File(s) Summary
Workspace update route
apps/web/app/api/workspaces/[idOrSlug]/route.ts
Removed local ssoEmailDomain handling and generic email guard; payload now conditionally sets ssoEnforcedAt (Date
SAML connection routes
apps/web/app/api/workspaces/[idOrSlug]/saml/route.ts
POST handler now receives session, derives ssoEmailDomain from session.user.email and updates Project; DELETE clears both ssoEmailDomain and ssoEnforcedAt.
Dashboard SAML settings UI
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/security/saml.tsx
Removed useSession; useWorkspace exposes ssoEmailDomain; optimistic update payload/type changed to `{ ssoEnforcedAt: Date
Auth flow checks
apps/web/lib/auth/options.ts
Email domain computed from user.email; presence of ssoEmailDomain required for SAML-enabled workspace and domain comparison (case-insensitive) enforced; denies auth on mismatch or missing ssoEmailDomain.
SAML enforcement utility
apps/web/lib/api/workspaces/is-saml-enforced-for-email-domain.ts
Domain normalization uses toLocaleLowerCase(); replaced prisma.project.count with prisma.project.findUnique({ select: { ssoEnforcedAt } }); returns true when ssoEnforcedAt is present.
Schemas
apps/web/lib/zod/schemas/workspaces.ts
Added `ssoEmailDomain: string
Database schema
packages/prisma/schema/workspace.prisma
Added nullable field ssoEnforcedAt DateTime? to Project model.
Migration script
apps/web/scripts/migrations/backfill-saml-sso.ts
New script to infer ssoEmailDomain from first user emails for enterprise workspaces; prints table of updates and includes a commented-out batch update call.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Admin as Admin (Dashboard)
  participant UI as SAML Settings UI
  participant API as PATCH /api/workspaces/:idOrSlug
  participant DB as Prisma (Project)

  Admin->>UI: Toggle "Enforce SAML"
  UI->>API: { enforceSAML: true|false }
  API->>DB: Update Project.ssoEnforcedAt = now() or null
  DB-->>API: Updated Project { ssoEnforcedAt }
  API-->>UI: { ssoEnforcedAt }
  UI-->>Admin: Reflect toggle and badge (domain text from workspace.ssoEmailDomain)
Loading
sequenceDiagram
  autonumber
  actor Admin as Admin
  participant UI as SAML Settings UI
  participant SAMLAPI as POST/DELETE /api/workspaces/:idOrSlug/saml
  participant Sess as Session
  participant DB as Prisma (Project)

  rect rgba(200,255,200,0.12)
  note over Admin,SAMLAPI: Create SAML connection
  Admin->>UI: Connect SAML
  UI->>SAMLAPI: POST (includes session)
  SAMLAPI->>Sess: Read session.user.email
  SAMLAPI->>DB: Update Project.ssoEmailDomain = domain(session.email)
  SAMLAPI-->>UI: 201 Created
  end

  rect rgba(255,230,200,0.12)
  note over Admin,SAMLAPI: Delete SAML connection
  Admin->>UI: Remove SAML
  UI->>SAMLAPI: DELETE
  SAMLAPI->>DB: Set ssoEmailDomain = null, ssoEnforcedAt = null
  SAMLAPI-->>UI: 200 OK
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • steven-tey

Poem

In burrows of code I nibble and hop,
A timestamp now marks the SSO crop,
I stitch the domain when connections bloom,
Clear fields on delete — no longer gloom.
Thump-thump — a rabbit's tiny refactor bop. 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Fix SAML SSO Enforcement Implementation" accurately reflects the main purpose of the changeset, which consolidates and corrects how SAML SSO enforcement is handled across the API, UI, schema, and database layers. It is concise, specific, and directly related to the primary update without including unnecessary detail.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

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.

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: 6

Caution

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

⚠️ Outside diff range comments (1)
apps/web/app/api/workspaces/[idOrSlug]/route.ts (1)

109-130: Verify SAML configuration before allowing enforcement.

The code validates that SAML connections exist when enforceSAML is true (lines 117-129), which is good. However, there's no verification that ssoEmailDomain has been set on the workspace. Since the email domain is now managed separately (set in saml/route.ts during POST), there's a potential race condition or misconfiguration scenario:

  1. Admin creates SAML connection (sets ssoEmailDomain)
  2. Admin deletes SAML connection (clears ssoEmailDomain and ssoEnforcedAt)
  3. Admin creates a new SAML connection with a different email domain
  4. Meanwhile, another admin could enable enforcement before step 3 completes

This could result in ssoEnforcedAt being set while ssoEmailDomain is null or stale.

Consider adding a check to verify ssoEmailDomain is set:

     if (connections.length === 0) {
       throw new DubApiError({
         code: "forbidden",
         message: "SAML SSO is not configured for this workspace.",
       });
     }
+
+    if (!workspace.ssoEmailDomain) {
+      throw new DubApiError({
+        code: "forbidden",
+        message: "SAML SSO email domain is not configured for this workspace.",
+      });
+    }
   }
🧹 Nitpick comments (5)
apps/web/scripts/migrations/backfill-saml-sso.ts (2)

36-41: Detect unique collisions before writing

Avoid attempting updates that will violate unique(ssoEmailDomain). Deduplicate and log collisions.

Apply:

-    workspacesToUpdate.push({
-      id: workspace.id,
-      name: workspace.name,
-      ssoEmailDomain: emailDomain,
-    });
+    workspacesToUpdate.push({
+      id: workspace.id,
+      name: workspace.name,
+      ssoEmailDomain: emailDomain,
+    });
+
+  // Collapse by domain and flag collisions
+  const byDomain = new Map<string, { id: string; name: string }[]>();
+  for (const w of workspacesToUpdate) {
+    const arr = byDomain.get(w.ssoEmailDomain) ?? [];
+    arr.push({ id: w.id, name: w.name });
+    byDomain.set(w.ssoEmailDomain, arr);
+  }
+  for (const [domain, owners] of byDomain) {
+    if (owners.length > 1) {
+      console.warn(
+        `Collision on domain "${domain}" across ${owners.length} workspaces:`,
+        owners.map((o) => o.name).join(", "),
+      );
+    }
+  }

45-56: Add an --apply switch and safe update loop

Provide a dry-run by default. On --apply, update sequentially and handle unique violations gracefully.

Apply:

-  // await Promise.allSettled(
-  //   workspacesToUpdate.map((workspace) =>
-  //     prisma.project.update({
-  //       where: {
-  //         id: workspace.id,
-  //       },
-  //       data: {
-  //         ssoEmailDomain: workspace.ssoEmailDomain,
-  //       },
-  //     }),
-  //   ),
-  // );
+  if (process.argv.includes("--apply")) {
+    for (const workspace of workspacesToUpdate) {
+      try {
+        await prisma.project.update({
+          where: { id: workspace.id },
+          data: { ssoEmailDomain: workspace.ssoEmailDomain },
+        });
+        console.log(
+          `Updated ${workspace.name} (${workspace.id}) -> ${workspace.ssoEmailDomain}`,
+        );
+      } catch (err: any) {
+        console.error(
+          `Failed to update ${workspace.name} (${workspace.id}): ${err?.code || err?.message}`,
+        );
+      }
+    }
+  } else {
+    console.log("Dry-run. Pass --apply to write changes.");
+  }
apps/web/lib/api/workspaces/is-saml-enforced-for-email-domain.ts (1)

9-9: Normalize domains with toLowerCase and simplify return

  • Use locale-invariant lowercasing for email domains to avoid locale pitfalls and to be consistent across code.
  • Simplify the final return.

Apply:

-  const emailDomain = email.split("@")[1].toLocaleLowerCase();
+  const emailDomain = email.split("@")[1]?.toLowerCase();
-  if (workspace?.ssoEnforcedAt) {
-    return true;
-  }
-
-  return false;
+  return Boolean(workspace?.ssoEnforcedAt);

Also applies to: 29-34

apps/web/lib/auth/options.ts (1)

430-441: Safer domain parsing and locale‑invariant comparison

Use optional chaining and toLowerCase to avoid locale issues; bail out if parsing fails.

Apply:

-          const emailDomain = user.email.split("@")[1];
+          const emailDomain = user.email?.split("@")[1]?.toLowerCase();
@@
-          if (
-            emailDomain.toLocaleLowerCase() !==
-            ssoEmailDomain.toLocaleLowerCase()
-          ) {
+          if (!emailDomain) {
+            return false;
+          }
+          if (emailDomain !== ssoEmailDomain.toLowerCase()) {
             return false;
           }
apps/web/lib/zod/schemas/workspaces.ts (1)

125-127: Add descriptions for new fields; confirm public exposure is intended

Add .describe for API docs clarity. Also confirm that exposing ssoEmailDomain on the base (public) schema is intended.

Apply:

-    ssoEmailDomain: z.string().nullable(),
-    ssoEnforcedAt: z.date().nullable(),
+    ssoEmailDomain: z
+      .string()
+      .nullable()
+      .describe("Email domain associated with SAML SSO for this workspace."),
+    ssoEnforcedAt: z
+      .date()
+      .nullable()
+      .describe("Timestamp when SAML SSO enforcement was enabled."),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8afb17e and 9982e41.

📒 Files selected for processing (8)
  • apps/web/app/api/workspaces/[idOrSlug]/route.ts (2 hunks)
  • apps/web/app/api/workspaces/[idOrSlug]/saml/route.ts (3 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/security/saml.tsx (4 hunks)
  • apps/web/lib/api/workspaces/is-saml-enforced-for-email-domain.ts (2 hunks)
  • apps/web/lib/auth/options.ts (1 hunks)
  • apps/web/lib/zod/schemas/workspaces.ts (1 hunks)
  • apps/web/scripts/migrations/backfill-saml-sso.ts (1 hunks)
  • packages/prisma/schema/workspace.prisma (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/security/saml.tsx (1)
apps/web/lib/openapi/workspaces/update-workspace.ts (1)
  • updateWorkspace (9-42)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (7)
packages/prisma/schema/workspace.prisma (1)

44-52: Deprecate unused ssoEnabled; confirm ssoEmailDomain uniqueness

  • ssoEnabled is never referenced—add a deprecation doc comment or remove it.
  • ssoEmailDomain has a @unique constraint, disallowing multiple workspaces per domain; verify this business rule before backfilling or enabling SAML.
-  ssoEnabled        Boolean   @default(false) // TODO: this is not used
+  /// @deprecated Not used; scheduled for removal.
+  ssoEnabled        Boolean   @default(false)
apps/web/app/api/workspaces/[idOrSlug]/route.ts (2)

146-148: Setting ssoEnforcedAt looks correct.

The conditional logic properly sets ssoEnforcedAt to the current timestamp when enforcement is enabled and clears it when disabled. This aligns with the new timestamp-based enforcement model.


213-217: Only slug conflicts possible here; conflict message is accurate
Project has two unique fields (slug, inviteCode) but this route only updates slug, so a P2002 can only stem from slug.

apps/web/app/api/workspaces/[idOrSlug]/saml/route.ts (1)

107-108: Correctly clearing both SAML fields on deletion.

The DELETE handler properly clears both ssoEmailDomain and ssoEnforcedAt when removing a SAML connection, ensuring consistent state.

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/security/saml.tsx (3)

23-23: Good: Using ssoEmailDomain from workspace context.

Correctly retrieving ssoEmailDomain from the workspace hook, which ensures it's available for display purposes even though enforcement is now tracked separately via ssoEnforcedAt.


208-215: UI correctly uses ssoEnforcedAt for conditional rendering.

The badge display is properly gated by ssoEnforcedAt and shows the ssoEmailDomain from the workspace context. This provides a clear indication of enforcement status and affected domain.


219-219: Switch state correctly reflects enforcement status.

The switch correctly uses ssoEnforcedAt !== null to determine if SAML enforcement is active, aligning with the new timestamp-based approach.

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 (1)
apps/web/lib/auth/options.ts (1)

430-442: Guard against malformed email domains before lowercasing

If user.email lacks a second @ segment, .split("@")[1] is undefined, so calling .toLocaleLowerCase() will throw and reject the SAML sign-in. Deriving the domain with .pop()?.trim() and short-circuiting when it’s missing makes this path resilient while still enforcing the match.

-          const emailDomain = user.email.split("@")[1];
+          const emailDomain = user.email.split("@").pop()?.trim();
+
+          if (!emailDomain) {
+            return false;
+          }
 
           // ssoEmailDomain should be required for all SAML enabled workspace
           // this should not happen
           if (!ssoEmailDomain) {
             return false;
           }
 
           if (
-            emailDomain.toLocaleLowerCase() !==
-            ssoEmailDomain.toLocaleLowerCase()
+            emailDomain.toLowerCase() !==
+            ssoEmailDomain.trim().toLowerCase()
           ) {
             return false;
           }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9982e41 and 6ffa4f1.

📒 Files selected for processing (1)
  • apps/web/lib/auth/options.ts (1 hunks)

@steven-tey steven-tey merged commit 3c86c6f into main Oct 3, 2025
8 of 9 checks passed
@steven-tey steven-tey deleted the fix-saml branch October 3, 2025 23:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants