Skip to main content

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}