feat(dashboard): identity→tier grants for IdPs that mint no tier claim (JEF-501)#268
Merged
Merged
Conversation
…m (JEF-501) Cloudflare Access over GitHub relays no `tier` claim (GitHub emits none), so every operator floors to `redacted` and the forensic/raw debugging surface is unreachable. Adds an operator-configured identity→tier grant (PROTECTOR_DASHBOARD_OIDC_TIER_GRANTS, format `tier=id1,id2;tier=id3`) that resolves the ceiling from the VERIFIED sub/email when no tier claim is present. Precedence: (1) an explicit, recognized `tier` claim always wins — the IdP's own statement is authoritative, even over a grant; (2) else the highest tier granted to the verified identity (sub exact match, email case-insensitive); (3) else the unchanged Redacted floor. Parsing is strict (mirrors the MIN_TIER parser): an unknown tier name or malformed entry is a loud ConfigError so build_dashboard_auth/build_mcp_verifier refuse to serve rather than silently drop or silently over-grant. Threads an optional `email` claim through Claims/Identity for the match. The chart gains a `tierGrants` value (empty by default — actual grants are wired in the separate cluster repo). Closes JEF-501 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP
…501) Security review of PR #268 found two HIGH over-grant paths in the identity->tier grant matching: 1. An email-typed grant matched claims.email with no ownership check. A valid signature only proves the IdP minted the token, not that the subject owns the email it self-asserts (social login, a self-service directory) -- an attacker could set their account email to a granted operator's address and get elevated. Fixed by adding Claims.email_verified (bool, #[serde(default)] so absent means unverified, the safe default) and requiring it be true before an email-typed grant can match. 2. matches() compared every identifier against BOTH sub (exact) and email (case-insensitive) with a single OR, so an email-shaped grant could also match an opaque sub that happened to equal the same string, and vice versa -- silently widening the granted set. Fixed by typing each grant identifier at parse time (GrantId::Email vs GrantId::Sub, split on '@' presence, which is unambiguous for a sub) so it only ever matches its own field. Added tests for both: an unverified email equal to a grant resolves to Redacted while the same verified email resolves to the granted tier; a sub equal to a granted email string (and vice versa) does not match. Split the growing JEF-501 test surface into a new tier_grants_tests.rs to keep both files well under the repo's 1,000-line cap. Re-verified: cargo fmt, cargo clippy --all-targets -D warnings (clean), full cargo test green (907 passed). Addresses JEF-501 security review findings on PR #268. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP
Owner
Author
|
Addressed both HIGH findings from the security review:
New tests cover both: unverified vs. verified email against the same Gates re-run: Not merging — leaving for architect review. |
thejefflarson
enabled auto-merge (squash)
July 24, 2026 07:18
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Cloudflare Access over GitHub (this cluster's setup) relays no
tierclaim — GitHub emits none — so every operator floors toredactedand the forensic/raw debugging surface is unreachable (verified live). Adds an operator-configured identity→tier grant that resolves the ceiling from the VERIFIED token identity (sub/email) when notierclaim is present.PROTECTOR_DASHBOARD_OIDC_TIER_GRANTS, formattier=id1,id2;tier=id3, e.g.raw=alice@example.com;forensic=bob@example.com. Parsed strictly (mirrors theMIN_TIERparser, JEF-487) — an unknown tier name or malformed entry is a loudConfigErrorsobuild_dashboard_auth/build_mcp_verifierrefuse to serve, never silently drop the grant or over-grant.tierclaim always wins — the IdP's own statement is authoritative, even over a grant; (2) else the highest tier granted to the verified identity (subexact match,emailcase-insensitive); (3) else the unchangedRedactedfloor. An unrecognized (garbage) claim value falls through to the grant lookup rather than shadowing it as an "explicit redacted" statement.emailclaim throughClaims/Identity(post-signature-verified only — never a header/arg/tool param).EffectiveTier::clamp, the.capped_at(Forensic)bulk cap, or the audit path — the resolved tier is still just the ceiling those already clamp against.engine.dashboard.oidc.tierGrants(empty by default) →PROTECTOR_DASHBOARD_OIDC_TIER_GRANTS. Left empty; the actual grant for this cluster is wired in the separate cluster repo (../cluster), not here.Precedence example (from the acceptance criteria)
A token with an explicit
tier: forensicclaim and arawgrant for the same identity resolves toforensic— the claim wins.Test plan
cargo fmt --all -- --checkcargo clippy --all-targets -- -D warnings(clean)cargo test— full suite green (912 passed, 0 failed, 2 ignored [network-only])claims.rs/tests.rs): raw/forensic/unlisted grant resolution by email or sub; explicit claim precedence over a grant; unrecognized claim falls through to a grant (not the floor); case-insensitive email match / exact sub match; missing-email + sub-based grantemail/tierclaims, driven through the fullVerifier::verify()pathTIER_GRANTSparses and resolves correctly; unknown tier name / malformed syntax / empty identifier list / stray;all fail loud withConfigErrorrun_looptest:build_dashboard_auth/build_mcp_verifierboth refuse to serve on a misconfiguredTIER_GRANTS, both serve on a valid one (mirrors the existingMIN_TIERfail-loud test)helm lint+helm template— chart renders correctly with/withouttierGrantsset, no strayTIER_GRANTSenv var when unsetfile_size_guardtest passes; largest touched file is 845 lines)Closes JEF-501
🤖 Generated with Claude Code
https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP