Skip to content

feat(scim): Add SCIM 2.0 discovery endpoints - #119884

Merged
michelletran-sentry merged 8 commits into
getsentry:masterfrom
saadk408:feat/scim-discovery-endpoints
Oct 5, 2026
Merged

michelletran-sentry merged 8 commits into
getsentry:masterfrom
saadk408:feat/scim-discovery-endpoints

Conversation

@saadk408

@saadk408 saadk408 commented Jul 17, 2026 •

Copy link
Copy Markdown
Member

Completes the three SCIM 2.0 discovery endpoints defined in RFC 7644 §4 under the per-organization SCIM prefix — read-only GETs returning static, protocol-defined documents — plus RFC-compliance fixes to the responses of the existing private /Schemas index (own section below). The new endpoints reuse the existing SCIM auth gate: no new token types, scopes, or permission classes, and nothing tenant-specific in the responses except the org slug inside meta.location URLs.

Identity Governance & Administration (IGA) platforms (SailPoint, Saviynt, One Identity, among others) run compliance pre-flights that probe these endpoints and refuse to integrate when they fail, marking Sentry "not SCIM 2.0 compliant". Before this PR, two of the three endpoints 404'd; the third (/Schemas) existed as a private index whose content failed the same validators.

SCIM background (if you haven't worked with it)
  • SCIM ("System for Cross-domain Identity Management", RFC 7643/7644) is the standard protocol identity providers use to push users and groups into SaaS apps. The IdP (Okta, Microsoft Entra) is the HTTP client; Sentry is the service provider being provisioned into. In Sentry's implementation, SCIM Users map to organization members and SCIM Groups map to teams.
  • The protocol surface per tenant is small: /Users, /Groups, and three discovery endpoints that let a client ask the server what it supports.
  • The discovery endpoints (RFC 7644 §4) are a machine-readable self-description: /ServiceProviderConfig ("which protocol features do you support?"), /ResourceTypes ("which resource kinds exist here?" — each entry binds a URL path like /Users to a schema), and /Schemas ("what attributes does each resource have?"). Their content is derived from the implementation, not from tenant data.
  • A schema URN like urn:ietf:params:scim:schemas:core:2.0:User is a well-known identifier defined by the RFC, not something Sentry invents.
  • Why the gap existed unnoticed: Okta and Entra hardcode their Sentry schema client-side and never probe discovery — only the newer IGA platforms' pre-flights do.

How SCIM auth works at Sentry, and what this PR does with it

  • Token: enabling SCIM on an organization's AuthProvider (AuthProvider.enable_scim()) auto-creates an internal integration ("SCIM Internal Integration") whose installation API token is the credential IdPs send as Authorization: Bearer …. The token is valid only for that org and carries member:read|write|admin + team:write|admin scopes. This PR does not touch token minting.
  • Request path: all SCIM endpoints inherit SCIMEndpoint(OrganizationEndpoint). Authorization is OrganizationSCIMPermission(OrganizationPermission): the standard org permission check runs first, then it additionally requires the org's auth provider to have flags.scim_enabled. The new endpoints use OrganizationSCIMMemberPermission (GET → member:read|write|admin), the same class that already guards /Users and the existing /Schemas index.
  • Filter rejection ordering: discovery endpoints reject a filter param with a SCIM-format 403 (an RFC 7644 §4 SHOULD; rationale under Deviations below). reject_filter_param is called at the top of each GET handler, i.e. after DRF's permission checks (it can't live in initial(), which runs before the org is resolved). A caller who fails authorization therefore gets the normal authorization failure and can't use the filter rejection as an oracle. Each endpoint's filter-403 has its own test; the ordering property is asserted by test_filter_param_does_not_bypass_permission_check.
  • Why discovery stays behind the gate: some SCIM servers serve discovery anonymously, but Sentry's whole SCIM tree is bearer-gated, and serving discovery anonymously would let anyone probe which orgs have SCIM enabled, so these endpoints keep the same gate. The compliance validators used below authenticate before probing, so the gate doesn't hurt the goal.
  • Auth-matrix coverage: the agent-token matrix in tests/sentry/seer/endpoints/test_organization_agent_token.py exercises every PUBLIC GET endpoint with session, user-token, ViewerContext and agent-token credentials. The four new endpoints join the existing SCIM routes in its OUTER_BOUNDARY_GET_STATUSES list — they accept only the SCIM credential, so every other credential gets 403 — and the matrix gets real path values for resource_type_name and schema_uri. Test-only.

What this PR adds

  • GET /scim/v2/ServiceProviderConfig — single resource describing Sentry's actual capabilities (patch supported, filter supported with eq only and maxResults: 100 — describing /Users–/Groups filtering, no bulk/sort/etag/changePassword, bearer-token auth scheme).
  • GET /scim/v2/ResourceTypes and GET /scim/v2/ResourceTypes/{name} — User (organization members) and Group (teams) entries mapped to the existing core schema URNs; unknown names get a SCIM-format 404.
  • GET /scim/v2/Schemas/{schemaUri} — per-URI lookup of the existing User/Group schema definitions, with a SCIM-format 404 for unknown URIs. The route pattern is deliberately slash-permissive so malformed URIs still reach the endpoint and get the SCIM 404 rather than a generic Django 404.

The endpoints reject filter with 403 (an RFC 7644 §4 SHOULD) and, as §4 permits, always return everything and ignore pagination parameters. They are GET-only; write methods (POST/PUT/PATCH/DELETE) get 405s in the RFC 7644 §3.12 error format (the envelope shown under Deviations below). The new endpoints are publish_status = PUBLIC with full drf-spectacular docs — PUBLIC from day one because the response shape is fixed by the RFC and the point of the feature is that external platforms can rely on it; the pre-existing /Schemas index stays PRIVATE. No new models or migrations. The handlers perform no database queries; the only DB access is the standard org-endpoint plumbing (token auth, org resolution, the permission check).

Members and teams endpoints are untouched. knownSentryApiUrls.generated.ts is committed and verified drift-free locally (the api-url-typescript auto-commit job doesn't run on fork PRs). The docs.sentry.io SSO prose page (sentry-docs repo) is a follow-up.

API house-style deviations (all RFC-driven)

Three places this intentionally differs from the develop.sentry.dev API guidelines, matching how the existing SCIM tree already behaves:

  • PascalCase route segments (/ServiceProviderConfig, /ResourceTypes) — the fixed endpoint names RFC 7644 §4 defines.
  • Error bodies use the SCIM envelope ({"schemas": [...], "status": "403", "detail": …}) rather than DRF's bare {"detail": …} — required (MUST) by RFC 7644 §3.12.
  • 403 (not 400) for a filter param — RFC 7644 §4 recommends (SHOULD) rejection so clients can't assume their filter was applied; the compliance validators check for exactly this.

Changes to the existing (PRIVATE) /Schemas index responses

Low-risk: neither Okta nor Entra is known to probe discovery or send query parameters to /Schemas (both hardcode the schema client-side, per the background above), no request handler reads these schema definitions (they are documentation only), and the previous values were RFC-invalid or didn't match what Sentry returns. One index entry before and after:

// before
{
  "id": "urn:ietf:params:scim:schemas:core:2.0:User",
  "attributes": [{"name": "userName", "mutability": "read", ...}, ...],
  "meta": {"resourceType": "Schema",
           "location": "/v2/Schemas/urn:ietf:params:scim:schemas:core:2.0:User"}
}

// after
{
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Schema"],
  "id": "urn:ietf:params:scim:schemas:core:2.0:User",
  "attributes": [{"name": "userName", "mutability": "immutable", ...}, ...],
  "meta": {"resourceType": "Schema",
           "location": "https://sentry.io/api/0/organizations/{org}/scim/v2/Schemas/urn:ietf:params:scim:schemas:core:2.0:User"}
}
  • User mutability now says what Sentry does with each attribute ("read" is not a legal RFC 7643 §7 keyword and would fail the very validators this PR targets):
    • userName → immutable: consumed only at create (it becomes the member's email), and Sentry has no rename. Not readOnly, because Entra excludes readOnly attributes from provisioning payloads entirely and cannot create users without userName.
    • emails and name, with their sub-attributes → readOnly: both are derived server-side (emails from userName; name.givenName/familyName always return N/A), and client-sent values are ignored, which is how RFC 7644 §3.3/§3.5.1 define readOnly handling. name.givenName/familyName were previously readWrite.
    • active stays readWrite: PATCH active=false is the deprovisioning path.
    • The User name attribute also gains explicit mutability/returned/uniqueness: RFC 7643 §2.2 supplies defaults when they're omitted, but Entra's schema parser rejects attributes without an explicit mutability.
  • Group members now matches the /Groups payload: the schema declared sub-attributes value and $ref, both returned: default, but /Groups returns value and display. So $ref was never returned despite returned: default (RFC 7643 §7), and display was never declared. $ref (optional in the RFC 7643 §8.7.1 Group schema) is replaced with display, declared readOnly because Sentry derives it from the member's email. RFC 7643 is internally inconsistent on display mutability (§2.4 says immutable; the §8.7.1 User schema's groups.display is readOnly; see erratum 8358), so the schema follows what Sentry does.
  • Index entries now match the details endpoint exactly (shared build_schema_representation): each resource gains its RFC-required per-resource schemas declaration, and meta.location changed from a broken relative path that matched no real route to the absolute per-org URL. Locked by test_index_entries_match_details_representation.
  • Parameter handling now goes through the shared discovery base class: ?filter= returns a SCIM-format 403 (previously 400 on an invalid filter, silently ignored on a valid one), pagination params are ignored rather than validated (startIndex is now always 1 where it previously echoed the requested value; all schemas were returned in both old and new versions), and the 405 body is SCIM-format.

The mutability values and the Group members sub-attributes are locked by tests, including one that compares the declared sub-attributes with a real /Groups response. Attribute definitions are otherwise unchanged.

Review focus and validation

Start with utils.py — the SCIMDiscoveryEndpoint base and the permission wiring linked above hold all the behavior. The judgment calls in this PR are the /Schemas changes above and the publish_status = PUBLIC decision. The payload shapes (the three endpoint files, mirrored in src/sentry/apidocs/examples/scim_examples.py) are machine-validated against RFC 7643 §5/§6 by the two independent checkers below; the slash-permissive .+ route regexes in urls.py deserve a glance.

Validated on the current head against a live devserver with SCIM enabled via the real AuthProvider.enable_scim() flow, over a public tunnel: scim2-tester passes all 11 discovery checks, and Microsoft's Entra ID SCIM Validator discovers the schema and passes 17/21 provisioning tests, including all 12 Group tests (and its 3 Group preview tests). The 4 failures are pre-existing /Users behaviors in members.py (a duplicate of a pending invite returns 400, PATCH supports only active: false, no rename, and deactivation deletes the member), unchanged by this PR and queued as follow-up issues. Method and earlier validator screenshots are in this comment; layer-by-layer evidence is in the validation comment.

Issues

@github-actions github-actions Bot added Scope: Frontend Automatically applied to PRs that change frontend components Scope: Backend Automatically applied to PRs that change backend components labels Jul 17, 2026
Comment thread src/sentry/core/endpoints/scim/schemas.py Outdated
@goransh-walia

Copy link
Copy Markdown

Clean and minimal — appreciate that it stays scoped to the actual fix.

@saadk408

saadk408 commented Jul 20, 2026 •

Copy link
Copy Markdown
Member Author

⤴️ Superseded by the complete testing summary (adds the getsentry-layer end-to-end run, Entra validator screenshots, and the guideline-mapped suites). This comment remains the detailed SCIM-correctness evidence.

Validation evidence (no IGA tenant required)

The discovery endpoints were validated with three independent layers against a local devserver with SCIM enabled through the production path (AuthProvider.enable_scim), not test fixtures.

Layer 1 — Live HTTP matrix (23/23 pass)

curl-based checks against the running devserver covering: exact RFC 7643 §5 ServiceProviderConfig shape (no id/list keys, bearer auth scheme), ResourceTypes ListResponse envelope with totalResults: 2, per-name and per-URI lookups (incl. percent-encoded URNs), meta.location built from system.url-prefix, SCIM-format 403 for ?filter=, 404 for unknown/case-mismatched/slash-containing names and URIs, 405 for non-GET, 401 without a token, pagination params ignored, and Accept: application/scim+json handling.

Layer 2 — scim2-tester (independent RFC 7643/7644 checker): discovery 11/11

python-scim/scim2-tester parses responses through strict pydantic models of the RFC schemas. Every discovery check passed: ServiceProviderConfig, ResourceTypes index/by-id, cross-reference of each ResourceType.schema to a resolvable /Schemas/{urn} (the exact IGA pre-flight pattern), Schemas index/by-id, and invalid-URL 404s. Its /Users+/Groups CRUD findings match Sentry's documented provisioning limitations (userName-is-email, no .search, PATCH scope) — pre-existing surface, untouched by this PR. One out-of-scope note: unknown paths under the SCIM base fall through to the global /api/0 plain-text 404 catchall rather than a SCIM-format body (pre-existing, affects no defined endpoint).

Layer 3 — Microsoft Entra ID SCIM Validator (their real provisioning client, via a short-lived tunnel)

"Schema discovered!" in Discover-schema mode — the validator built its entire test run from our /Schemas + /ResourceTypes responses — and 16/21 provisioning tests passed, including every Group operation (create, duplicate-conflict, patch displayName, add/remove member, delete) and user filter/delete. All 5 failures are the pre-existing /Users provisioning surface (email-derivation from userName, N/A names, no duplicate 409, no rename, deactivate-deletes), not the discovery endpoints.

This layer caught two real gaps that RFC-only checking cannot (both fixed in 1e969e5, with regression tests):

  1. Entra's parser requires mutability to be present on every schema attribute even though RFC 7643 §2.2 defaults it — the User name attribute omitted it.
  2. Entra excludes non-writable attributes from provisioning payloads entirely and cannot create users without sending userName — its TestRunner failed with KeyNotFoundException: 'userName' while the schema declared userName/emails as readOnly. Now readWrite, matching the RFC 7643 core schema.

Also flagged by the validator (pre-existing, API-wide, out of scope here): Sentry's SCIM responses use Content-Type: application/json rather than application/scim+json (RFC 7644 §3.1 SHOULD; Entra accepts both).

Comment thread src/sentry/core/endpoints/scim/schemas.py Outdated
saadk408 added a commit to saadk408/sentry that referenced this pull request Jul 20, 2026
The /Schemas index returned the raw stored schema dicts, whose hardcoded relative meta.location (/v2/Schemas/...) does not resolve and which lacked the schemas attribute the new details endpoint adds — so an entry listed at the index disagreed with the resource at its own location. Both handlers now share build_schema_representation(), the stale meta blocks are dropped from the stored dicts, and a test locks index entries to be byte-identical to their details responses.

Flagged by Cursor Bugbot on getsentry#119884.
@saadk408
saadk408 force-pushed the feat/scim-discovery-endpoints branch from 6416edf to fee0ce2 Compare July 20, 2026 06:54
saadk408 added a commit to saadk408/sentry that referenced this pull request Jul 20, 2026
The /Schemas index returned the raw stored schema dicts, whose hardcoded relative meta.location (/v2/Schemas/...) does not resolve and which lacked the schemas attribute the new details endpoint adds — so an entry listed at the index disagreed with the resource at its own location. Both handlers now share build_schema_representation(), the stale meta blocks are dropped from the stored dicts, and a test locks index entries to be byte-identical to their details responses.

Flagged by Cursor Bugbot on getsentry#119884.
@saadk408 saadk408 added the Trigger: getsentry tests Once code is reviewed: apply label to PR to trigger getsentry tests label Jul 20, 2026
@github-actions github-actions Bot removed the Trigger: getsentry tests Once code is reviewed: apply label to PR to trigger getsentry tests label Jul 20, 2026
@saadk408 saadk408 added the Trigger: getsentry tests Once code is reviewed: apply label to PR to trigger getsentry tests label Jul 20, 2026
@saadk408

Copy link
Copy Markdown
Member Author

Testing summary — end to end in real environments, plus every guideline tier

Everything below is against the current head (1e4b3de). This extends the earlier validation comment, which remains the detailed layer-by-layer evidence.

Working end to end, in a real environment

Real environment. A live devserver with SCIM enabled the production way (real AuthProvider, real bearer token), exposed over the public internet via the standard develop.sentry.dev ngrok workflow — run twice: once on OSS sentry, and once with this branch served under the getsentry devserver with a mocked Business-plan customer org (real SaaS route composition and plan-based feature resolution). Identical results in both.

$ curl -s https://<tunnel-domain>/api/0/organizations/<org>/scim/v2/ServiceProviderConfig \
       -H "Authorization: Bearer $SCIM_TOKEN"
HTTP 200
{"schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
 ...,
 "meta": {"resourceType": "ServiceProviderConfig",
          "location": "https://<tunnel-domain>/api/0/organizations/<org>/scim/v2/ServiceProviderConfig"}}

Against a real IdP client. Microsoft's Entra ID SCIM Validator — a production Microsoft service making requests from Microsoft infrastructure — run against the getsentry-layer environment above. It discovered the schema through this PR's endpoints and populated its attribute model from them:

entra-schema-discovered

Provisioning test results — 16/21 passed, and the banner's "not compatible" verdict refers entirely to the 5 failures in the pre-existing /Users surface (create/rename/duplicate-409/disable semantics), which this PR does not touch and which are enumerated as known gaps in the validation comment. The application/scim+json content-type advisory is likewise pre-existing and out of scope:

entra-results-passed entra-results-failed-preexisting

The discovery goal — the thing this PR adds — is the part Microsoft's validator confirms: schema discovery works against a running Sentry over the public internet. (Two Entra-stricter-than-RFC behaviors were found via this harness during development, fixed, and locked by tests.)

Independent RFC harness. scim2-tester validates every response against strict pydantic models of RFC 7643/7644: 11/11 discovery checks pass. Full transcript from the getsentry-layer run (remaining errors are the same pre-existing /Users–/Groups behaviors, unchanged by this PR):

Full scim2-tester transcript
Performing a SCIM compliance check on https://<tunnel-domain>/api/0/organizations/<org>/scim/v2/ ...
SUCCESS service_provider_config_endpoint
SUCCESS query_all_resource_types
  Resource types available are: 'User', 'Group'
SUCCESS query_resource_type_by_id
  Successfully accessed the /ResourceTypes/User endpoint.
SUCCESS query_resource_type_by_id
  Successfully accessed the /ResourceTypes/Group endpoint.
SUCCESS resource_types_schema_validation
  ResourceType 'User' schema 'urn:ietf:params:scim:schemas:core:2.0:User' is accessible
SUCCESS resource_types_schema_validation
  ResourceType 'Group' schema 'urn:ietf:params:scim:schemas:core:2.0:Group' is accessible
SUCCESS access_invalid_resource_type
  /resource_types/c522dfab-03d4-4738-b14b-6f9ba726f7db invalid URL correctly returned a 404 error
SUCCESS query_all_schemas
  Schemas available are: 'User', 'Group'
SUCCESS access_schema_by_id
  Successfully accessed schema: urn:ietf:params:scim:schemas:core:2.0:User
SUCCESS access_schema_by_id
  Successfully accessed schema: urn:ietf:params:scim:schemas:core:2.0:Group
SUCCESS access_invalid_schema
  /Schemas/7277bffa-d1dd-41d0-9825-aaff262aee91 invalid URL correctly returned a 404 error
ERROR random_url
  Unexpected response content format Expecting value: line 1 column 1 (char 0)
ERROR object_creation
  {"email":["Enter a valid email address.","Enter a valid email address."]}
ERROR object_query
  {"email":["Enter a valid email address.","Enter a valid email address."]}
ERROR object_query_without_id
  {"email":["Enter a valid email address.","Enter a valid email address."]}
ERROR object_query_with_attributes
  {"email":["Enter a valid email address.","Enter a valid email address."]}
ERROR object_list_with_attributes
  {"email":["Enter a valid email address.","Enter a valid email address."]}
ERROR search_with_attributes
  {"email":["Enter a valid email address.","Enter a valid email address."]}
ERROR object_replacement
  {"email":["Enter a valid email address.","Enter a valid email address."]}
ERROR object_deletion
  {"email":["Enter a valid email address.","Enter a valid email address."]}
ERROR check_add_attribute
  {"email":["Enter a valid email address.","Enter a valid email address."]}
ERROR check_remove_attribute
  {"email":["Enter a valid email address.","Enter a valid email address."]}
ERROR check_replace_attribute
  {"email":["Enter a valid email address.","Enter a valid email address."]}
ERROR object_creation
  Expected type Group but got undefined object with no schema
ERROR object_query
  Expected type Group but got undefined object with no schema
ERROR object_query_without_id
  Expected type Group but got undefined object with no schema
ERROR object_query_with_attributes
  GET /Resource/{id}: requested attribute 'externalId' missing from Group response
ERROR object_query_with_attributes
  GET /Resource/{id}: excluded attribute 'displayName' still present in Group response
ERROR object_list_with_attributes
  GET /Resource: requested attribute 'externalId' missing from Group response
ERROR object_list_with_attributes
  GET /Resource: excluded attribute 'displayName' still present in Group response
ERROR search_with_attributes
  Unexpected response status code: 404
ERROR object_replacement
  Expected type Group but got undefined object with no schema
ERROR object_deletion
  Expected type Group but got undefined object with no schema
ERROR check_add_attribute
  Expected type Group but got undefined object with no schema
ERROR check_remove_attribute
  Attribute 'displayName' was not removed
ERROR check_remove_attribute
  Failed to remove attribute 'members': invalidFilter:
ERROR check_replace_attribute
  Expected type Group but got undefined object with no schema

The harness is reproducible

No bespoke rig — standard develop.sentry.dev tooling plus two public validators. Any reviewer can rerun the whole matrix in ~15 minutes:

devservices up && sentry devserver --ngrok=<your-tunnel-domain>
# enable SCIM + mint a token on a dev org (script in the validation comment)
BASE=https://<your-tunnel-domain>/api/0/organizations/<org>/scim/v2
curl -H "Authorization: Bearer $TOKEN" "$BASE/ServiceProviderConfig"
uv tool install scim2-cli && scim2 --url "$BASE" --header "Authorization: Bearer $TOKEN" test

I'm also happy to demo this live against a running instance for whoever picks up the review — the environment is two commands to bring up.

Standard suites — all green on this head

  • Endpoint tests: full tests/sentry/core/endpoints/scim/ suite, 158 passed locally on the merged head (also clean under SENTRY_DETECT_TESTCASE_MISUSE=1). Covers permission gates, invalid input, SCIM-format error bodies, and the Entra/Azure variant.
  • API docs: make build-api-docs clean; tests/apidocs/endpoints/scim/ 18 passed.
  • CI: the full backend suite ran (all 22 shards green) — urls.py changes force a full run, so there's no selective-testing blind spot.
  • Lint/typing: all prek hooks and the mypy pre-push hook pass on every changed file; backend typing green in CI.
  • getsentry: the dispatched getsentry CI skipped its backend suite ("no related files changed") — verified that's a genuine no-op (getsentry has no SCIM-specific tests), which is why the live getsentry-layer run above exists: it's the only way to exercise that layer, and it passes.

Why 4 checks show red (fork-PR artifacts, not failures)

  • api url typescript generation — needs repo App-token secrets unavailable to fork PRs; the committed generated file is verified drift-free locally.
  • snapshot tests — the upload step needs a secret; the tests themselves ran.
  • openapi-diff — optional-by-design; it exists to display new public endpoints, which this PR adds.
  • Vercel — fork preview deploys can't authenticate.

@github-actions github-actions Bot removed the Trigger: getsentry tests Once code is reviewed: apply label to PR to trigger getsentry tests label Jul 22, 2026
@saadk408 saadk408 added the Trigger: getsentry tests Once code is reviewed: apply label to PR to trigger getsentry tests label Jul 22, 2026

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 472b83e. Configure here.

Comment thread src/sentry/core/endpoints/scim/utils.py
@saadk408
saadk408 marked this pull request as ready for review July 22, 2026 01:35
@saadk408
saadk408 requested review from a team as code owners July 22, 2026 01:35
Comment thread src/sentry/core/endpoints/scim/utils.py
Comment thread src/sentry/core/endpoints/scim/schemas.py Outdated
Comment thread src/sentry/core/endpoints/scim/schemas.py Outdated
@github-actions github-actions Bot removed the Trigger: getsentry tests Once code is reviewed: apply label to PR to trigger getsentry tests label Jul 22, 2026
@saadk408 saadk408 added the Trigger: getsentry tests Once code is reviewed: apply label to PR to trigger getsentry tests label Jul 22, 2026

@michelletran-sentry michelletran-sentry 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.

Generally LGTM

@github-actions github-actions Bot removed the Trigger: getsentry tests Once code is reviewed: apply label to PR to trigger getsentry tests label Jul 24, 2026
@saadk408 saadk408 added the Trigger: getsentry tests Once code is reviewed: apply label to PR to trigger getsentry tests label Jul 24, 2026
@github-actions github-actions Bot removed the Trigger: getsentry tests Once code is reviewed: apply label to PR to trigger getsentry tests label Jul 24, 2026
Implement the RFC 7644 section 4 discovery endpoints under the per-org SCIM v2 prefix: GET
/ServiceProviderConfig (single resource describing supported features), GET /ResourceTypes and
GET /ResourceTypes/{name} (User and Group), and GET /Schemas/{schemaUri} (per-URI lookup with a
SCIM-format 404 for unknown URIs). IGA platforms run SCIM compliance pre-flights that require
these endpoints and currently mark Sentry as non-compliant when they 404.

All discovery endpoints require the SCIM bearer token like the rest of the SCIM API, are
GET-only (SCIM-format 405 bodies otherwise), reject filter params with 403, and ignore
pagination params per RFC 7644 section 4. They are published to the API docs.

Also normalizes the invalid 'mutability: read' values in the stored schema definitions to
'readOnly' (the RFC 7643 keyword) and aligns the private /Schemas index with the discovery
behavior (filter now 403s, startIndex is no longer echoed). Okta and Entra ID call neither
path with parameters, so existing provisioning is unaffected; members and teams endpoints are
untouched.

Fixes getsentry#114950
Validated the discovery endpoints against the Entra ID SCIM Validator and scim2-tester. Two schema fixes fell out:

- The User schema's complex name attribute omitted mutability entirely; RFC 7643 defaults it when absent, but Entra's schema parser rejects attributes without an explicit value.
- userName and emails were declared readOnly (a mechanical normalization of the previously invalid "read" keyword); Entra excludes non-writable attributes from provisioning payloads entirely and cannot create users without sending userName, so they are now readWrite per the RFC 7643 core schema. emails.display stays readOnly (service-generated).

With these fixes the Entra validator discovers the schema cleanly and runs its full provisioning suite from the discovered attributes.
The /Schemas index returned the raw stored schema dicts, whose hardcoded relative meta.location (/v2/Schemas/...) does not resolve and which lacked the schemas attribute the new details endpoint adds — so an entry listed at the index disagreed with the resource at its own location. Both handlers now share build_schema_representation(), the stale meta blocks are dropped from the stored dicts, and a test locks index entries to be byte-identical to their details responses.

Flagged by Cursor Bugbot on getsentry#119884.
Review feedback: the discovery schema advertised readWrite for attributes Sentry never consumes. userName is now immutable (consumed only at create as the member email; renames are unsupported), and emails/name plus their sub-attributes are readOnly (derived server-side, values sent by clients are ignored). active stays readWrite since PATCH active=false is the deprovisioning path.

immutable rather than readOnly for userName because Entra excludes readOnly attributes from provisioning payloads entirely; verified against the Entra SCIM Validator that immutable attributes are still sent on create (17/21, one better than the previous 16/21 baseline) and scim2-tester discovery remains 11/11.
@michelletran-sentry
michelletran-sentry force-pushed the feat/scim-discovery-endpoints branch from 09a6e4f to 14b5afc Compare August 25, 2026 13:32
@michelletran-sentry michelletran-sentry added the Trigger: getsentry tests Once code is reviewed: apply label to PR to trigger getsentry tests label Aug 25, 2026
@getsantry getsantry Bot removed the Stale label Aug 26, 2026
@getsantry getsantry Bot added the Stale label Sep 17, 2026
@getsantry

getsantry Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

This pull request has gone three weeks without activity. In another week, I will close it.

But! If you comment or otherwise update it, I will reset the clock, and if you add the label WIP, I will leave it alone unless WIP is removed ... forever!


"A weed is but an unloved flower." ― Ella Wheeler Wilcox 🥀

@getsantry getsantry Bot closed this Sep 25, 2026
auto-merge was automatically disabled September 25, 2026 07:00

Pull request was closed

The agent-token matrix (getsentry#121840) enumerates every PUBLIC GET endpoint and
checks each login method against it. The four SCIM discovery endpoints use
OrganizationSCIMMemberPermission, so like the existing SCIM routes they
reject non-SCIM credentials with 403 at the permission boundary.

List them in OUTER_BOUNDARY_GET_STATUSES and give the matrix real path
values for resource_type_name and schema_uri: the User resource type and
the core User schema URI, as in the RFC 7644 section 4 examples.

Refs getsentry#114950
The Group schema declared members sub-attributes value and $ref, both
returned by default, but /Groups returns value and display. So $ref was
never returned despite returned:default, and display was never declared
(RFC 7643 sections 2.4 and 7).

Replace $ref with display. It is readOnly: Sentry derives it from the
member's email and ignores client-sent values, which RFC 7644 sections 3.3
and 3.5.1 define as readOnly handling. RFC 7643 is inconsistent about
display mutability (erratum 8358), so the schema declares what Sentry
actually does, as it already does for the User schema.

A new test checks the declared members sub-attributes against a real
/Groups response.

Refs getsentry#114950
@saadk408 saadk408 reopened this Oct 2, 2026
@github-actions github-actions Bot removed the Trigger: getsentry tests Once code is reviewed: apply label to PR to trigger getsentry tests label Oct 2, 2026
@sentry

sentry Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Sentry Snapshot Testing

Name Added Removed Changed Renamed Unchanged Skipped Status
sentry-frontend
sentry-frontend
0 0 0 0 592 0 ✅ Unchanged

⚙️ sentry-frontend Snapshot Settings

@saadk408
saadk408 requested a review from a team as a code owner October 2, 2026 05:43
@saadk408 saadk408 added the Trigger: getsentry tests Once code is reviewed: apply label to PR to trigger getsentry tests label Oct 2, 2026
Comment thread src/sentry/core/endpoints/scim/resource_types.py
tests/AGENTS.md requires fixtures over Model.objects.create. The
create_auth_provider fixture already runs in the control silo, so the
explicit assume_test_silo_mode wrapper and its imports go too.

Refs getsentry#114950
@github-actions github-actions Bot removed the Trigger: getsentry tests Once code is reviewed: apply label to PR to trigger getsentry tests label Oct 2, 2026
@saadk408 saadk408 added the Trigger: getsentry tests Once code is reviewed: apply label to PR to trigger getsentry tests label Oct 2, 2026
@getsantry getsantry Bot removed the Stale label Oct 2, 2026
@saadk408

saadk408 commented Oct 2, 2026 •

Copy link
Copy Markdown
Member Author

@michelletran-sentry, could you take another look? Your July approval is still on the PR, but product code has changed since then, and auto-merge was turned off when the stale bot closed the PR. Here's what changed after 14b5afc7625. I've also refreshed the PR description to match.

1. Master merged in (32141ebf898). I merged rather than rebased, so the commits you reviewed are unchanged. There were no conflicts.

2. Agent-token auth matrix (a7ffcd3d658, test-only). Master's new test_organization_agent_token.py checks authentication for every PUBLIC GET, and it failed CI on the four new endpoints.

  • They now sit in OUTER_BOUNDARY_GET_STATUSES next to the existing SCIM routes, so every non-SCIM credential gets 403.
  • The matrix also gets real path values for resource_type_name and schema_uri.
  • GitHub auto-requested that file's code owners, getsentry/machine-learning-ai, as reviewers.

3. Group members schema (e7624163cf7). This is the one product change, and I found the problem after you approved.

  • Problem: the Group schema declared two members sub-attributes, value and $ref, both returned: default. But /Groups returns value and display (SCIMTeamMemberListItem). So $ref was advertised but never returned, and display was never declared.
  • Fix: $ref is optional in the RFC 7643 §8.7.1 Group schema, so I replaced it with display (readOnly, returned: default). RFC 7643 erratum 6011 (status: Reported) proposes the same display sub-attribute with the same characteristics, and the §8.4 Group example already includes members.display.
  • Why readOnly: Sentry builds display from the member's email and ignores any value a client sends. That's how RFC 7644 §3.3 and §3.5.1 treat readOnly. The RFC isn't consistent here: §2.4 says display is immutable, while groups.display in the §8.7.1 User schema is readOnly (see erratum 8358).
  • Test: a new test compares the declared sub-attributes with a real /Groups response.
  • Shipped index: this also changes the PRIVATE /Schemas index, which has listed members.$ref since feat(scim): Schemas endpoint #27186. The OpenAPI response type is unchanged, and $ref never carried a value, so no client loses data.
  • Left alone: RFC 7644 §3.5.2 says a PATCH that modifies a readOnly attribute SHALL get an error, but /Groups PATCH silently ignores display, and our own PATCH docs example sends it. That's existing, lenient behavior, and Entra only sends value.

4. Test fixture cleanup (bae00f781d4, test-only). One test now uses create_auth_provider instead of AuthProvider.objects.create, per tests/AGENTS.md.

Validation on the current head. I ran it against a live devserver, with SCIM enabled through the real enable_scim() flow, over a public tunnel:

  • scim2-tester: all 11 discovery checks pass.
  • Entra SCIM Validator: it discovers the schema and passes 17/21, including all 12 Group tests. That's the same score as July, and the 4 failures are the existing /Users behaviors listed in the description.
  • Our pre-flight harness for SailPoint IdentityIQ's documented compliant-mode requirements: all Tier-1 checks pass.

CI is green apart from the three jobs that always fail on fork PRs: the api-url-typescript auto-commit, Vercel, and openapi-diff. The API publishing guide says openapi-diff is expected to fail and isn't required to merge; here it shows only the four new paths being added.

API reference preview: the four new pages, rendered by sentry-docs from this branch's OpenAPI build 1-service-provider-config 2-list-resource-types 3-get-resource-type 4-get-schema

Seer flagged the leading slash in the ResourceType endpoint value ("/Users"). I replied on that thread with the RFC references; it's a false positive.

I have two l (low, non-blocking) questions:

  • Should /Schemas/{uri} match case-insensitively? It matches exactly, and test_lookup_is_case_sensitive pins that. RFC 7644 §3.10 says every facet of a fully encoded attribute name, the URN included, is case insensitive. That isn't a MUST for path lookups, though, and real clients send the canonical URN. /ResourceTypes/{name} should stay exact either way, because verified erratum 8475 makes the ResourceType name case-exact. Keep exact matching and add a comment explaining why, or switch to case-insensitive matching?
  • Should the shared attribute lists be copied? build_schema_representation puts the module-level attribute lists straight into response.data. Nothing changes them today, but if something ever did, the process-wide constants would be corrupted. A copy.deepcopy would prevent that at negligible cost. Is it worth adding?

If it still looks good, could you approve the current head and turn auto-merge back on?

@michelletran-sentry
michelletran-sentry merged commit 57d129d into getsentry:master Oct 5, 2026
81 of 115 checks passed
@saadk408
saadk408 deleted the feat/scim-discovery-endpoints branch October 6, 2026 00:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Backend Automatically applied to PRs that change backend components Scope: Frontend Automatically applied to PRs that change frontend components Trigger: getsentry tests Once code is reviewed: apply label to PR to trigger getsentry tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants