feat: implement SCIM handler for SCIM 2.0 compliance - #25572
Conversation
| @@ -0,0 +1,595 @@ | |||
| package scim | |||
There was a problem hiding this comment.
This is the main file to review. Most of the logic came from the legacy implementation
…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.
Documentation CheckUpdates Needed
Automated review via Coder Agents |
| var activeSet *bool | ||
| for _, op := range operations { | ||
| switch op.Op { | ||
| case "add": |
There was a problem hiding this comment.
Did you mean to add handling for this add case?
| err = ru.Delete(r, user.ID.String()) | ||
| require.NoError(t, err) | ||
|
|
||
| // Step 7: Get → confirm inactive after delete. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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...
| Default: "false", | ||
| // TODO: When SCIM 2.0 has been tested more, flip this to false to default to the new scim | ||
| Default: "true", |
There was a problem hiding this comment.
I default to this new behavior disabled. Needs to be opted into for now
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.
|
Manually verified deactivating a user on Okta suspended them in Coder |
|
/coder-agents-review |
|
Chat: Review posted | View chat Review history
deep-review v0.5.0 | Round 1 | Last posted: Round 1, 4 findings (1 P2, 2 P3, 1 Nit), COMMENT. Review Finding inventoryFindings
Contested and acknowledged(None yet.) Round logRound 1Netero-only. 1 P2, 2 P3, 1 Nit. Reviewed against 0d9718e..9cc868b. About deep-reviewCRF = Coder Review Finding (P0-P4, Nit, Note)
|
There was a problem hiding this comment.
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.
| var activeSet *bool | ||
| for _, op := range operations { | ||
| switch op.Op { | ||
| case "add": |
There was a problem hiding this comment.
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
🤖
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.
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.
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]>
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/ServiceProviderConfigdiscovery, and could only handle a narrow Okta-shaped happy path. The old handler remains the default behind theCODER_SCIM_USE_LEGACYflag (hidden, defaulttrue); set it tofalseto 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
SubjectSCIMProvisionerRBAC subject (dbauthz.AsSCIMProvisioner) scoped to user CRUD.CreateUseris additionally escalated toAsSystemRestrictedbecause the implicitmemberrole assignment is gated by the staticassignRoleswhitelist, which has noscimentry.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 underenterprise/coderd/./Schemas,/ResourceTypes,/ServiceProviderConfigdiscovery endpoints.GET /Userssupportsfilter,attributes,excludedAttributes, paging.PATCH /Users/{id}(path/value/replace ops) andPUT /Users/{id}.GET /Users/{id}andDELETE /Users/{id}consistent with the spec.ExactUsernameSQL query for SCIM lookups; no migrations.Authorization
subjectSCIM/AsSCIMProvisioneris a dedicated RBAC subject scoped to user CRUD.AsSystemRestrictedwrapsCreateUseronly, because thememberrole assignment is bypassed by theassignRoleswhitelist.enterprise/audit/table.goupdated for the new resource interactions.Caveats (Okta)
FamilyName. Coder does not split names intoGivenName/FamilyName.Stack
scripts/scimverify(external compliance harness) ships on top in test: add script for unit testing scim 2.0 #25620.Generated by @Emyrk in collaboration with Coder Agents.
Manual QA with okta by @Emyrk