evidentsource_client/
auth.rs

1//! Authentication support for EvidentSource client
2//!
3//! This module provides credentials and interceptor types for authenticating
4//! with EvidentSource servers.
5//!
6//! # Supported Authentication Methods
7//!
8//! ## Bearer Token (OAuth2/JWT)
9//!
10//! For production use with OAuth2/JWT authentication. The token is sent in the
11//! standard `Authorization: Bearer <token>` header.
12//!
13//! **Important**: Bearer tokens require TLS - never send tokens over insecure connections.
14//!
15//! ```rust,ignore
16//! use evidentsource_client::{EvidentSource, Credentials};
17//!
18//! let es = EvidentSource::connect_with_auth(
19//!     "https://api.example.com:50051",
20//!     Credentials::BearerToken(my_jwt_token),
21//! ).await?;
22//! ```
23//!
24//! ## DevMode
25//!
26//! For local development and testing, use DevMode credentials:
27//!
28//! ```rust,ignore
29//! use evidentsource_client::{EvidentSource, Credentials, DevModeCredentials};
30//!
31//! let es = EvidentSource::connect_with_auth(
32//!     "http://localhost:50051",
33//!     Credentials::DevMode(
34//!         DevModeCredentials::new("dev-user@example.com")
35//!             .with_email("dev@example.com")
36//!             .with_display_name("Developer")
37//!     ),
38//! ).await?;
39//! ```
40
41use tonic::service::Interceptor;
42use tonic::Status;
43
44/// Credentials for authenticating with EvidentSource server.
45#[derive(Clone, Debug)]
46pub enum Credentials {
47    /// Bearer token for OAuth2/JWT authentication.
48    ///
49    /// The token is sent in the standard `Authorization: Bearer <token>` header.
50    ///
51    /// **Important**: Bearer tokens should only be sent over TLS connections.
52    BearerToken(String),
53
54    /// DevMode credentials for local development.
55    ///
56    /// Headers sent:
57    /// - `x-dev-subject` (required)
58    /// - `x-dev-email` (optional)
59    /// - `x-dev-display-name` (optional)
60    /// - `x-dev-grants` (optional, JSON)
61    DevMode(DevModeCredentials),
62
63    /// No authentication.
64    ///
65    /// Only works if the server has `allow_anonymous=true`.
66    None,
67}
68
69/// DevMode credentials for local development and testing.
70#[derive(Clone, Debug, Default)]
71pub struct DevModeCredentials {
72    /// Unique identifier for the user (required).
73    pub subject: String,
74    /// Email address (optional).
75    pub email: Option<String>,
76    /// Display name (optional).
77    pub display_name: Option<String>,
78    /// Authorization grants as JSON string (optional).
79    ///
80    /// Format: `{"global": [...], "databases": {...}, "all_databases": [...]}`
81    pub grants: Option<String>,
82}
83
84impl DevModeCredentials {
85    /// Create new DevMode credentials with the given subject.
86    pub fn new(subject: impl Into<String>) -> Self {
87        Self {
88            subject: subject.into(),
89            ..Default::default()
90        }
91    }
92
93    /// Set the email address.
94    pub fn with_email(mut self, email: impl Into<String>) -> Self {
95        self.email = Some(email.into());
96        self
97    }
98
99    /// Set the display name.
100    pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
101        self.display_name = Some(display_name.into());
102        self
103    }
104
105    /// Set the grants as a JSON string.
106    ///
107    /// Format: `{"global": [...], "databases": {...}, "all_databases": [...]}`
108    pub fn with_grants(mut self, grants: impl Into<String>) -> Self {
109        self.grants = Some(grants.into());
110        self
111    }
112}
113
114/// Tonic interceptor that injects authentication headers into gRPC requests.
115#[derive(Clone, Debug)]
116pub struct AuthInterceptor {
117    credentials: Credentials,
118}
119
120impl AuthInterceptor {
121    /// Create a new auth interceptor with the given credentials.
122    pub fn new(credentials: Credentials) -> Self {
123        Self { credentials }
124    }
125}
126
127impl Interceptor for AuthInterceptor {
128    fn call(&mut self, mut request: tonic::Request<()>) -> Result<tonic::Request<()>, Status> {
129        let metadata = request.metadata_mut();
130
131        match &self.credentials {
132            Credentials::BearerToken(token) => {
133                if let Ok(value) = format!("Bearer {}", token).parse() {
134                    metadata.insert("authorization", value);
135                }
136            }
137            Credentials::DevMode(creds) => {
138                if let Ok(value) = creds.subject.parse() {
139                    metadata.insert("x-dev-subject", value);
140                }
141                if let Some(email) = &creds.email {
142                    if let Ok(value) = email.parse() {
143                        metadata.insert("x-dev-email", value);
144                    }
145                }
146                if let Some(name) = &creds.display_name {
147                    if let Ok(value) = name.parse() {
148                        metadata.insert("x-dev-display-name", value);
149                    }
150                }
151                if let Some(grants) = &creds.grants {
152                    if let Ok(value) = grants.parse() {
153                        metadata.insert("x-dev-grants", value);
154                    }
155                }
156            }
157            Credentials::None => {}
158        }
159
160        Ok(request)
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn test_dev_mode_credentials_builder() {
170        let creds = DevModeCredentials::new("user@example.com")
171            .with_email("user@example.com")
172            .with_display_name("Test User")
173            .with_grants(r#"{"global": ["reader"]}"#);
174
175        assert_eq!(creds.subject, "user@example.com");
176        assert_eq!(creds.email, Some("user@example.com".to_string()));
177        assert_eq!(creds.display_name, Some("Test User".to_string()));
178        assert_eq!(creds.grants, Some(r#"{"global": ["reader"]}"#.to_string()));
179    }
180
181    #[test]
182    fn test_credentials_clone() {
183        let creds = Credentials::BearerToken("test-token".to_string());
184        let cloned = creds.clone();
185        assert!(matches!(cloned, Credentials::BearerToken(t) if t == "test-token"));
186    }
187}