Skip to content

feat: SCIM/SSO/CORS identity-boundary hardening (WS5)#412

Merged
PaulDotterer merged 2 commits into
mainfrom
feat/scim-sso-identity-boundary
Jun 13, 2026
Merged

feat: SCIM/SSO/CORS identity-boundary hardening (WS5)#412
PaulDotterer merged 2 commits into
mainfrom
feat/scim-sso-identity-boundary

Conversation

@PaulDotterer

@PaulDotterer PaulDotterer commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

WS5 — SCIM / SSO / CORS identity-boundary hardening (server-only)

Hardens the identity boundary where SCIM and OIDC SSO take input from partially-trusted IdPs. Closes the WS5 SCIM + OIDC + CORS findings (crypto AAD #8 ships separately). See ADR 0008.

SCIM

  • Cross-provider IDOR closed. Group member-add requires provider-ownership of every target user at all four add-sinks (reconcile / patch-add / patch-replace / create-with-members); unowned members are skipped (idempotent). User-resource verbs already 404 cross-provider — pinned by tests.
  • AutoLinkByEmail account-takeover guard. An IdP-asserted email matching a pre-existing local password account is refused (409) unless the provider's new trust_email_assertions flag is set (the operator's explicit opt-in, e.g. migrating password users to SSO). Passwordless/already-SSO accounts still link. New column via migration 012 + Go projector + sqlc regen.
  • SCIM follows the login switch: requires enabled = TRUE, not just scim_enabled.
  • No auth oracle: unknown-slug / no-token / wrong-token all return one identical 401 invalid credentials with a constant-time bcrypt compare (auth.DummyHash).

OIDC / SSO

  • Bounded outbound client. Discovery, token exchange, and lazy JWKS fetch run through an *http.Client with connect/TLS/response/overall timeouts via oidc.ClientContext (never http.DefaultClient).
  • Real bug fixed: a soft-deleted linked user's stale-link cleanup fell through to a bogus lookup identity link: <nil> error instead of re-linking/re-creating (the post-block err==nil guard wrongly returned). Now gated on err != nil.

CORS

  • Allow-all reflects the Origin but never sends Access-Control-Allow-Credentials (closes the credentialed-wildcard CSRF/token-theft hole); named origins keep credentials. The control server refuses to boot with CORS_ALLOW_ALL when TLS is enabled or the listen addr is non-localhost (validateConfig extracted as a pure, testable fn).

Tests

Cross-provider IDOR (4 member-add sinks + 4 user verbs), AutoLinkByEmail trust gate (takeover/trust/passwordless/absent), SCIM auth rejection matrix + disabled-provider + timing-indistinguishability, OIDC bounded-client + discovery-timeout, verifier aud/exp/iss/byte-tamper rejections, linker auto-link/existing-link/soft-deleted branches, PKCE S256 known-answer + state entropy, SSOCallback slug-mismatch, CORS credentialed-wildcard + boot guard.

Migration 012, ADR 0008, control README. Closes #411.

Summary by CodeRabbit

Release Notes

  • New Features

    • CORS configuration now validates production safety, refusing allow-all mode when TLS is enabled or listening on non-localhost addresses.
    • SCIM auto-link by email now requires explicit operator configuration before linking to existing password-protected accounts.
  • Bug Fixes

    • OIDC discovery and token operations now bounded with timeouts to prevent indefinite hangs.
    • SCIM authentication responses unified to eliminate timing/existence oracles for unknown or disabled providers.
    • SCIM group member operations now enforce provider ownership, preventing cross-provider access.
  • Documentation

    • Added architectural decision record documenting identity boundary invariants and enforcement strategies.

…(WS5)

Server-only identity-boundary hardening across SCIM, OIDC SSO, and CORS.

SCIM
- Group member-add requires provider-ownership of every target user at all
  four add-sinks (reconcile / patch-add / patch-replace / create) — closes the
  cross-provider IDOR; unowned members are skipped (idempotent).
- AutoLinkByEmail refuses to bind an IdP-asserted email to a pre-existing local
  PASSWORD account unless the provider's new trust_email_assertions flag is set
  (account-takeover guard). New column via migration 012 + Go projector + sqlc;
  passwordless/already-SSO accounts still link.
- SCIM follows the provider login switch (requires enabled = TRUE, not just
  scim_enabled).
- Auth has no existence/timing oracle: unknown-slug / no-token / wrong-token all
  return one identical 401 ("invalid credentials") with a constant-time bcrypt
  compare against auth.DummyHash.

OIDC/SSO
- Bound every outbound OIDC call (discovery, exchange, lazy JWKS) with
  connect/TLS/response/overall timeouts via oidc.ClientContext (no
  http.DefaultClient).
- Fix a real linker bug: a soft-deleted linked user's stale-link cleanup fell
  through to a bogus "lookup identity link: <nil>" error instead of re-linking/
  re-creating (the post-block err==nil guard returned). Now gated on err != nil.

CORS
- Allow-all mode reflects the Origin but never sends Allow-Credentials (closes
  the credentialed-wildcard hole); named origins keep credentials. The control
  server refuses to boot with CORS_ALLOW_ALL when TLS is enabled or the listen
  addr is non-localhost (validateConfig extracted as a pure, testable fn).

Tests: cross-provider IDOR (member-add ×4 sinks + user-resource verbs),
AutoLinkByEmail trust gate (takeover/trust/passwordless/absent), SCIM auth
rejection matrix + disabled-provider + timing-indistinguishability, OIDC
bounded-client + discovery-timeout, verifier aud/exp/iss/byte-tamper rejections,
linker auto-link/existing-link/soft-deleted branches, PKCE S256 known-answer +
state entropy, SSOCallback slug-mismatch, CORS credentialed-wildcard + boot
guard. ADR 0008 + control README. Migration 012.

Closes #411.
@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@PaulDotterer, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 49 minutes and 11 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e9c5f4db-6be6-423a-a0f7-8eaf6d59c5db

📥 Commits

Reviewing files that changed from the base of the PR and between b4d7fdf and 9cae286.

📒 Files selected for processing (1)
  • internal/idp/oidc_verify_test.go
📝 Walkthrough

Walkthrough

This PR implements WS5 SCIM/SSO/CORS identity-boundary hardening across six domains: boot-time CORS safety validation, identity-provider schema extension for email-linking trust gates, SCIM authentication timing-indistinguishability, provider-scoped group member authorization, OIDC timeout bounding, and CORS credential separation. All changes are fail-closed, with extensive test coverage and accompanying ADR documentation.

Changes

SCIM/SSO/CORS Identity-Boundary Hardening (WS5)

Layer / File(s) Summary
Configuration validation and CORS boot-time gate
cmd/control/flags.go, cmd/control/config_guard_test.go, cmd/control/README.md
Boot-time validation refactor centralizes config invariants into pure validateConfig function that rejects dev-only CORS allow-all when TLS is enabled or listen address is non-localhost; accompanies with comprehensive test coverage for boot rejection scenarios and JWT secret validation.
Identity provider schema and projection persistence
internal/store/identity_provider.go, internal/store/migrations/012_idp_trust_email_assertions.sql, internal/store/postgres/identity_provider.go, internal/store/queries/identity_providers.sql, internal/projectors/identity_provider.go, internal/projectors/identity_provider_listener.go
Schema extension adds TrustEmailAssertions boolean flag to identity_providers_projection table via migration; projector payloads and listeners wire the flag through event decoding for create/update operations to enable downstream email-linking trust gates.
SCIM authentication hardening and timing-safe responses
internal/scim/auth.go, internal/scim/auth_matrix_test.go, internal/store/queries/scim.sql
SCIM bearer-token authentication unified to return constant HTTP 401 "invalid credentials" for all failures (unknown provider, disabled provider, missing/wrong token); both provider lookup and token verification paths use dummy bcrypt compare to prevent timing/existence oracles; query layer gates SCIM on both scim_enabled and provider enabled flags; comprehensive auth rejection matrix test validates malformed/invalid header variants and timing-indistinguishability.
SCIM email-linking account-takeover prevention
internal/scim/users_create.go, internal/scim/users_link_test.go
SCIM user creation now blocks AutoLinkByEmail from linking IdP-asserted email to pre-existing local password accounts unless provider explicitly enables TrustEmailAssertions; returns HTTP 409 Conflict on attempted takeover without opt-in; test coverage verifies rejection without trust signal, allowance with signal, passwordless auto-link, and new-user creation.
SCIM provider-scoped group member authorization (IDOR prevention)
internal/scim/groups_helpers.go, internal/scim/groups_create.go, internal/scim/groups_mutate.go, internal/scim/handler_test.go, internal/scim/groups_authz_test.go
Group member-add operations (PATCH add/replace, PUT reconcile, create-with-members) gate each addition via provider-ownership verification; new mayAddMemberToGroup helper skips event emission for cross-provider users; cross-provider IDOR test suites verify provider B cannot add provider A's users to groups and cannot CRUD provider A's user resources.
CORS middleware security and credential separation
internal/middleware/cors.go, internal/middleware/cors_test.go
CORS middleware refactored to separate explicit allow-list (sets Access-Control-Allow-Credentials: true) from allow-all mode (reflects origin but omits credentials header to prevent credentialed cross-origin requests); test coverage validates all three modes (allow-all, explicit allow, rejection).
OIDC HTTP client timeout bounding for outbound calls
internal/idp/oidc.go, internal/idp/oidc_timeout_test.go
OIDC provider now stores internally configured httpClient with bounded timeout; client injected into context during discovery so lazy JWKS fetch inherits timeouts; ExchangeCode and VerifyAndExtractClaims route calls through clientCtx helper; test coverage validates client configuration and discovery timeout enforcement.
OIDC token verification rejection tests and state primitive validation
internal/idp/oidc_verify_test.go, internal/idp/state_test.go
Verification test suite extended with fixture-based tests for wrong audience, expiration, wrong issuer, and byte-tampered signature rejections; state/nonce/PKCE primitives validated for entropy (32 bytes via base64-RawURLEncoding), distinctness, and PKCE challenge RFC 7636 known-answer compliance.
Identity linker auto-link/cleanup and soft-delete fallthrough
internal/idp/linker.go, internal/idp/linker_test.go
Linker error handling refined to allow nil-error fallthrough for intentional cleanup/re-creation (e.g., soft-deleted user linkage); mock querier extended with override fields to exercise more lookup branches; test coverage added for auto-link with existing user, disabled auto-link, existing-link login updates, and soft-deleted user cleanup.
SSO callback slug-mismatch rejection and auth-state consumption
internal/api/sso_handler_failure_paths_test.go
SSO handler failure-path test verifies SSOCallback rejects auth states created for provider A when invoked with provider B's slug, returns CodeInvalidArgument with "slug mismatch" message, and confirms auth-state row is consumed even on mismatch.
Architecture decision record and configuration documentation
docs/adr/0008-scim-sso-identity-boundary.md, cmd/control/README.md
ADR 0008 documents SCIM/SSO identity-boundary invariants (provider-scoped SCIM, email→password trust gating, provider enabled flag, timing-safe auth, OIDC timeouts, credential-less CORS allow-all); README updated with SCIM and CORS configuration sections.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • manchtools/power-manage-server#244: Refactors "not found" detection in SCIM auth to use store.IsNotFound, changing error classification that feeds the PR's bearer-token failure-path hardening.
  • manchtools/power-manage-server#221: Both extend internal/idp/oidc_verify_test.go coverage for signed id_token verification rejection paths.
  • manchtools/power-manage-server#207: Both extend SSOHandler failure-path coverage in internal/api/sso_handler_failure_paths_test.go for security-relevant callback rejection branches.

Poem

🐇 A rabbit hops through SCIM's walls,
Checking who owns each user that calls,
With timeouts and trust gates, and CORS so tight,
The identity boundary shines extra bright! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.33% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: hardening of SCIM/SSO/CORS identity boundaries to address WS5 findings, which is the primary focus of the entire changeset.
Linked Issues check ✅ Passed All objectives from #411 are met: SCIM provider ownership checks [groups_authz_test.go, groups_helpers.go, groups_mutate.go, groups_create.go], AutoLinkByEmail guard [users_create.go, users_link_test.go, identity_provider.go, migration 012], SCIM provider enabled gate [scim.sql], constant-time auth rejection [scim/auth.go, auth_matrix_test.go], OIDC timeouts [oidc.go, oidc_timeout_test.go, oidc_verify_test.go], linker fix [linker.go, linker_test.go], CORS credential separation [cors.go, cors_test.go], boot guard [flags.go, config_guard_test.go, README.md], ADR and documentation.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the linked issue #411: SCIM provider ownership, AutoLinkByEmail gating, OIDC timeouts, linker fix, CORS credentials, boot validation, and comprehensive test coverage. No unrelated changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/scim-sso-identity-boundary

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 and usage tips.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/scim/auth.go (1)

24-40: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Unify no-token/malformed-token failures to the same invalid credentials response.

Line 24-40 still returns distinct 401 bodies (missing authorization header, authorization header must use Bearer scheme, bearer token is empty). That conflicts with the WS5 objective for a single indistinguishable rejection across unknown-slug/no-token/wrong-token paths.

Suggested fix
-		// Extract bearer token from Authorization header
-		authHeader := r.Header.Get("Authorization")
-		if authHeader == "" {
-			h.logger.Warn("SCIM auth failed: missing authorization header", "method", r.Method, "path", r.URL.Path)
-			writeError(w, http.StatusUnauthorized, "missing authorization header")
-			return
-		}
-
-		if !strings.HasPrefix(authHeader, "Bearer ") {
-			h.logger.Warn("SCIM auth failed: not a Bearer token", "method", r.Method, "path", r.URL.Path)
-			writeError(w, http.StatusUnauthorized, "authorization header must use Bearer scheme")
-			return
-		}
-
-		token := strings.TrimPrefix(authHeader, "Bearer ")
-		if token == "" {
-			h.logger.Warn("SCIM auth failed: empty token", "method", r.Method, "path", r.URL.Path)
-			writeError(w, http.StatusUnauthorized, "bearer token is empty")
-			return
-		}
+		// Extract bearer token from Authorization header.
+		// WS5: no-token/malformed-token paths are indistinguishable from other auth failures.
+		authHeader := r.Header.Get("Authorization")
+		token := ""
+		if strings.HasPrefix(authHeader, "Bearer ") {
+			token = strings.TrimPrefix(authHeader, "Bearer ")
+		}
+		if token == "" {
+			h.logger.Warn("SCIM auth failed: missing or malformed bearer token", "method", r.Method, "path", r.URL.Path)
+			_ = bcrypt.CompareHashAndPassword([]byte(auth.DummyHash), []byte("invalid"))
+			writeError(w, http.StatusUnauthorized, "invalid credentials")
+			return
+		}
🤖 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/scim/auth.go` around lines 24 - 40, The three auth failure branches
(missing authHeader, non‑Bearer prefix, empty token) currently return distinct
401 bodies; change them to return the same generic response to meet WS5: replace
the specific writeError messages in those branches with a single message like
"invalid credentials" and use the same HTTP 401 status for each, and also avoid
leaking different failure reasons in logs by updating the logger.Warn calls in
those branches to a single generic message (e.g., "SCIM auth failed: invalid
credentials") while retaining the method/path fields; the changes apply to the
code that checks authHeader, the strings.HasPrefix("Bearer ") branch, and the
token == "" branch and should still call the existing writeError function and
return immediately as before.
🧹 Nitpick comments (1)
internal/scim/groups_authz_test.go (1)

100-124: ⚡ Quick win

Assert success status in cross-provider member-add negative paths.

These subtests currently validate membership outcome only. Capture and assert the HTTP status as well, so regressions don’t pass due to unrelated request failures.

Proposed test hardening
  t.Run("patch_add_cross_provider_rejected", func(t *testing.T) {
      gid := createSCIMGroup(t, env, slugB, tokenB, "g-padd-"+testutil.NewID()[:6], nil)
-     scimReq(t, env, slugB, tokenB, http.MethodPatch, "/Groups/"+gid, patchAdd(userA))
+     w := scimReq(t, env, slugB, tokenB, http.MethodPatch, "/Groups/"+gid, patchAdd(userA))
+     require.Equal(t, http.StatusOK, w.Code, "%s", w.Body.String())
      assert.NotContains(t, groupMemberIDs(t, env, gid), userA,
          "providerB must NOT add providerA's user via PATCH add (IDOR)")
  })

  t.Run("patch_replace_cross_provider_rejected", func(t *testing.T) {
      gid := createSCIMGroup(t, env, slugB, tokenB, "g-prep-"+testutil.NewID()[:6], nil)
-     scimReq(t, env, slugB, tokenB, http.MethodPatch, "/Groups/"+gid, patchReplaceMembers(userA))
+     w := scimReq(t, env, slugB, tokenB, http.MethodPatch, "/Groups/"+gid, patchReplaceMembers(userA))
+     require.Equal(t, http.StatusOK, w.Code, "%s", w.Body.String())
      assert.NotContains(t, groupMemberIDs(t, env, gid), userA,
          "providerB must NOT add providerA's user via PATCH replace members (IDOR)")
  })

  t.Run("put_reconcile_cross_provider_rejected", func(t *testing.T) {
      gid := createSCIMGroup(t, env, slugB, tokenB, "g-put-"+testutil.NewID()[:6], nil)
      put := map[string]any{
          "schemas":     []string{"urn:ietf:params:scim:schemas:core:2.0:Group"},
          "displayName": "g-put-" + testutil.NewID()[:6],
          "members":     []map[string]any{{"value": userA}},
      }
-     scimReq(t, env, slugB, tokenB, http.MethodPut, "/Groups/"+gid, put)
+     w := scimReq(t, env, slugB, tokenB, http.MethodPut, "/Groups/"+gid, put)
+     require.Equal(t, http.StatusOK, w.Code, "%s", w.Body.String())
      assert.NotContains(t, groupMemberIDs(t, env, gid), userA,
          "providerB must NOT add providerA's user via PUT reconcile (IDOR)")
  })
🤖 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/scim/groups_authz_test.go` around lines 100 - 124, In the three
subtests patch_add_cross_provider_rejected,
patch_replace_cross_provider_rejected, and
put_reconcile_cross_provider_rejected, capture the HTTP status returned by
scimReq and assert that it indicates a successful request (e.g. 2xx) before
asserting on groupMemberIDs; specifically, change the scimReq calls in those
tests to return the response/status (from scimReq) and add an assertion like
require.True(t, status >= 200 && status < 300, "expected success status for
PATCH/PUT request") so failures in the request itself do not make the negative
membership assertions pass.
🤖 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 `@internal/idp/oidc_verify_test.go`:
- Around line 335-344: The tamper test mutates the final base64url signature
character which can be semantically no-op; instead mutate a non-terminal
signature character: in the block that builds tampered from idToken (variables
parts, sig, tampered), choose an index like idx := len(sig)-2 (ensure len(sig)
>= 2) and flip sig[idx] (e.g., toggle between 'A' and 'B' or XOR the byte)
rather than mutating sig[len(sig)-1], then set parts[2] = string(sig) and join
to tampered; this makes the mutation deterministic and avoids harmless changes
to the final base64 padding bits.

---

Outside diff comments:
In `@internal/scim/auth.go`:
- Around line 24-40: The three auth failure branches (missing authHeader,
non‑Bearer prefix, empty token) currently return distinct 401 bodies; change
them to return the same generic response to meet WS5: replace the specific
writeError messages in those branches with a single message like "invalid
credentials" and use the same HTTP 401 status for each, and also avoid leaking
different failure reasons in logs by updating the logger.Warn calls in those
branches to a single generic message (e.g., "SCIM auth failed: invalid
credentials") while retaining the method/path fields; the changes apply to the
code that checks authHeader, the strings.HasPrefix("Bearer ") branch, and the
token == "" branch and should still call the existing writeError function and
return immediately as before.

---

Nitpick comments:
In `@internal/scim/groups_authz_test.go`:
- Around line 100-124: In the three subtests patch_add_cross_provider_rejected,
patch_replace_cross_provider_rejected, and
put_reconcile_cross_provider_rejected, capture the HTTP status returned by
scimReq and assert that it indicates a successful request (e.g. 2xx) before
asserting on groupMemberIDs; specifically, change the scimReq calls in those
tests to return the response/status (from scimReq) and add an assertion like
require.True(t, status >= 200 && status < 300, "expected success status for
PATCH/PUT request") so failures in the request itself do not make the negative
membership assertions pass.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 48351ad1-5f3f-4e2d-8325-89dd5f09398f

📥 Commits

Reviewing files that changed from the base of the PR and between da1e3a9 and b4d7fdf.

⛔ Files ignored due to path filters (3)
  • internal/store/generated/identity_providers.sql.go is excluded by !**/generated/**
  • internal/store/generated/models.go is excluded by !**/generated/**
  • internal/store/generated/scim.sql.go is excluded by !**/generated/**
📒 Files selected for processing (29)
  • cmd/control/README.md
  • cmd/control/config_guard_test.go
  • cmd/control/flags.go
  • docs/adr/0008-scim-sso-identity-boundary.md
  • internal/api/sso_handler_failure_paths_test.go
  • internal/idp/linker.go
  • internal/idp/linker_test.go
  • internal/idp/oidc.go
  • internal/idp/oidc_timeout_test.go
  • internal/idp/oidc_verify_test.go
  • internal/idp/state_test.go
  • internal/middleware/cors.go
  • internal/middleware/cors_test.go
  • internal/projectors/identity_provider.go
  • internal/projectors/identity_provider_listener.go
  • internal/scim/auth.go
  • internal/scim/auth_matrix_test.go
  • internal/scim/groups_authz_test.go
  • internal/scim/groups_create.go
  • internal/scim/groups_helpers.go
  • internal/scim/groups_mutate.go
  • internal/scim/handler_test.go
  • internal/scim/users_create.go
  • internal/scim/users_link_test.go
  • internal/store/identity_provider.go
  • internal/store/migrations/012_idp_trust_email_assertions.sql
  • internal/store/postgres/identity_provider.go
  • internal/store/queries/identity_providers.sql
  • internal/store/queries/scim.sql

Comment thread internal/idp/oidc_verify_test.go
Flipping the final base64url char of an RS256 signature can decode to the same
bytes (it carries only a couple of meaningful bits), a no-op that could let
verification pass. Mutate a mid-segment char so the decoded signature always
changes.
@PaulDotterer PaulDotterer merged commit 477fda7 into main Jun 13, 2026
10 checks passed
@PaulDotterer PaulDotterer deleted the feat/scim-sso-identity-boundary branch June 13, 2026 20:43
PaulDotterer added a commit that referenced this pull request Jun 13, 2026
…sponse (#416)

PR #412 added the trust_email_assertions flag + SCIM AutoLinkByEmail
account-takeover gate, but the flag had no Create/Update RPC field — operators
could only set it via a raw event. With the sdk proto field
(manchtools/power-manage-sdk#98), map req.Msg.TrustEmailAssertions into the
IdentityProviderCreated/Updated events and return it on the IdentityProvider
response, so it is settable + visible via the API/UI. Default false (secure).

Bumps sdk to the proto field. Closes the WS5 trust_email follow-up.
PaulDotterer added a commit that referenced this pull request Jun 26, 2026
…ation (#465)

v2026.06 shipped migrations 001–008 as a clean baseline (itself a consolidation
of v2026.05's set). Six incremental migrations then landed on main for the
2026.07 cycle — 009 role-permission split (#333), 010 role-grant scope columns
(#334), 011 events append-only trigger (#404), 012 IdP trust_email_assertions
(#412), 013 LUKS token hashing (#420), 014 reconciler-owned system-role
permissions (#440). Per the project's per-release migration convention, collapse
them into a single ordered 009_v2026_07.sql so the 2026.06→2026.07 delta is one
migration.

Faithful concatenation: every statement preserved verbatim, Up in original apply
order, Down reversed, each StatementBegin/End block intact. No schema change —
`sqlc generate` produces an identical generated/ tree, and the seed-parity guard
(system-role permissions stay reconciler-owned) still passes.
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.

sec: SCIM/SSO/CORS identity-boundary hardening (WS5)

1 participant