feat(token-rate-limit): key quotas by authenticated subject - #980
feat(token-rate-limit): key quotas by authenticated subject#980nerdalert wants to merge 1 commit into
Conversation
Add an opt-in authenticated_subject quota key that consumes Praxis request-local identity, hashes it into an opaque backend key, and fails closed when verified identity is unavailable. Global keying remains the default. Signed-off-by: Brent Salisbury <bsalisbu@redhat.com>
8dd9aa0 to
0a93307
Compare
leseb
left a comment
There was a problem hiding this comment.
need a new praxis core version
praxis-bot
left a comment
There was a problem hiding this comment.
praxis-bot review: feat(token-rate-limit): key quotas by authenticated subject
Clean, well-structured change. The security model is sound: fail-closed on missing identity, SHA-256 hashing before storage, trusted extension only (never caller-controlled headers). Two findings, both medium.
| # | Severity | File | Finding |
|---|---|---|---|
| 1 | Medium | mod.rs |
Stale doc comment on AdmittedReservation::key |
| 2 | Medium | tests.rs |
Missing positive-path unit test for authenticated-subject keying |
1. Stale doc comment (not in diff -- body-only note)
AdmittedReservation::key (mod.rs ~L709) still reads:
The budget key this reservation was admitted under (see
FALLBACK_KEY-- always that sentinel in this milestone).
After this PR the key can be a subject-derived hash (subject:v1:<base64>), so "always that sentinel" is no longer accurate. Update to reflect that the key is either FALLBACK_KEY (global mode) or a hashed subject (authenticated-subject mode).
2. Missing positive-path unit test
See inline comment.
| let mut ctx = crate::test_utils::make_filter_context(&req); | ||
|
|
||
| let action = filter.on_request(&mut ctx).await.unwrap(); | ||
|
|
There was a problem hiding this comment.
Medium -- missing positive-path unit test for authenticated-subject keying.
This test proves the None (fail-closed) path, but no test exercises the Some path: two requests with different AuthenticatedIdentity subjects getting independent budgets through on_request.
ctx.extensions is public, so the test is straightforward:
#[tokio::test]
async fn authenticated_subject_keying_partitions_budgets_by_identity() {
let yaml = single_rule_yaml_with(
"key: authenticated_subject",
"algorithm: sliding_window\nwindow: 1h\ncapacity: 100\nreserved_tokens: 100",
);
let filter = TokenRateLimitFilter::from_config(&yaml).unwrap();
let req = crate::test_utils::make_request(http::Method::POST, "/v1/chat");
// Subject A exhausts its budget.
let mut ctx_a = crate::test_utils::make_filter_context(&req);
ctx_a.extensions.insert(AuthenticatedIdentity::new("app-a"));
assert!(matches!(filter.on_request(&mut ctx_a).await.unwrap(), FilterAction::Continue));
let mut ctx_a2 = crate::test_utils::make_filter_context(&req);
ctx_a2.extensions.insert(AuthenticatedIdentity::new("app-a"));
assert!(matches!(filter.on_request(&mut ctx_a2).await.unwrap(), FilterAction::Reject(_)));
// Subject B is independent -- still has full budget.
let mut ctx_b = crate::test_utils::make_filter_context(&req);
ctx_b.extensions.insert(AuthenticatedIdentity::new("app-b"));
assert!(matches!(filter.on_request(&mut ctx_b).await.unwrap(), FilterAction::Continue));
}This would close the unit-level coverage gap by proving the end-to-end wiring from extension lookup through resolve_key through per-key backend isolation. Adjust the AuthenticatedIdentity constructor to whatever the actual API is.
🔴 Dependency and merge readiness
Praxis PR praxis-proxy/praxis#1108 has merged into Praxis
main. This AI PR is therefore ready to leave draft and receive review.It can merge after all of the following are complete:
0.5.4to that released version.AuthenticatedIdentityandkey: authenticated_subjectconsumes it successfully without any local path or[patch.crates-io]override.Merging the code before that release would not change existing quota behavior because
key: globalremains the default. However, Basic Auth withkey: authenticated_subjectwould fail closed with HTTP 401 while AI still resolves Praxis0.5.4. The dependency bump and integration test are therefore required before merge, not merely before publication.Summary
Adds an opt-in
authenticated_subjectkey to the token-rate-limit filter. When selected, each verified application or user receives an independent quota while gateway replicas share that subject's budget through the existing Valkey backend.The default remains
global, preserving the current behavior and configuration compatibility.Cross-repository contract
This complements the broader token-rate-limiting work tracked in #121. It implements one secure bucket-key source; it does not close the epic.
Praxis PR praxis-proxy/praxis#1108 is merged. It makes Basic Auth publish its verified username through the private request-local
AuthenticatedIdentityextension. This filter consumes that same authentication-neutral type. Policy/JWT authentication already uses the type, allowing the quota key to be reused by JWT/OIDC/OAuth-backed authentication without coupling token quota to Basic Auth.The important boundary is that AI never derives quota identity from a caller-controlled header.
Behavior
key: authenticated_subjectat the token-rate-limit filter level.key: globalas the default.AuthenticatedIdentitysubject.Use case
Three applications can authenticate through the same endpoint and share the same quota rule configuration while receiving separate subject-keyed budgets. Requests for one application share quota state across gateway replicas, but cannot consume another application's capacity. Grid provider selection remains independent and occurs only after quota admission.
Tests and validation
Focused coverage includes:
The complete Grid qualification passed twice, 9/9 scenarios per run, using this code with the merged Praxis identity producer. The committed branch contains no
[patch.crates-io], sibling path dependency, generated evidence, or local build compatibility changes.Related to #121 and praxis-proxy/grid#101.
Depends on praxis-proxy/praxis#1108.
Companion qualification: praxis-proxy/grid#127.
Companion demo: praxis-proxy/demos#20.