Skip to content

feat: add OIDC workspace grant mapping - #2395

Merged
yohamta0 merged 8 commits into
mainfrom
feat/idp-workspace-grants
Jul 20, 2026
Merged

yohamta0 merged 8 commits into
mainfrom
feat/idp-workspace-grants

Conversation

@yohamta0

@yohamta0 yohamta0 commented Jul 20, 2026 •

Copy link
Copy Markdown
Member

Summary

Add identity-provider group mappings for workspace-scoped authorization while preserving safe administrative operations and current session behavior.

Changes

  • Map OIDC groups to workspace grants with strict and fallback access policies.
  • Synchronize roles and workspace access at sign-in, including safe zero-grant users.
  • Expose SSO-managed authorization state in the users API and interface.
  • Add configuration, schema, environment, backend, and frontend coverage.

Why

Organizations need identity-provider-controlled workspace access without granting global workspace visibility.

Behavior

  • Global role mappings replace scoped grants.
  • Unmatched users can default to all workspaces or no named workspaces.
  • Unknown workspaces remain dormant and nonfatal.
  • Disabling organization-role synchronization preserves stored authorization.

Related Issues

No linked issue.

Testing

  • make test
  • Relevant Go race-enabled package tests
  • pnpm test
  • pnpm typecheck
  • pnpm build
  • API generation and validation
  • .local/bin/golangci-lint run --timeout=10m ./...

Checklist

  • Code follows the project style guidelines
  • Self-review of the code has been performed
  • Tests have been added or updated as needed
  • Documentation has been updated as needed
  • Changes have been tested locally

Summary by CodeRabbit

  • New Features

    • Added workspace-scoped OIDC role/workspace access mappings with a configurable default access mode (all/none).
    • Users API now reports whether OIDC workspace access synchronization is enabled.
    • UI shows “Managed by SSO” for synced OIDC users and renders workspace access read-only.
    • Added fail-closed gating for builtin OIDC login/callback until initial setup is complete.
  • Bug Fixes

    • OIDC-managed users can’t change/reset passwords (now returns a 403; password actions removed from the menu).
    • Fixed authorization sync to prevent stale user-list data and keep workspace permissions consistent after updates.

@yohamta0
yohamta0 marked this pull request as ready for review July 20, 2026 05:23
@coderabbitai

coderabbitai Bot commented Jul 20, 2026 •

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c77ff48e-2514-4c7e-a357-8d0f702b451e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

OIDC role mappings now support workspace-scoped grants and default access policies. Login provisioning synchronizes roles and workspace access, password operations are restricted for OIDC users, setup gating is added, APIs report synchronization state, and the frontend displays provider-controlled authorization fields.

Changes

OIDC workspace authorization

Layer / File(s) Summary
Configuration contracts and loading
internal/cmn/config/*, internal/cmn/schema/*
OIDC workspace grants, defaults, validation, schema definitions, environment loading, legacy key mappings, and validation tests are added.
Workspace-aware role mapping
internal/service/oidcprovision/rolemapper.go, internal/service/oidcprovision/rolemapper_test.go
Role mapping compiles workspace grants, merges matching groups by role priority, applies defaults, and enforces strict matching and workspace validation.
Authorization provisioning and authentication controls
internal/service/oidcprovision/service.go, internal/service/auth/*
OIDC login creates and synchronizes workspace access, canonicalizes grants, checks workspace existence, and rejects password operations for OIDC users.
Server wiring and API contracts
internal/service/frontend/server.go, internal/service/frontend/auth/*, internal/service/frontend/api/v1/*, api/v1/*, ui/src/api/v1/schema.ts
Workspace stores are wired into provisioning, OIDC handlers require completed setup, and API contracts expose synchronization state and OIDC password-management errors.

Managed user interface

Layer / File(s) Summary
Managed user display and editing
ui/src/pages/users/*, ui/src/components/WorkspaceAccessEditor.tsx, ui/src/components/UserMenu.tsx, ui/src/components/__tests__/*
Managed OIDC users show provider-controlled role and workspace access summaries, hide local password actions, submit username-only edits, and ignore stale user-list refresh responses.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant OIDCProvider
  participant RoleMapper
  participant OIDCProvisioning
  participant UserStore
  participant UsersAPI
  participant UsersPage
  OIDCProvider->>RoleMapper: send role and group claims
  RoleMapper->>OIDCProvisioning: return role and workspace access
  OIDCProvisioning->>UserStore: synchronize user authorization
  UsersPage->>UsersAPI: request users
  UsersAPI-->>UsersPage: users and sync-enabled flag
  UsersPage->>UsersPage: render managed OIDC state
Loading

Possibly related PRs

  • dagucloud/dagu#2000: Updates the related builtin OIDC login and callback gating flow.
  • dagucloud/dagu#2019: Introduces WorkspaceAccess behavior used by the authorization update paths.
  • dagucloud/dagu#2199: Modifies the same password-management service flows affected by OIDC restrictions.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately describes the main change: adding OIDC workspace grant mapping.
Description check ✅ Passed The description matches the template and includes summary, changes, related issues, testing, and checklist sections.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/idp-workspace-grants

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
internal/cmn/config/loader.go (1)

1634-1634: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant env binding for workspace_mappings.

auth.oidc.role_mapping.workspace_mappings is also populated by loadOIDCWorkspaceMappingsEnv() via l.v.Set(...), which takes precedence over this binding, so line 1634 is effectively dead. It's harmless today, but if someone later removes the custom parser assuming this binding covers it, JSON-map parsing would silently break (the raw string wouldn't unmarshal into map[string][]OIDCWorkspaceGrant). Consider dropping this binding or adding a comment cross-referencing the custom parser.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cmn/config/loader.go` at line 1634, Remove the redundant
auth.oidc.role_mapping.workspace_mappings entry from the environment binding
list, since loadOIDCWorkspaceMappingsEnv already populates this setting through
l.v.Set. Keep the custom parser as the sole handling path for
AUTH_OIDC_WORKSPACE_MAPPINGS.
ui/src/pages/users/UserFormModal.tsx (2)

74-85: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Magic string 'oidc' used instead of the generated UserAuthProvider enum. Both sites compare user.authProvider against the literal 'oidc' rather than the enum exported from @/api/v1/schema, losing compile-time safety if the schema value ever changes.

  • ui/src/pages/users/UserFormModal.tsx#L74-L85: replace user.authProvider === 'oidc' with user.authProvider === UserAuthProvider.oidc (import the enum from @/api/v1/schema).
  • ui/src/pages/users/index.tsx#L42-L56: replace user.authProvider !== 'oidc' with user.authProvider !== UserAuthProvider.oidc; note the local UserAuthProvider function component must be renamed first to avoid a naming collision with the imported enum.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/pages/users/UserFormModal.tsx` around lines 74 - 85, The auth-provider
comparisons use a magic string instead of the generated enum. In
ui/src/pages/users/UserFormModal.tsx:74-85, import UserAuthProvider from
`@/api/v1/schema` and compare against UserAuthProvider.oidc; in
ui/src/pages/users/index.tsx:42-56, rename the local UserAuthProvider component
to avoid the collision, import the generated enum, and use UserAuthProvider.oidc
for the comparison.

135-158: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated role-resolution ternary.

effectiveRole = workspaceAccess.all ? role : UserRole.viewer (line 138) duplicates the identical expression driving the Role Select's value at line 233. If either expression is edited independently in the future, the displayed role and the submitted role could silently diverge.

♻️ Proposed fix: single source of truth
+  const effectiveRole = workspaceAccess.all ? role : UserRole.viewer;
+
   const handleSubmit = async (e: React.FormEvent) => {
     ...
-      const effectiveRole = workspaceAccess.all ? role : UserRole.viewer;
       const payload = managedBySSO
               <Select
-                value={workspaceAccess.all ? role : UserRole.viewer}
+                value={effectiveRole}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/pages/users/UserFormModal.tsx` around lines 135 - 158, Define the
resolved role expression once in the UserFormModal component and reuse that
value for both the Role Select's value and the request payload's effectiveRole,
replacing the duplicate workspaceAccess.all ternary while preserving the
existing viewer fallback.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/cmn/config/loader.go`:
- Line 1634: Remove the redundant auth.oidc.role_mapping.workspace_mappings
entry from the environment binding list, since loadOIDCWorkspaceMappingsEnv
already populates this setting through l.v.Set. Keep the custom parser as the
sole handling path for AUTH_OIDC_WORKSPACE_MAPPINGS.

In `@ui/src/pages/users/UserFormModal.tsx`:
- Around line 74-85: The auth-provider comparisons use a magic string instead of
the generated enum. In ui/src/pages/users/UserFormModal.tsx:74-85, import
UserAuthProvider from `@/api/v1/schema` and compare against UserAuthProvider.oidc;
in ui/src/pages/users/index.tsx:42-56, rename the local UserAuthProvider
component to avoid the collision, import the generated enum, and use
UserAuthProvider.oidc for the comparison.
- Around line 135-158: Define the resolved role expression once in the
UserFormModal component and reuse that value for both the Role Select's value
and the request payload's effectiveRole, replacing the duplicate
workspaceAccess.all ternary while preserving the existing viewer fallback.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 11aad743-6071-4ce1-9d73-2cde13bfab10

📥 Commits

Reviewing files that changed from the base of the PR and between 85203e2 and 2062684.

📒 Files selected for processing (25)
  • api/v1/api.gen.go
  • api/v1/api.yaml
  • internal/cmn/config/config.go
  • internal/cmn/config/config_test.go
  • internal/cmn/config/definition.go
  • internal/cmn/config/key_hints.go
  • internal/cmn/config/loader.go
  • internal/cmn/config/loader_test.go
  • internal/cmn/schema/config.schema.json
  • internal/cmn/schema/config_schema_test.go
  • internal/service/auth/service.go
  • internal/service/auth/service_test.go
  • internal/service/frontend/api/v1/users.go
  • internal/service/frontend/api/v1/users_internal_test.go
  • internal/service/frontend/server.go
  • internal/service/oidcprovision/rolemapper.go
  • internal/service/oidcprovision/rolemapper_test.go
  • internal/service/oidcprovision/service.go
  • internal/service/oidcprovision/service_test.go
  • ui/src/api/v1/schema.ts
  • ui/src/components/WorkspaceAccessEditor.tsx
  • ui/src/components/__tests__/WorkspaceAccessEditor.test.tsx
  • ui/src/pages/users/UserFormModal.tsx
  • ui/src/pages/users/__tests__/index.test.tsx
  • ui/src/pages/users/index.tsx

Copy link
Copy Markdown
Member Author

Addressed the valid review feedback in 33aac5fd2:

  • removed the redundant generic env binding so AUTH_OIDC_WORKSPACE_MAPPINGS is handled only by its JSON-aware parser
  • replaced OIDC auth-provider string literals with the generated enum and renamed the local display component
  • centralized effective-role resolution for display and submission
  • updated the PR description to match the repository template

Validation after the changes: config tests, all 429 UI tests, TypeScript typecheck, Prettier, ESLint, and full Go lint (0 issues) pass.

The generic docstring-coverage warning was not applied: this repository requires comments to describe caller-facing API contracts, and adding low-value comments to satisfy a percentage would conflict with that standard.

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 20, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@yohamta0

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 20, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@yohamta0

Copy link
Copy Markdown
Member Author

@copilot full review

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/cmn/schema/config.schema.json (1)

421-436: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Mirror the reserved workspace-name check in the schema.
workspace currently accepts names like all, default, and global, but workspace.ValidateName rejects them at runtime. Add the reserved-name exclusion here so schema-based tooling matches cfg.Validate().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cmn/schema/config.schema.json` around lines 421 - 436, The
OIDCWorkspaceGrantDef workspace schema pattern does not exclude reserved names
rejected by workspace.ValidateName. Update the workspace property’s validation
pattern to reject all, default, and global while preserving the existing
allowed-character constraints.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ui/src/components/WorkspaceAccessEditor.tsx`:
- Around line 59-97: Update the styling in the WorkspaceAccessEditor render
paths: replace information-block backgrounds with bg-slate-200
dark:bg-slate-700, use text-slate-500 dark:text-slate-500 for muted labels and
availability text, and text-slate-800 dark:text-slate-200 for primary workspace
names. In the grants list, replace truncate with whitespace-normal break-words
so long workspace names wrap instead of being clipped.

---

Nitpick comments:
In `@internal/cmn/schema/config.schema.json`:
- Around line 421-436: The OIDCWorkspaceGrantDef workspace schema pattern does
not exclude reserved names rejected by workspace.ValidateName. Update the
workspace property’s validation pattern to reject all, default, and global while
preserving the existing allowed-character constraints.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e7274809-966f-428e-b54d-1ac517e5650d

📥 Commits

Reviewing files that changed from the base of the PR and between 85203e2 and 3bb3ba9.

📒 Files selected for processing (32)
  • api/v1/api.gen.go
  • api/v1/api.yaml
  • internal/cmn/config/config.go
  • internal/cmn/config/config_test.go
  • internal/cmn/config/definition.go
  • internal/cmn/config/key_hints.go
  • internal/cmn/config/loader.go
  • internal/cmn/config/loader_test.go
  • internal/cmn/schema/config.schema.json
  • internal/cmn/schema/config_schema_test.go
  • internal/service/auth/service.go
  • internal/service/auth/service_test.go
  • internal/service/frontend/api/v1/auth.go
  • internal/service/frontend/api/v1/auth_internal_test.go
  • internal/service/frontend/api/v1/users.go
  • internal/service/frontend/api/v1/users_internal_test.go
  • internal/service/frontend/auth/oidc.go
  • internal/service/frontend/auth/oidc_test.go
  • internal/service/frontend/server.go
  • internal/service/frontend/server_test.go
  • internal/service/oidcprovision/rolemapper.go
  • internal/service/oidcprovision/rolemapper_test.go
  • internal/service/oidcprovision/service.go
  • internal/service/oidcprovision/service_test.go
  • ui/src/api/v1/schema.ts
  • ui/src/components/UserMenu.tsx
  • ui/src/components/WorkspaceAccessEditor.tsx
  • ui/src/components/__tests__/UserMenu.test.tsx
  • ui/src/components/__tests__/WorkspaceAccessEditor.test.tsx
  • ui/src/pages/users/UserFormModal.tsx
  • ui/src/pages/users/__tests__/index.test.tsx
  • ui/src/pages/users/index.tsx

Comment on lines +59 to +97
<div className="rounded-md border border-border bg-muted/30 px-3 py-2 text-sm">
All workspaces
</div>
);
}

if (access.grants.length === 0) {
return (
<div className="rounded-md border border-border bg-muted/30 px-3 py-2 text-sm text-muted-foreground">
No named workspace grants
</div>
);
}

const knownWorkspaces = new Set(
workspaces?.map((workspace) => workspace.name)
);

return (
<div className="max-h-56 overflow-auto rounded-md border border-border">
{access.grants.map((grant) => {
const unavailable =
workspaces !== undefined && !knownWorkspaces.has(grant.workspace);
return (
<div
key={grant.workspace}
className="flex items-center justify-between gap-3 border-b border-border px-3 py-2 text-sm last:border-b-0"
>
<div className="min-w-0">
<div className="truncate">{grant.workspace}</div>
{unavailable && (
<div className="text-xs text-muted-foreground">
Workspace not currently available
</div>
)}
</div>
<span className="rounded bg-muted px-1.5 py-0.5 text-xs capitalize text-muted-foreground">
{grant.role}
</span>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Update styling to comply with UI coding guidelines.

The current implementation uses bg-muted/30, text-muted-foreground, and truncate, which contradicts the explicit coding guidelines for the UI:

  • Backgrounds: Use bg-slate-200 dark:bg-slate-700 for similar information blocks.
  • Text Hierarchy: Use text-slate-500 dark:text-slate-500 for muted text, and text-slate-800 dark:text-slate-200 for primary text.
  • Long Text: Always handle long text in lists with whitespace-normal break-words instead of clipping it with truncate.
🎨 Proposed fixes for styling and text wrapping
-      <div className="rounded-md border border-border bg-muted/30 px-3 py-2 text-sm">
+      <div className="rounded-md border border-border bg-slate-200 dark:bg-slate-700 px-3 py-2 text-sm text-slate-800 dark:text-slate-200">
         All workspaces
       </div>
     );
   }
 
   if (access.grants.length === 0) {
     return (
-      <div className="rounded-md border border-border bg-muted/30 px-3 py-2 text-sm text-muted-foreground">
+      <div className="rounded-md border border-border bg-slate-200 dark:bg-slate-700 px-3 py-2 text-sm text-slate-500 dark:text-slate-500">
         No named workspace grants
       </div>
     );
   }
 
   const knownWorkspaces = new Set(
     workspaces?.map((workspace) => workspace.name)
   );
 
   return (
     <div className="max-h-56 overflow-auto rounded-md border border-border">
       {access.grants.map((grant) => {
         const unavailable =
           workspaces !== undefined && !knownWorkspaces.has(grant.workspace);
         return (
           <div
             key={grant.workspace}
             className="flex items-center justify-between gap-3 border-b border-border px-3 py-2 text-sm last:border-b-0"
           >
             <div className="min-w-0">
-              <div className="truncate">{grant.workspace}</div>
+              <div className="whitespace-normal break-words text-slate-800 dark:text-slate-200">{grant.workspace}</div>
               {unavailable && (
-                <div className="text-xs text-muted-foreground">
+                <div className="text-xs text-slate-500 dark:text-slate-500">
                   Workspace not currently available
                 </div>
               )}
             </div>
-            <span className="rounded bg-muted px-1.5 py-0.5 text-xs capitalize text-muted-foreground">
+            <span className="rounded bg-slate-200 dark:bg-slate-700 px-1.5 py-0.5 text-xs capitalize text-slate-500 dark:text-slate-500">
               {grant.role}
             </span>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="rounded-md border border-border bg-muted/30 px-3 py-2 text-sm">
All workspaces
</div>
);
}
if (access.grants.length === 0) {
return (
<div className="rounded-md border border-border bg-muted/30 px-3 py-2 text-sm text-muted-foreground">
No named workspace grants
</div>
);
}
const knownWorkspaces = new Set(
workspaces?.map((workspace) => workspace.name)
);
return (
<div className="max-h-56 overflow-auto rounded-md border border-border">
{access.grants.map((grant) => {
const unavailable =
workspaces !== undefined && !knownWorkspaces.has(grant.workspace);
return (
<div
key={grant.workspace}
className="flex items-center justify-between gap-3 border-b border-border px-3 py-2 text-sm last:border-b-0"
>
<div className="min-w-0">
<div className="truncate">{grant.workspace}</div>
{unavailable && (
<div className="text-xs text-muted-foreground">
Workspace not currently available
</div>
)}
</div>
<span className="rounded bg-muted px-1.5 py-0.5 text-xs capitalize text-muted-foreground">
{grant.role}
</span>
<div className="rounded-md border border-border bg-slate-200 dark:bg-slate-700 px-3 py-2 text-sm text-slate-800 dark:text-slate-200">
All workspaces
</div>
);
}
if (access.grants.length === 0) {
return (
<div className="rounded-md border border-border bg-slate-200 dark:bg-slate-700 px-3 py-2 text-sm text-slate-500 dark:text-slate-500">
No named workspace grants
</div>
);
}
const knownWorkspaces = new Set(
workspaces?.map((workspace) => workspace.name)
);
return (
<div className="max-h-56 overflow-auto rounded-md border border-border">
{access.grants.map((grant) => {
const unavailable =
workspaces !== undefined && !knownWorkspaces.has(grant.workspace);
return (
<div
key={grant.workspace}
className="flex items-center justify-between gap-3 border-b border-border px-3 py-2 text-sm last:border-b-0"
>
<div className="min-w-0">
<div className="whitespace-normal break-words text-slate-800 dark:text-slate-200">{grant.workspace}</div>
{unavailable && (
<div className="text-xs text-slate-500 dark:text-slate-500">
Workspace not currently available
</div>
)}
</div>
<span className="rounded bg-slate-200 dark:bg-slate-700 px-1.5 py-0.5 text-xs capitalize text-slate-500 dark:text-slate-500">
{grant.role}
</span>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/components/WorkspaceAccessEditor.tsx` around lines 59 - 97, Update the
styling in the WorkspaceAccessEditor render paths: replace information-block
backgrounds with bg-slate-200 dark:bg-slate-700, use text-slate-500
dark:text-slate-500 for muted labels and availability text, and text-slate-800
dark:text-slate-200 for primary workspace names. In the grants list, replace
truncate with whitespace-normal break-words so long workspace names wrap instead
of being clipped.

Source: Coding guidelines

Copilot AI commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

@copilot full review

Full review complete. Here's the summary:

CI status: The only failing CI run (TestEnqueueCatchupRun_PersistsQueuedCatchupMetadata on Windows) is in internal/service/scheduler, which is unchanged by this PR — it's a pre-existing Windows file-sync flakiness unrelated to this feature.

All targeted tests pass locally: oidcprovision (115 tests), config (306), auth service, frontend API, OIDC auth handler, schema validation.


Security

  • Admin-level workspace grants are correctly rejected in compileWorkspaceMappings at mapper construction time, not just at use time.
  • OIDC identity lookup uses both issuer + subject for uniqueness, preventing cross-provider identity confusion.
  • Strict mode (roleAttributeStrict) correctly rejects login when no mapping matches, including in syncUserAccess on re-login.
  • When workspace access policy is active, sync failures on re-login are fatal — preventing a user from retaining access if their IdP groups change.
  • OIDC users are blocked from password management at both the auth service layer and API handler layer.
  • requireAdmin guards all user management endpoints.

Correctness

  • syncUserAccess uses updated := *user (shallow copy), which is safe because WorkspaceAccess is replaced with CloneWorkspaceAccess(newAccess) before the update call — the pointer is not mutated in-place.
  • canonicalWorkspaceAccess sorts grants deterministically before equality comparison, ensuring idempotent sync decisions (no spurious store writes).
  • Zero workspace grants are intentionally permitted for OIDC-provisioned users in "none" mode. The OIDC provisioner writes directly to the user store, bypassing ValidateWorkspaceAccess (which requires at least one grant for scoped access). This is the correct path for the "no named workspace access" state.
  • When defaultWorkspaceAccess defaults to "all" (no workspace mappings configured), unmatched users receive their defaultRole with full workspace access — safe and backward-compatible.
  • Stale user-list responses are guarded correctly with the fetchSequence ref pattern in ui/src/pages/users/index.tsx.
  • Multiple group memberships correctly resolve to the highest-privilege role via rolePriority.

Edge cases handled

  • Duplicate workspace names within a group mapping → validation error at mapper construction.
  • Blank group names → rejected.
  • Nested claim paths (e.g., realm_access.roles for Keycloak) → supported via dot-notation in getNestedClaim.
  • Missing/nonexistent workspaces in OIDC mappings → logged as dormant (non-fatal for login).
  • Username collision during OIDC signup → retried up to 3 times with progressive suffixes.
  • Admin updating an OIDC-synced user's unrelated fields (e.g., isDisabled) → workspace access preserved; will be re-synced at next login.

No issues requiring changes were identified.

@yohamta0
yohamta0 merged commit fce1629 into main Jul 20, 2026
14 checks passed
@yohamta0
yohamta0 deleted the feat/idp-workspace-grants branch July 20, 2026 09:02
@coderabbitai coderabbitai Bot mentioned this pull request Jul 20, 2026
5 tasks done
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