- ERRORS_BY_USER: labeled counter {user_id, endpoint, error_name}
- REQUESTS_BY_USER: labeled counter {user_id, endpoint}
- extract_user_id(): decode JWT sub claim from Authorization header
- Error names: auth_failure, forbidden, rate_limited, bad_request, embedding_failure
- Ingest handler: tracks user_id from claims.sub
- Query handler: tracks user_id from JWT decode
- Context handler: tracks user_id from claims.sub
- render_labeled_counter(): generic Prometheus label renderer
- User identity from gateway JWT (claims.sub per API.md)
- 515 tests passing
This commit is contained in:
@@ -61,6 +61,49 @@ pub fn validate_and_rate_limit(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extract user identity from JWT claims (sub field)
|
||||
///
|
||||
/// Tries to decode JWT from Authorization header to get `sub` claim.
|
||||
/// Falls back to "anonymous" if auth is disabled or header missing.
|
||||
/// Used by metrics to track errors/requests per user.
|
||||
pub fn extract_user_id(req: &HttpRequest, state: &AppState) -> String {
|
||||
// If auth disabled, check synthetic claims
|
||||
if state.jwt_validator.is_none() {
|
||||
return "anonymous".to_string();
|
||||
}
|
||||
|
||||
// Try to extract sub from JWT
|
||||
let token = req.headers()
|
||||
.get("Authorization")
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.and_then(|h| h.strip_prefix("Bearer "))
|
||||
.unwrap_or("");
|
||||
|
||||
if token.is_empty() {
|
||||
return "anonymous".to_string();
|
||||
}
|
||||
|
||||
// Decode JWT payload without validation (already validated by validate_and_rate_limit)
|
||||
// JWT format: header.payload.signature
|
||||
let parts: Vec<&str> = token.split('.').collect();
|
||||
if parts.len() != 3 {
|
||||
return "anonymous".to_string();
|
||||
}
|
||||
|
||||
// Decode base64 payload
|
||||
use base64::Engine;
|
||||
let engine = base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||
if let Ok(payload_bytes) = engine.decode(parts[1]) {
|
||||
if let Ok(payload) = serde_json::from_slice::<serde_json::Value>(&payload_bytes) {
|
||||
if let Some(sub) = payload.get("sub").and_then(|s| s.as_str()) {
|
||||
return sub.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"anonymous".to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user