Skip to main content

brainos_core/
auth.rs

1//! Shared authentication logic for all protocol adapters.
2//!
3//! This module is the single source of truth for API key validation and
4//! permission checking. Every adapter (HTTP, WebSocket, gRPC, MCP) must
5//! use these functions instead of rolling their own validation.
6
7use subtle::ConstantTimeEq;
8
9use crate::config::ApiKeyConfig;
10
11/// Result of an authentication check.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum AuthResult {
14    /// Auth is disabled (no API keys configured) — allow all.
15    Open,
16    /// Key is valid and has the requested permission.
17    Allowed,
18    /// No key was provided.
19    MissingKey,
20    /// Key was provided but does not match any configured key.
21    InvalidKey,
22    /// Key is valid but lacks the requested permission.
23    InsufficientPermission,
24}
25
26impl AuthResult {
27    /// Returns `true` if the request should be allowed.
28    pub fn is_allowed(&self) -> bool {
29        matches!(self, AuthResult::Open | AuthResult::Allowed)
30    }
31
32    /// Returns a human-readable error message, or `None` if allowed.
33    pub fn error_message(&self, permission: &str) -> Option<String> {
34        match self {
35            AuthResult::Open | AuthResult::Allowed => None,
36            AuthResult::MissingKey => Some("Missing API key".to_string()),
37            AuthResult::InvalidKey => Some("Invalid API key".to_string()),
38            AuthResult::InsufficientPermission => {
39                Some(format!("API key does not have '{permission}' permission"))
40            }
41        }
42    }
43}
44
45/// Extract a bearer token from an "Authorization" header value string.
46///
47/// E.g. `extract_bearer_from_value("Bearer abc123")` returns `Some("abc123")`.
48pub fn extract_bearer_from_value(value: &str) -> Option<&str> {
49    value.strip_prefix("Bearer ")
50}
51
52/// Validate an API key against the configured keys with permission checking.
53///
54/// - If `api_keys` is empty, auth is disabled and all requests are allowed.
55/// - If `provided_key` is `None`, returns `MissingKey`.
56/// - If the key doesn't match any configured key, returns `InvalidKey`.
57/// - If the key matches but lacks the required permission, returns `InsufficientPermission`.
58/// - Otherwise returns `Allowed`.
59pub fn check_auth(
60    api_keys: &[ApiKeyConfig],
61    provided_key: Option<&str>,
62    permission: &str,
63) -> AuthResult {
64    if api_keys.is_empty() {
65        return AuthResult::Open;
66    }
67
68    let key = match provided_key {
69        Some(k) if !k.is_empty() => k,
70        _ => return AuthResult::MissingKey,
71    };
72
73    match api_keys.iter().find(|k| {
74        let a = k.key.as_bytes();
75        let b = key.as_bytes();
76        a.len() == b.len() && a.ct_eq(b).into()
77    }) {
78        None => AuthResult::InvalidKey,
79        Some(k) if !k.has_permission(permission) => AuthResult::InsufficientPermission,
80        Some(_) => AuthResult::Allowed,
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    fn test_keys() -> Vec<ApiKeyConfig> {
89        vec![
90            ApiKeyConfig {
91                key: "rw-key".to_string(),
92                name: "Read-Write".to_string(),
93                permissions: vec!["read".to_string(), "write".to_string()],
94            },
95            ApiKeyConfig {
96                key: "ro-key".to_string(),
97                name: "Read-Only".to_string(),
98                permissions: vec!["read".to_string()],
99            },
100        ]
101    }
102
103    #[test]
104    fn test_empty_keys_allows_all() {
105        assert_eq!(check_auth(&[], Some("anything"), "write"), AuthResult::Open);
106        assert_eq!(check_auth(&[], None, "read"), AuthResult::Open);
107    }
108
109    #[test]
110    fn test_missing_key() {
111        let keys = test_keys();
112        assert_eq!(check_auth(&keys, None, "read"), AuthResult::MissingKey);
113        assert_eq!(check_auth(&keys, Some(""), "read"), AuthResult::MissingKey);
114    }
115
116    #[test]
117    fn test_invalid_key() {
118        let keys = test_keys();
119        assert_eq!(
120            check_auth(&keys, Some("bad-key"), "read"),
121            AuthResult::InvalidKey
122        );
123    }
124
125    #[test]
126    fn test_insufficient_permission() {
127        let keys = test_keys();
128        assert_eq!(
129            check_auth(&keys, Some("ro-key"), "write"),
130            AuthResult::InsufficientPermission
131        );
132    }
133
134    #[test]
135    fn test_valid_read() {
136        let keys = test_keys();
137        assert_eq!(
138            check_auth(&keys, Some("ro-key"), "read"),
139            AuthResult::Allowed
140        );
141        assert_eq!(
142            check_auth(&keys, Some("rw-key"), "read"),
143            AuthResult::Allowed
144        );
145    }
146
147    #[test]
148    fn test_valid_write() {
149        let keys = test_keys();
150        assert_eq!(
151            check_auth(&keys, Some("rw-key"), "write"),
152            AuthResult::Allowed
153        );
154    }
155}