Skip to main content

rns_ctl/
auth.rs

1use crate::http::{HttpRequest, HttpResponse};
2use crate::state::ControlPlaneConfigHandle;
3
4/// Check authentication on an HTTP request.
5/// Returns Ok(()) if authenticated, Err(response) with 401 if not.
6pub fn check_auth(
7    req: &HttpRequest,
8    config: &ControlPlaneConfigHandle,
9) -> Result<(), HttpResponse> {
10    let config = config.read().unwrap();
11    if config.disable_auth {
12        return Ok(());
13    }
14
15    let expected = match &config.auth_token {
16        Some(t) => t.as_str(),
17        None => return Ok(()), // No token configured and auth not disabled = open (shouldn't happen)
18    };
19
20    let auth_header = req.headers.get("authorization");
21    match auth_header {
22        Some(val) => {
23            if let Some(token) = val.strip_prefix("Bearer ") {
24                if token == expected {
25                    Ok(())
26                } else {
27                    Err(HttpResponse::unauthorized("Invalid token"))
28                }
29            } else {
30                Err(HttpResponse::unauthorized("Expected Bearer token"))
31            }
32        }
33        None => Err(HttpResponse::unauthorized("Missing Authorization header")),
34    }
35}
36
37/// Check WebSocket auth via query parameter `?token=...`.
38pub fn check_ws_auth(query: &str, config: &ControlPlaneConfigHandle) -> Result<(), HttpResponse> {
39    let config = config.read().unwrap();
40    if config.disable_auth {
41        return Ok(());
42    }
43
44    let expected = match &config.auth_token {
45        Some(t) => t.as_str(),
46        None => return Ok(()),
47    };
48
49    let params = crate::http::parse_query(query);
50    match params.get("token") {
51        Some(token) if token == expected => Ok(()),
52        _ => Err(HttpResponse::unauthorized("Missing or invalid token")),
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59    use std::collections::HashMap;
60
61    fn make_config(token: Option<&str>, disable: bool) -> crate::state::ControlPlaneConfigHandle {
62        std::sync::Arc::new(std::sync::RwLock::new(crate::config::CtlConfig {
63            auth_token: token.map(String::from),
64            disable_auth: disable,
65            ..crate::config::CtlConfig::default()
66        }))
67    }
68
69    fn make_req(auth_header: Option<&str>) -> HttpRequest {
70        let mut headers = HashMap::new();
71        if let Some(val) = auth_header {
72            headers.insert("authorization".into(), val.into());
73        }
74        HttpRequest {
75            method: "GET".into(),
76            path: "/api/info".into(),
77            query: String::new(),
78            headers,
79            body: Vec::new(),
80        }
81    }
82
83    #[test]
84    fn auth_disabled() {
85        let config = make_config(Some("secret"), true);
86        assert!(check_auth(&make_req(None), &config).is_ok());
87    }
88
89    #[test]
90    fn auth_no_token_configured() {
91        let config = make_config(None, false);
92        assert!(check_auth(&make_req(None), &config).is_ok());
93    }
94
95    #[test]
96    fn auth_valid_token() {
97        let config = make_config(Some("secret"), false);
98        assert!(check_auth(&make_req(Some("Bearer secret")), &config).is_ok());
99    }
100
101    #[test]
102    fn auth_invalid_token() {
103        let config = make_config(Some("secret"), false);
104        assert!(check_auth(&make_req(Some("Bearer wrong")), &config).is_err());
105    }
106
107    #[test]
108    fn auth_missing_header() {
109        let config = make_config(Some("secret"), false);
110        assert!(check_auth(&make_req(None), &config).is_err());
111    }
112
113    #[test]
114    fn ws_auth_valid() {
115        let config = make_config(Some("abc"), false);
116        assert!(check_ws_auth("token=abc", &config).is_ok());
117    }
118
119    #[test]
120    fn ws_auth_invalid() {
121        let config = make_config(Some("abc"), false);
122        assert!(check_ws_auth("token=xyz", &config).is_err());
123    }
124}