Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat: implement SCIM handler for SCIM 2.0 compliance - #25572

Merged
Emyrk merged 34 commits into
mainfrom
stevenmasley/scim2
May 28, 2026
Merged

feat: implement SCIM handler for SCIM 2.0 compliance#25572
Emyrk merged 34 commits into
mainfrom
stevenmasley/scim2

Conversation

@Emyrk

@Emyrk Emyrk commented May 21, 2026

Copy link
Copy Markdown
Member

Rewrites the SCIM 2.0 user provisioning handler to be RFC 7644 compliant. Verified against an external SCIM compliance suite (16/16 checks).

The previous handler did not return queried users, did not support PATCH or filter expressions, omitted /Schemas, /ResourceTypes, and /ServiceProviderConfig discovery, and could only handle a narrow Okta-shaped happy path. The old handler remains the default behind the CODER_SCIM_USE_LEGACY flag (hidden, default true); set it to false to opt into the new SCIM 2.0 handler. Once it has run in production for a while, the default will flip and the legacy handler will be removed.

SCIM provisioning now runs under a dedicated SubjectSCIMProvisioner RBAC subject (dbauthz.AsSCIMProvisioner) scoped to user CRUD. CreateUser is additionally escalated to AsSystemRestricted because the implicit member role assignment is gated by the static assignRoles whitelist, which has no scim entry.

Refs PLAT-148, PLAT-225
Refs https://linear.app/codercom/project/refactor-and-fully-implement-scim-20-specification-d641c8c7ab92

Reviewer notes

What's new

  • enterprise/coderd/scim/ package replaces the old single-file handler under enterprise/coderd/.
  • /Schemas, /ResourceTypes, /ServiceProviderConfig discovery endpoints.
  • GET /Users supports filter, attributes, excludedAttributes, paging.
  • PATCH /Users/{id} (path/value/replace ops) and PUT /Users/{id}.
  • GET /Users/{id} and DELETE /Users/{id} consistent with the spec.
  • New ExactUsername SQL query for SCIM lookups; no migrations.

Authorization

  • subjectSCIM / AsSCIMProvisioner is a dedicated RBAC subject scoped to user CRUD.
  • AsSystemRestricted wraps CreateUser only, because the member role assignment is bypassed by the assignRoles whitelist.
  • enterprise/audit/table.go updated for the new resource interactions.

Caveats (Okta)

  • Okta requires FamilyName. Coder does not split names into GivenName / FamilyName.
  • Okta defaults the SCIM username to the email. Coder normalizes that to a valid username, and the transformation is not reversible.

Stack

Generated by @Emyrk in collaboration with Coder Agents.
Manual QA with okta by @Emyrk

@Emyrk
Emyrk marked this pull request as draft May 21, 2026 14:50
@Emyrk Emyrk changed the title feat: Scim 2.0 feat!: Scim 2.0 May 21, 2026
@Emyrk
Emyrk force-pushed the stevenmasley/scim2 branch from 13b61c3 to 701ef7e Compare May 21, 2026 21:19
@Emyrk
Emyrk force-pushed the stevenmasley/scim2 branch from 2d550a3 to a0fd5b3 Compare May 22, 2026 14:32
@@ -0,0 +1,595 @@
package scim

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is the main file to review. Most of the logic came from the legacy implementation

Emyrk added 10 commits May 22, 2026 09:58
…ResourceUser

- Replace: dbUser was discarded after updateUserStatus, so the SCIM
  response carried the stale pre-update row. Capture the updated row.
- GetUserCount dbauthz test: authorization moved from ResourceSystem
  to ResourceUser; update the assertion to match.

Generated by Coder Agents.
…pidocs

- legacyscim swag refs: rename coderd.SCIMUser to legacyscim.SCIMUser
  in @Param/@success comments. The type moved when the legacy handler
  was extracted into its own package, but the annotations were not
  updated, so swag init strict mode failed and make gen could not
  produce _gen/manifest-staging.json.
- scim.go gocritic nolint: drop the stray space after //nolint:. The
  malformed directive was parsed as an unknown linter name and did
  not suppress the AsSCIMProvisioner rule, breaking lint/go.
- Regenerate coderd/apidoc and docs/reference/api to pick up the
  legacyscim.SCIMUser rename and the new scim_use_legacy deployment
  option that was already added to codersdk.

Generated by Coder Agents.
…ates users

ResourceUser.Create called CreateUser with the SCIM provisioner context.
CreateUser invokes InsertUser, which always implicitly assigns the member
role, and may also call InsertOrganizationMember when org sync assigns the
default org. Neither role can be assigned by the SCIM provisioner role,
so creating a user through SCIM returned 500 with 'not authorized to
assign role "member"'.

The legacy SCIM handler wrapped the same call in dbauthz.AsSystemRestricted;
the new handler had a comment promising the same wrapper but the wrapper
itself was missing. Restore the legacy behavior so SCIM POST /Users
succeeds and refresh the comment to describe what the code actually does.

Verified with scimverify (https://verify.scim.dev/): 16/16 tests pass,
up from 11/13 (2 user-create failures) before this fix.

Generated by Coder Agents.
@Emyrk Emyrk changed the title feat!: Scim 2.0 feat!: implement SCIM handler for SCIM 2.0 compliance May 22, 2026
@Emyrk
Emyrk marked this pull request as ready for review May 22, 2026 19:02
@github-actions github-actions Bot added the release/breaking This label is applied to PRs to detect breaking changes as part of the release process label May 22, 2026
@coderagents

coderagents Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Documentation Check

Updates Needed

  • docs/admin/users/oidc-auth/index.md - The SCIM section (lines 137-162) states the implementation is "not a fully certified or guaranteed implementation of the SCIM 2.0 specification" and references issue Refactor & fully implement SCIM 2.0 specification #15830 for "tracked gaps and ongoing work." This PR rewrites the handler to be RFC 7644 compliant (16/16 compliance checks). The disclaimer should be updated to reflect the improved compliance posture, and the Refactor & fully implement SCIM 2.0 specification #15830 reference should be re-evaluated.
  • docs/admin/users/oidc-auth/index.md - Document new SCIM capabilities added by this PR: GET /Users with filter/attributes/paging support, PATCH /Users/{id}, PUT /Users/{id}, and discovery endpoints (/Schemas, /ResourceTypes, /ServiceProviderConfig). The current docs only mention provisioning/deprovisioning; IdP administrators need to know which operations are now supported.
  • docs/admin/users/oidc-auth/index.md - Document the CODER_SCIM_USE_LEGACY environment variable. Although the flag is hidden and defaults to false, existing deployments upgrading from the old handler should know this rollback option exists during the transition period. A brief note in the SCIM section (e.g., "If you encounter issues after upgrading, set CODER_SCIM_USE_LEGACY=true to revert to the previous handler") would help operators.

Automated review via Coder Agents

Comment thread enterprise/coderd/scim/scimusers.go Outdated
Comment thread enterprise/coderd/scim/scimusers.go Outdated
var activeSet *bool
for _, op := range operations {
switch op.Op {
case "add":

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Did you mean to add handling for this add case?

Comment thread enterprise/coderd/scim/scimusers.go Outdated
Comment thread enterprise/coderd/scim/users.go
Comment thread enterprise/coderd/scim/users.go
err = ru.Delete(r, user.ID.String())
require.NoError(t, err)

// Step 7: Get → confirm inactive after delete.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we want a 404 here:

Service providers MAY choose not to permanently delete the resource, e.g., for audit purposes. However, in such cases, the service provider MUST return a 404 (Not Found) error code for all operations associated with the previously deleted resource.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is a disconnect from our user model. We never delete users, we just mark them as Suspended (not active in SCIM). To return a 404 would make the Create flow return an error if they do not exist.

I don't have a good solution here...

Comment thread enterprise/coderd/scim/scimusers.go Outdated
Comment thread enterprise/coderd/scim_test.go
Comment thread enterprise/coderd/scim/expression.go
Comment thread codersdk/deployment.go Outdated
Comment on lines +3448 to +3449
Default: "false",
// TODO: When SCIM 2.0 has been tested more, flip this to false to default to the new scim
Default: "true",

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I default to this new behavior disabled. Needs to be opted into for now

Emyrk added 4 commits May 26, 2026 11:13
The suspended-user recreate branch in b3e6e0a discarded the row
returned by updateUserStatus, so userResource was built from the
pre-update (still suspended) row and the SCIM response reported
active=false even though the DB row was now dormant.

Capture the updated row, matching the fix applied to Replace in
aa3e1f5. Add TestResourceUser_Create covering the conflict,
reactivation, and no-op paths.

Generated by Coder Agents.
Adds TestScim, mirroring TestLegacyScim's structure (disabled, noAuth,
post, patch, put) and covering behavior unique to the v2 implementation:
discovery endpoints, 409 Conflict on duplicate active users, suspended
user reactivation on recreate, GET by id, and DELETE.

Requests use a SCIM 2.0 compliant body (with the 'schemas' attribute)
because the elimity-com/scim library validates against the core User
schema URI and rejects bodies that omit it.

Generated by Coder Agents.
@Emyrk

Emyrk commented May 26, 2026

Copy link
Copy Markdown
Member Author

Manually verified deactivating a user on Okta suspended them in Coder

@Emyrk Emyrk changed the title feat!: implement SCIM handler for SCIM 2.0 compliance feat: implement SCIM handler for SCIM 2.0 compliance May 27, 2026
@github-actions github-actions Bot removed the release/breaking This label is applied to PRs to detect breaking changes as part of the release process label May 27, 2026
@Emyrk
Emyrk requested a review from zedkipp May 27, 2026 15:12
@Emyrk

Emyrk commented May 27, 2026

Copy link
Copy Markdown
Member Author

/coder-agents-review

@coder-agents-review

coder-agents-review Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

Chat: Review posted | View chat
Requested: 2026-05-27 16:09 UTC by @Emyrk
Spend: $3.73 / $100.00

Review history
  • R1 (2026-05-27), 1 Nit, 1 P2, 2 P3, COMMENT. Review

deep-review v0.5.0 | Round 1 | 0d9718e..9cc868b

Last posted: Round 1, 4 findings (1 P2, 2 P3, 1 Nit), COMMENT. Review

Finding inventory

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P2 Open dbauthz.go:897 Doc comment on AsSCIMProvisioner names wrong function (copy-paste from AsSubAgentAPI) R1 Netero Yes
CRF-2 Nit Open authz.go:88 SubjectSCIMProvisioner omits SubjectType prefix, breaking naming convention R1 Netero Yes
CRF-3 P3 Open expression.go:13 userQuery has no test coverage (3 exit points, 0 tests) R1 Netero Yes
CRF-4 P3 Open users.go:314 Patch silently drops add operations for active field (RFC 7644 3.5.2.1 violation) R1 Netero Yes

Contested and acknowledged

(None yet.)

Round log

Round 1

Netero-only. 1 P2, 2 P3, 1 Nit. Reviewed against 0d9718e..9cc868b.

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Kurapika security
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

First-pass review (Netero). 1 P2, 2 P3, 1 Nit.

This is a mechanical first pass only. The full review panel has not yet reviewed this PR. These are defects the author should address before the panel spends parallel review time.

The SCIM 2.0 rewrite is well-structured: clean separation between the new handler package and the legacy code, healthy test-to-code ratio, and the feature flag approach is solid for a gradual rollout. The AsSCIMProvisioner RBAC subject with scoped permissions is the right pattern.

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/database/dbauthz/dbauthz.go Outdated
Comment thread coderd/rbac/authz.go Outdated
Comment thread enterprise/coderd/scim/expression.go
var activeSet *bool
for _, op := range operations {
switch op.Op {
case "add":

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3 [CRF-4] The add case is empty. If an IdP sends {"op": "add", "path": "active", "value": true}, it is silently dropped and the user's status does not change. Per RFC 7644 section 3.5.2.1, add on a singular attribute that already exists should behave like replace. Okta uses replace, but Azure AD and OneLogin may use add.

Netero

🤖

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added a comment

Comment thread enterprise/coderd/scim/users.go Outdated
@Emyrk
Emyrk merged commit 4591212 into main May 28, 2026
30 of 33 checks passed
@Emyrk
Emyrk deleted the stevenmasley/scim2 branch May 28, 2026 15:00
Emyrk added a commit that referenced this pull request May 29, 2026
Add a shell-based wrapper around the scimverify CLI (https://verify.scim.dev/) that runs SCIM 2.0 compliance checks against a live Coder dev server with SCIM enabled.

The script (scripts/scimverify/run.sh) creates a temporary test user, generates a config that targets that user for PUT/PATCH/DELETE operations (rather than scimverify's default 'AUTO' which picks the admin), runs the full suite, then cleans up. After the suite finishes it also POSTs the same body again to verify Coder's reactivate-on-recreate behavior, which the upstream scimverify suite does not cover.

config.yaml seeds the suite with five POST cases that exercise: minimal body, multi-email primary-email selection, and initial active=false.

README.md documents prerequisites, usage, and how to read the TAP output.

This work originally lived alongside the SCIM 2.0 implementation that landed in #25572. This commit isolates the verification harness so it can land on top of current main without dragging the now-duplicated SCIM Go code with it.

Generated by Coder Agents.
Emyrk added a commit that referenced this pull request May 29, 2026
Add a shell-based wrapper around the scimverify CLI (https://verify.scim.dev/) that runs SCIM 2.0 compliance checks against a live Coder dev server with SCIM enabled.

The script (scripts/scimverify/run.sh) creates a temporary test user, generates a config that targets that user for PUT/PATCH/DELETE operations (rather than scimverify's default 'AUTO' which picks the admin), runs the full suite, then cleans up. After the suite finishes it also POSTs the same body again to verify Coder's reactivate-on-recreate behavior, which the upstream scimverify suite does not cover.

config.yaml seeds the suite with five POST cases that exercise: minimal body, multi-email primary-email selection, and initial active=false.

README.md documents prerequisites, usage, and how to read the TAP output.

This work originally lived alongside the SCIM 2.0 implementation that landed in #25572. This commit isolates the verification harness so it can land on top of current main without dragging the now-duplicated SCIM Go code with it.

Generated by Coder Agents.
Emyrk added a commit that referenced this pull request Jul 27, 2026
Documents the SCIM 2.0 handler introduced in #25572 and how to opt in.

Adds a "SCIM 2.0 handler" subsection to the SCIM section of
`docs/admin/users/oidc-auth/index.md`:

- The handler follows RFC 7644 and supports user
provisioning/deprovisioning and user listing.
- Opt in with `CODER_SCIM_USE_LEGACY=false` (also `--scim-use-legacy` /
`scimUseLegacy`); requires a server restart.
- Behavior notes: delete/deactivate suspends (never hard-deletes),
reactivation goes through dormant, usernames are immutable.
- Notes it will eventually become the default behavior.

Behavior details were verified against
`enterprise/coderd/scimroutes.go`, `enterprise/coderd/scim/`, and the
`SCIM Use Legacy` option in `codersdk/deployment.go`.

`make lint/markdown` and `make lint/emdash` pass.

---

Generated by Coder Agents on behalf of @Emyrk.

---------

Co-authored-by: Nick Vigilante <[email protected]>
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