1use subtle::ConstantTimeEq;
8
9use crate::config::ApiKeyConfig;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum AuthResult {
14 Allowed,
16 MissingKey,
18 InvalidKey,
20 InsufficientPermission,
22}
23
24impl AuthResult {
25 pub fn is_allowed(&self) -> bool {
27 matches!(self, AuthResult::Allowed)
28 }
29
30 pub fn error_message(&self, permission: &str) -> Option<String> {
32 match self {
33 AuthResult::Allowed => None,
34 AuthResult::MissingKey => Some("Missing API key".to_string()),
35 AuthResult::InvalidKey => Some("Invalid API key".to_string()),
36 AuthResult::InsufficientPermission => {
37 Some(format!("API key does not have '{permission}' permission"))
38 }
39 }
40 }
41}
42
43pub fn extract_bearer_from_value(value: &str) -> Option<&str> {
47 value.strip_prefix("Bearer ")
48}
49
50pub fn find_key_ct<'a>(api_keys: &'a [ApiKeyConfig], key: &str) -> Option<&'a ApiKeyConfig> {
64 let key_bytes = key.as_bytes();
65 let mut matched: Option<&'a ApiKeyConfig> = None;
66 for entry in api_keys {
67 let stored = entry.key.as_bytes();
68 if stored.len() != key_bytes.len() {
69 continue;
70 }
71 if stored.ct_eq(key_bytes).unwrap_u8() == 1 && matched.is_none() {
72 matched = Some(entry);
73 }
74 }
75 matched
76}
77
78pub fn check_auth(
86 api_keys: &[ApiKeyConfig],
87 provided_key: Option<&str>,
88 permission: &str,
89) -> AuthResult {
90 if api_keys.is_empty() {
91 return AuthResult::MissingKey;
92 }
93
94 let key = match provided_key {
95 Some(k) if !k.is_empty() => k,
96 _ => return AuthResult::MissingKey,
97 };
98
99 match api_keys.iter().find(|k| {
100 let a = k.key.as_bytes();
101 let b = key.as_bytes();
102 a.len() == b.len() && a.ct_eq(b).into()
110 }) {
111 None => AuthResult::InvalidKey,
112 Some(k) if !k.has_permission(permission) => AuthResult::InsufficientPermission,
113 Some(_) => AuthResult::Allowed,
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 fn test_keys() -> Vec<ApiKeyConfig> {
122 vec![
123 ApiKeyConfig {
124 key: "rw-key".to_string(),
125 name: "Read-Write".to_string(),
126 permissions: vec!["read".to_string(), "write".to_string()],
127 agent_id: None,
128 },
129 ApiKeyConfig {
130 key: "ro-key".to_string(),
131 name: "Read-Only".to_string(),
132 permissions: vec!["read".to_string()],
133 agent_id: None,
134 },
135 ]
136 }
137
138 #[test]
139 fn test_empty_keys_fails_closed() {
140 assert_eq!(
141 check_auth(&[], Some("anything"), "write"),
142 AuthResult::MissingKey
143 );
144 assert_eq!(check_auth(&[], None, "read"), AuthResult::MissingKey);
145 }
146
147 #[test]
148 fn test_missing_key() {
149 let keys = test_keys();
150 assert_eq!(check_auth(&keys, None, "read"), AuthResult::MissingKey);
151 assert_eq!(check_auth(&keys, Some(""), "read"), AuthResult::MissingKey);
152 }
153
154 #[test]
155 fn test_invalid_key() {
156 let keys = test_keys();
157 assert_eq!(
158 check_auth(&keys, Some("bad-key"), "read"),
159 AuthResult::InvalidKey
160 );
161 }
162
163 #[test]
164 fn test_insufficient_permission() {
165 let keys = test_keys();
166 assert_eq!(
167 check_auth(&keys, Some("ro-key"), "write"),
168 AuthResult::InsufficientPermission
169 );
170 }
171
172 #[test]
173 fn test_valid_read() {
174 let keys = test_keys();
175 assert_eq!(
176 check_auth(&keys, Some("ro-key"), "read"),
177 AuthResult::Allowed
178 );
179 assert_eq!(
180 check_auth(&keys, Some("rw-key"), "read"),
181 AuthResult::Allowed
182 );
183 }
184
185 #[test]
186 fn test_valid_write() {
187 let keys = test_keys();
188 assert_eq!(
189 check_auth(&keys, Some("rw-key"), "write"),
190 AuthResult::Allowed
191 );
192 }
193}