micromegas_auth/types.rs
1use crate::groups::ADMINS_GROUP;
2use anyhow::Result;
3use chrono::{DateTime, Utc};
4use std::sync::Arc;
5
6/// A DB-backed auth provider (e.g. `DbApiKeyAuthProvider`) could not reach its
7/// key store — distinguishes a store *outage* from a rejected credential all
8/// the way out to the HTTP/gRPC response.
9///
10/// Wraps the underlying `anyhow::Error` rather than replacing it: callers that
11/// only need to log or propagate the error keep using it as an `anyhow::Error`
12/// (via `?`/`From`), while `MultiAuthProvider`, `auth_middleware`, and the gRPC
13/// `AuthService` downcast (`anyhow::Error::downcast_ref`) to detect this specific
14/// kind and map it to a retryable status (503 / `Status::unavailable`) instead of
15/// the blanket "invalid credential" response every other error gets.
16///
17/// Per `rust/CLAUDE.md`'s anyhow-vs-thiserror rule, the retryable/terminal
18/// distinction *is* modeled as an explicit type rather than an ad-hoc string —
19/// that's the part of the rule this satisfies. What it doesn't satisfy: callers
20/// still reach that type via `downcast_ref` instead of matching on it directly,
21/// because `AuthProvider::validate_request` returns `anyhow::Result` and this
22/// plan chose not to change that trait's public return type. Treat this as a
23/// deliberate, scoped exception rather than a full application of the rule.
24#[derive(thiserror::Error, Debug)]
25#[error("auth provider unavailable: {0}")]
26pub struct ProviderUnavailable(#[source] pub anyhow::Error);
27
28/// Authentication type
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum AuthType {
31 /// API key authentication
32 ApiKey,
33 /// OIDC authentication
34 Oidc,
35}
36
37/// Authentication context containing user identity and metadata
38#[derive(Debug, Clone)]
39pub struct AuthContext {
40 /// Unique subject identifier (e.g., user ID, service account ID)
41 pub subject: String,
42 /// Email address (if available)
43 pub email: Option<String>,
44 /// Issuer (for OIDC) or "api_key" for API key auth
45 pub issuer: String,
46 /// Audience that was matched during token validation
47 /// - For OIDC access tokens: API audience (e.g., "<https://api.example.com>")
48 /// - For OIDC ID tokens: client ID
49 /// - For API key auth: None
50 pub audience: Option<String>,
51 /// Token expiration time (if applicable)
52 pub expires_at: Option<DateTime<Utc>>,
53 /// Authentication type
54 pub auth_type: AuthType,
55 /// Whether this authentication allows user delegation (acting on behalf of others)
56 /// - OIDC user tokens: false (user cannot impersonate others)
57 /// - Analytics API keys/service accounts: true (can act on behalf of users)
58 /// - Ingestion API keys: false — a write credential is not a delegating service account, and
59 /// it can never reach the gRPC path this flag governs anyway (`ingestion_api_keys` never
60 /// crosses `flight_sql_service_impl.rs`)
61 pub allow_delegation: bool,
62 /// The write audience an ingestion key is immutably bound to. `Some(..)` for every ingestion
63 /// API key (the `ingestion_api_keys.audience` column is `NOT NULL` as of migration v6);
64 /// `None` for every other principal kind. Write-side only; never consulted by `ReadPolicy`.
65 pub bound_audience: Option<String>,
66 /// The set of audiences an analytics service-account key is granted read access to. Empty for
67 /// every principal today — no analytics key provider populates it yet. Read-side only; folded
68 /// into `AudienceReadPolicy::resolve`'s union but never into `AudienceMintPolicy`'s mintable
69 /// set (a read grant confers no mint authority).
70 pub read_audiences: Vec<String>,
71 /// The caller's resolved, transitive local-group membership -- filled by
72 /// [`crate::membership::MembershipProvider`] from a [`crate::groups::GroupGraph`] snapshot,
73 /// **after** the inner provider authenticates the request. Empty until a
74 /// `MembershipProvider` fills it (disabled-auth and maintenance contexts set this to
75 /// `Arc::from([ADMINS_GROUP.to_string()])` directly instead). `AudienceReadPolicy`/
76 /// `AudienceMintPolicy` match each entry against `group:<name>` grant-map *selectors* via
77 /// [`crate::policy::caller_selectors`]/`selector_matches`.
78 pub memberships: Arc<[String]>,
79}
80
81impl AuthContext {
82 /// `true` iff `memberships` contains [`ADMINS_GROUP`] -- a method, not a field, so no
83 /// construction site can set it inconsistently with `memberships`.
84 pub fn is_admin(&self) -> bool {
85 self.memberships.iter().any(|g| g == ADMINS_GROUP)
86 }
87}
88
89/// Trait for extracting authentication-relevant data from requests
90pub trait RequestParts: Send + Sync {
91 /// Extract Authorization header as string
92 fn authorization_header(&self) -> Option<&str>;
93
94 /// Extract Bearer token from Authorization header
95 fn bearer_token(&self) -> Option<&str> {
96 self.authorization_header()
97 .and_then(|h| h.strip_prefix("Bearer "))
98 }
99
100 /// Get custom header value by name
101 fn get_header(&self, name: &str) -> Option<&str>;
102
103 /// Get request method (if applicable)
104 fn method(&self) -> Option<&str>;
105
106 /// Get request URI (if applicable)
107 fn uri(&self) -> Option<&str>;
108
109 /// The resolved client IP, `None` when nothing resolves (see
110 /// [`crate::client_ip::resolve_client_ip`]). This is what
111 /// [`crate::ip_allowlist::IpAllowlist::allows`] checks a restricted key's request against;
112 /// `None` never satisfies a non-empty allowlist.
113 fn client_ip(&self) -> Option<std::net::IpAddr>;
114}
115
116/// HTTP request validation input
117pub struct HttpRequestParts {
118 /// HTTP headers
119 pub headers: http::HeaderMap,
120 /// HTTP method
121 pub method: http::Method,
122 /// Request URI
123 pub uri: http::Uri,
124 /// The resolved client IP, computed once by the caller (see
125 /// [`crate::client_ip::resolve_client_ip`]) before this struct is built.
126 pub client_ip: Option<std::net::IpAddr>,
127}
128
129impl RequestParts for HttpRequestParts {
130 fn authorization_header(&self) -> Option<&str> {
131 self.headers
132 .get(http::header::AUTHORIZATION)
133 .and_then(|h| h.to_str().ok())
134 }
135
136 fn get_header(&self, name: &str) -> Option<&str> {
137 self.headers.get(name).and_then(|h| h.to_str().ok())
138 }
139
140 fn method(&self) -> Option<&str> {
141 Some(self.method.as_str())
142 }
143
144 fn uri(&self) -> Option<&str> {
145 Some(self.uri.path())
146 }
147
148 fn client_ip(&self) -> Option<std::net::IpAddr> {
149 self.client_ip
150 }
151}
152
153/// gRPC request validation input (tonic metadata)
154pub struct GrpcRequestParts {
155 /// gRPC metadata map
156 pub metadata: tonic::metadata::MetadataMap,
157 /// The resolved client IP, computed once by the caller (see
158 /// [`crate::client_ip::resolve_client_ip`]) before this struct is built.
159 pub client_ip: Option<std::net::IpAddr>,
160}
161
162impl RequestParts for GrpcRequestParts {
163 fn authorization_header(&self) -> Option<&str> {
164 self.metadata
165 .get("authorization")
166 .and_then(|h| h.to_str().ok())
167 }
168
169 fn get_header(&self, name: &str) -> Option<&str> {
170 self.metadata.get(name).and_then(|h| h.to_str().ok())
171 }
172
173 fn method(&self) -> Option<&str> {
174 None
175 }
176
177 fn uri(&self) -> Option<&str> {
178 None
179 }
180
181 fn client_ip(&self) -> Option<std::net::IpAddr> {
182 self.client_ip
183 }
184}
185
186/// Trait for authentication providers
187#[async_trait::async_trait]
188pub trait AuthProvider: Send + Sync {
189 /// Validate a request and return authentication context
190 async fn validate_request(&self, parts: &dyn RequestParts) -> Result<AuthContext>;
191}