use subtle::ConstantTimeEq;
use crate::config::ApiKeyConfig;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthResult {
Allowed,
MissingKey,
InvalidKey,
InsufficientPermission,
}
impl AuthResult {
pub fn is_allowed(&self) -> bool {
matches!(self, AuthResult::Allowed)
}
pub fn error_message(&self, permission: &str) -> Option<String> {
match self {
AuthResult::Allowed => None,
AuthResult::MissingKey => Some("Missing API key".to_string()),
AuthResult::InvalidKey => Some("Invalid API key".to_string()),
AuthResult::InsufficientPermission => {
Some(format!("API key does not have '{permission}' permission"))
}
}
}
}
pub fn extract_bearer_from_value(value: &str) -> Option<&str> {
value.strip_prefix("Bearer ")
}
pub fn find_key_ct<'a>(api_keys: &'a [ApiKeyConfig], key: &str) -> Option<&'a ApiKeyConfig> {
let key_bytes = key.as_bytes();
let mut matched: Option<&'a ApiKeyConfig> = None;
for entry in api_keys {
let stored = entry.key.as_bytes();
if stored.len() != key_bytes.len() {
continue;
}
if stored.ct_eq(key_bytes).unwrap_u8() == 1 && matched.is_none() {
matched = Some(entry);
}
}
matched
}
pub fn check_auth(
api_keys: &[ApiKeyConfig],
provided_key: Option<&str>,
permission: &str,
) -> AuthResult {
if api_keys.is_empty() {
return AuthResult::MissingKey;
}
let key = match provided_key {
Some(k) if !k.is_empty() => k,
_ => return AuthResult::MissingKey,
};
match api_keys.iter().find(|k| {
let a = k.key.as_bytes();
let b = key.as_bytes();
a.len() == b.len() && a.ct_eq(b).into()
}) {
None => AuthResult::InvalidKey,
Some(k) if !k.has_permission(permission) => AuthResult::InsufficientPermission,
Some(_) => AuthResult::Allowed,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_keys() -> Vec<ApiKeyConfig> {
vec![
ApiKeyConfig {
key: "rw-key".to_string(),
name: "Read-Write".to_string(),
permissions: vec!["read".to_string(), "write".to_string()],
agent_id: None,
},
ApiKeyConfig {
key: "ro-key".to_string(),
name: "Read-Only".to_string(),
permissions: vec!["read".to_string()],
agent_id: None,
},
]
}
#[test]
fn test_empty_keys_fails_closed() {
assert_eq!(
check_auth(&[], Some("anything"), "write"),
AuthResult::MissingKey
);
assert_eq!(check_auth(&[], None, "read"), AuthResult::MissingKey);
}
#[test]
fn test_missing_key() {
let keys = test_keys();
assert_eq!(check_auth(&keys, None, "read"), AuthResult::MissingKey);
assert_eq!(check_auth(&keys, Some(""), "read"), AuthResult::MissingKey);
}
#[test]
fn test_invalid_key() {
let keys = test_keys();
assert_eq!(
check_auth(&keys, Some("bad-key"), "read"),
AuthResult::InvalidKey
);
}
#[test]
fn test_insufficient_permission() {
let keys = test_keys();
assert_eq!(
check_auth(&keys, Some("ro-key"), "write"),
AuthResult::InsufficientPermission
);
}
#[test]
fn test_valid_read() {
let keys = test_keys();
assert_eq!(
check_auth(&keys, Some("ro-key"), "read"),
AuthResult::Allowed
);
assert_eq!(
check_auth(&keys, Some("rw-key"), "read"),
AuthResult::Allowed
);
}
#[test]
fn test_valid_write() {
let keys = test_keys();
assert_eq!(
check_auth(&keys, Some("rw-key"), "write"),
AuthResult::Allowed
);
}
}