Skip to main content

datasynth_server/grpc/
auth_interceptor.rs

1//! gRPC authentication interceptor.
2//!
3//! Validates API keys or JWT tokens from the `authorization` metadata key.
4
5use tonic::{Request, Status};
6
7/// API key validator function type for gRPC.
8pub type ApiKeyValidator = Box<dyn Fn(&str) -> bool + Send + Sync>;
9
10/// gRPC authentication interceptor configuration.
11#[derive(Clone)]
12pub struct GrpcAuthConfig {
13    /// Whether authentication is enabled.
14    pub enabled: bool,
15    /// Valid API keys (plaintext for gRPC — production should use hashed).
16    api_keys: Vec<String>,
17}
18
19impl GrpcAuthConfig {
20    /// Create auth config with specified API keys.
21    pub fn new(api_keys: Vec<String>) -> Self {
22        Self {
23            enabled: !api_keys.is_empty(),
24            api_keys,
25        }
26    }
27
28    /// Create disabled auth config.
29    pub fn disabled() -> Self {
30        Self {
31            enabled: false,
32            api_keys: Vec::new(),
33        }
34    }
35
36    /// Validate a token against configured keys.
37    pub fn validate_token(&self, token: &str) -> bool {
38        if !self.enabled {
39            return true;
40        }
41        self.api_keys.iter().any(|k| k == token)
42    }
43}
44
45/// Intercept gRPC requests to validate authentication.
46///
47/// Checks for `authorization` metadata key with `Bearer <token>` format.
48#[allow(clippy::result_large_err)]
49pub fn auth_interceptor(config: &GrpcAuthConfig, request: &Request<()>) -> Result<(), Status> {
50    if !config.enabled {
51        return Ok(());
52    }
53
54    let token = request
55        .metadata()
56        .get("authorization")
57        .and_then(|v| v.to_str().ok())
58        .and_then(|s| s.strip_prefix("Bearer "));
59
60    match token {
61        Some(t) if config.validate_token(t) => Ok(()),
62        Some(_) => Err(Status::unauthenticated("Invalid credentials")),
63        None => Err(Status::unauthenticated(
64            "Missing authorization metadata. Provide 'authorization: Bearer <token>'",
65        )),
66    }
67}
68
69#[cfg(test)]
70#[allow(clippy::unwrap_used)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn test_disabled_auth_passes() {
76        let config = GrpcAuthConfig::disabled();
77        let request = Request::new(());
78        assert!(auth_interceptor(&config, &request).is_ok());
79    }
80
81    #[test]
82    fn test_missing_token_fails() {
83        let config = GrpcAuthConfig::new(vec!["secret".to_string()]);
84        let request = Request::new(());
85        assert!(auth_interceptor(&config, &request).is_err());
86    }
87
88    #[test]
89    fn test_valid_token_passes() {
90        let config = GrpcAuthConfig::new(vec!["my-key".to_string()]);
91        let mut request = Request::new(());
92        request
93            .metadata_mut()
94            .insert("authorization", "Bearer my-key".parse().unwrap());
95        assert!(auth_interceptor(&config, &request).is_ok());
96    }
97
98    #[test]
99    fn test_invalid_token_fails() {
100        let config = GrpcAuthConfig::new(vec!["my-key".to_string()]);
101        let mut request = Request::new(());
102        request
103            .metadata_mut()
104            .insert("authorization", "Bearer wrong-key".parse().unwrap());
105        assert!(auth_interceptor(&config, &request).is_err());
106    }
107}