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}