Skip to main content

ralph_api/
auth.rs

1use std::sync::Arc;
2
3use anyhow::Result;
4use axum::http::{HeaderMap, header};
5
6use crate::config::{ApiConfig, AuthMode};
7use crate::errors::ApiError;
8use crate::protocol::RpcRequestEnvelope;
9
10pub trait Authenticator: Send + Sync {
11    fn authorize(
12        &self,
13        request: &RpcRequestEnvelope,
14        headers: &HeaderMap,
15    ) -> Result<String, ApiError>;
16    fn mode(&self) -> AuthMode;
17}
18
19#[derive(Debug, Clone, Default)]
20pub struct TrustedLocalAuthenticator;
21
22impl Authenticator for TrustedLocalAuthenticator {
23    fn authorize(
24        &self,
25        _request: &RpcRequestEnvelope,
26        _headers: &HeaderMap,
27    ) -> Result<String, ApiError> {
28        Ok("trusted_local".to_string())
29    }
30
31    fn mode(&self) -> AuthMode {
32        AuthMode::TrustedLocal
33    }
34}
35
36#[derive(Debug, Clone)]
37pub struct TokenAuthenticator {
38    expected_token: String,
39}
40
41impl TokenAuthenticator {
42    pub fn new(expected_token: String) -> Self {
43        Self { expected_token }
44    }
45}
46
47impl Authenticator for TokenAuthenticator {
48    fn authorize(
49        &self,
50        request: &RpcRequestEnvelope,
51        headers: &HeaderMap,
52    ) -> Result<String, ApiError> {
53        let provided_token = token_from_header(headers).or_else(|| {
54            request
55                .meta
56                .as_ref()
57                .and_then(|meta| meta.auth.as_ref())
58                .and_then(|auth| {
59                    if auth.mode == "token" {
60                        auth.token.clone()
61                    } else {
62                        None
63                    }
64                })
65        });
66
67        match provided_token {
68            Some(token) if token == self.expected_token => Ok(token),
69            Some(_) => Err(ApiError::unauthorized("invalid token")),
70            None => Err(ApiError::unauthorized(
71                "token auth is enabled and no token was provided",
72            )),
73        }
74    }
75
76    fn mode(&self) -> AuthMode {
77        AuthMode::Token
78    }
79}
80
81pub fn from_config(config: &ApiConfig) -> Result<Arc<dyn Authenticator>> {
82    match config.auth_mode {
83        AuthMode::TrustedLocal => Ok(Arc::new(TrustedLocalAuthenticator)),
84        AuthMode::Token => {
85            let token = config
86                .token
87                .clone()
88                .filter(|token| !token.trim().is_empty())
89                .ok_or_else(|| anyhow::anyhow!("token auth mode requires RALPH_API_TOKEN"))?;
90            Ok(Arc::new(TokenAuthenticator::new(token)))
91        }
92    }
93}
94
95fn token_from_header(headers: &HeaderMap) -> Option<String> {
96    let raw = headers.get(header::AUTHORIZATION)?;
97    let token = raw.to_str().ok()?;
98    let token = token.trim();
99    token
100        .strip_prefix("Bearer ")
101        .or_else(|| token.strip_prefix("bearer "))
102        .map(std::string::ToString::to_string)
103}
104
105#[cfg(test)]
106mod tests {
107    use axum::http::HeaderMap;
108    use serde_json::json;
109
110    use super::{Authenticator, TokenAuthenticator, from_config};
111    use crate::config::{ApiConfig, AuthMode};
112    use crate::protocol::parse_request;
113
114    #[test]
115    fn token_auth_allows_meta_token() {
116        let request = parse_request(&json!({
117            "apiVersion": "v1",
118            "id": "req-1",
119            "method": "system.health",
120            "params": {},
121            "meta": {
122                "auth": {
123                    "mode": "token",
124                    "token": "secret"
125                }
126            }
127        }))
128        .expect("request should parse");
129
130        let auth = TokenAuthenticator::new("secret".to_string());
131        assert!(auth.authorize(&request, &HeaderMap::new()).is_ok());
132    }
133
134    #[test]
135    fn from_config_requires_token_for_token_mode() {
136        let mut config = ApiConfig::default();
137        config.auth_mode = AuthMode::Token;
138        config.token = None;
139
140        let result = from_config(&config);
141        assert!(result.is_err());
142    }
143}