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}