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 |
| return scim.Resource{}, scimErrors.ScimErrorBadRequest(fmt.Sprintf("changing the 'userName' field is not supported (current value: %q)", dbUser.Username)) | ||
| } | ||
|
|
||
| // TODO: Check primary email |
There was a problem hiding this comment.
Did you mean to add this check?
| switch op.Op { | ||
| case "add": | ||
| case "remove": | ||
| if op.Path.String() == "active" { |
There was a problem hiding this comment.
The replace case below has a nil check on op.Path. I suspect you want the same here
| 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?
| }) | ||
| if err == nil { | ||
| // User already exists. Update their status if needed. | ||
| status := scimUserStatus(dbUser, &active) |
There was a problem hiding this comment.
Did you mean to scope this inside the if active { branch below? The status only seems to be used inside that scope. I can't tell if that's intended.
| totalCount := int64(len(users)) | ||
| if len(users) == int(qry.LimitOpt) { | ||
| // If the limit is not reached, that is the count | ||
| // TODO: If there is a query and the limit is reached, this is inaccurate. |
There was a problem hiding this comment.
What are the implications of this? Could we just fix it now?
|
|
||
| // Patch implements scim.ResourceHandler. Updates user attributes based on | ||
| // SCIM PatchOp operations. Currently, supports changing the active status. | ||
| func (ru *ResourceUser) Patch(r *http.Request, idStr string, operations []scim.PatchOperation) (scim.Resource, error) { |
There was a problem hiding this comment.
Do we need a similar mutability check in Patch that's in Replace?
An operation that is not compatible with an attribute's mutability or schema SHALL return the appropriate HTTP response status code and a JSON detail error response as defined in Section 3.12.
| 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.
| qry.LimitOpt = int32(params.Count) //nolint:gosec | ||
| qry.OffsetOpt = int32(params.StartIndex - 1) //nolint:gosec | ||
|
|
||
| if qry.LimitOpt <= 0 { |
There was a problem hiding this comment.
Are you sure on this default limit?
https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.2.4
A value of "0" indicates that no resource results are to be returned except for "totalResults"
There was a problem hiding this comment.
It would be nice to have some tests of the new HTTP handler.
There was a problem hiding this comment.
nitpicky, but I would omit the scim prefix from these file names the scim package to reduce stutter.
enterprise/coderd/scim/scimexpression.go -> enterprise/coderd/scim/expression.go
enterprise/coderd/scim/scimusers.go -> enterprise/coderd/scim/users.go
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 is preserved behind theCODER_SCIM_USE_LEGACYflag (hidden, defaultfalse) so existing deployments can opt out if something regresses.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