Repository navigation
Conversation
…laim
Add generic claim-gated candidate selection. Candidates carry a `labels`
map; the filter carries `match_claims: [{claim, label}]`. Per request the
filter reads the claim off the authenticated identity and keeps only
candidates whose matching label equals it, so a request is never routed
outside the caller's entitlement (data residency, tier, and so on).
The gate reuses the AuthenticatedIdentity extension the policy engine
already publishes, so no new plumbing is added. It fails closed: a gate
with no identity or a missing claim denies. A fenced-out capability
returns the same 404 as an unknown one, so a caller cannot enumerate the
capabilities it is fenced out of. The ungated path is unchanged and
allocates nothing new; a gate also fences session-affinity reuse and
weighted selection, so no draw lands out of scope.
Both static and overlay candidate modes carry the label, so a fleet
registry can drive the fence through the hot-reloaded overlay.
Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
leseb
left a comment
There was a problem hiding this comment.
Findings
[MAJOR][Defect] Gated round-robin can starve candidates — picker.rs:128
Different entitlement cohorts share one counter. Common interleavings can make a cohort continually select only one eligible backend.
[MINOR][Defect] Request claims are cloned on every gated request — intelligent_route.rs:560
Borrowed label/value pairs can satisfy the synchronous comparison without copying request-derived strings.
[MINOR][Defect] JWT documentation incorrectly says reserved claims never resolve — intelligent_route.md:81
The pinned mapper’s claims.include can restore registered or promoted claims into AuthenticatedIdentity::custom_claims().
Missing proof
[MAJOR] No successful identity-gated or overlay-backed end-to-end test — intelligent_route_claim_gating.rs:49
The example contains no identity producer and can only return 403. Its test would pass even if matching, successful routing, or overlay-label propagation were broken.
| return Err(format!("intelligent_route: match_claims exceeds maximum of {MAX_MATCH_CLAIMS}").into()); | ||
| } | ||
| for gate in &gates { | ||
| if gate.claim.trim().is_empty() || gate.label.trim().is_empty() { |
There was a problem hiding this comment.
.trim() catches whitespace-only strings, but the stored value keeps its whitespace. claim: " grid_region " passes validation yet never matches the JWT's "grid_region" at runtime, silently denying every request. Let's drop the .trim() to match validate_name's pattern, or trim-and-store. Worth adding a length bound on claim/label too, the way validate_labels caps at MAX_LABEL_LEN.
|
I think I would like to make this concept more general to start |
|
@hexfusion PPE already has the policy mechanism for this, but the use case is not fully supported end to end. In PPE, an The missing piece is in the host: the Praxis policy filter does not pass PPE’s I’d steer the PR toward wiring PPE’s constraint into candidate selection, including affinity reuse, then add an authenticated test that reaches an eligible backend. One remaining PPE gap matters for the PR’s broader claim: Do you want to recast the issue/PR based on the above? We can tag team, review, and prioritize this. This could also be a nice demo for the policy engine in Praxis demos. |
redhat-chai-bot
left a comment
There was a problem hiding this comment.
Adversarial Review — feat(routing): fence intelligent_route via entitlement claim
Well-designed entitlement fencing that adds config-driven access control to intelligent_route. Fail-closed design and enumeration-resistant 404 are security-positive choices. Reuses AuthenticatedIdentity cleanly.
Findings
- M1: Exact-equality matching only — hierarchical entitlements (e.g.,
us-east-1matchingus-*) are not supported. The PR correctly states "Richer operators are out of scope" but document this limitation prominently. - M2: Fenced 404 is indistinguishable from unknown model — good for security, but could confuse operators debugging access issues. Consider a debug-level log when a candidate is fenced out.
- L1: Documentation concisely explains the fencing model. 👍
- L2: Reusing
AuthenticatedIdentityfromtoken_rate_limitavoids new identity plumbing. Clean design.
Verdict: Recommend addressing outstanding review comments and rebasing before merge.
AI-generated. Review for accuracy.
Summary
Adds a generic, config-driven fence to
intelligent_route. Candidates carry alabelsmap, andmatch_claimskeeps only candidates whose label equals a claim on the authenticated identity, before the model-name pick. Adding an entitlement dimension (residency, tier, sovereignty) is configuration, not code.It fails closed. When a request carries no authenticated identity, is missing the gated claim, or matches no in-scope candidate, the gateway denies it rather than routing. A fenced-out capability returns the same 404 as an unknown one, so a caller cannot enumerate what it is fenced out of. Session-affinity reuse and weighted selection both respect the fence. The ungated path (no
match_claims) is unchanged and allocates nothing new. It reuses theAuthenticatedIdentityextensiontoken_rate_limitalready reads, so it adds no new plumbing.Matching is exact-equality only. Richer operators (range, hierarchy, set membership) are out of scope and can be added later without breaking this config.
Granting a user an entitlement
The fence holds no per-user state. The entitlement rides in the caller's token as a claim, sourced from the IdP (for example a Keycloak user attribute mapped into the JWT). alice's token carries:
{ "sub": "alice", "grid_region": "eu-west-1", "tier": "gold" }The gateway validates the token, lands
grid_regionon the authenticated identity, andmatch_claimskeeps only candidates whoseregionlabel equals it, so alice routes to theeu-west-1site. Granting or changing alice's residency is an IdP change (set the attribute), never a gateway change. A second dimension is the same move: the IdP addsgrid_tier, and the config adds{ claim: grid_tier, label: tier }plus atierlabel on candidates.Related issue
Closes #1282
Validation
cargo test -p praxis-ai-filters --lib routing::gives 405 passed. Covers matching, unlabeled-candidate exclusion, no-eligible-member, weighted-draws-stay-in-scope, validation bounds, fail-closed-without-identity, and the ungated-unchanged path.intelligent-route-claim-gating.yamlplus a fail-closed integration test.make lintclean. Commits are signed with aSigned-off-bytrailer.Breaking changes
None.
match_claimsandlabelsdefault empty, so existing configs route exactly as before.