Skip to main content

shaperail_runtime/auth/
api_key.rs

1use std::collections::HashMap;
2
3use super::extractor::AuthenticatedUser;
4
5/// In-memory store mapping API keys to authenticated users.
6///
7/// API keys are provided via `X-API-Key` header as an alternative to JWT.
8/// Keys are loaded at startup (from config or environment).
9#[derive(Debug, Clone)]
10pub struct ApiKeyStore {
11    keys: HashMap<String, AuthenticatedUser>,
12}
13
14impl ApiKeyStore {
15    /// Creates an empty API key store.
16    pub fn new() -> Self {
17        Self {
18            keys: HashMap::new(),
19        }
20    }
21
22    /// Registers an API key mapping to a user identity.
23    pub fn insert(&mut self, key: String, user_id: String, role: String) {
24        self.keys.insert(
25            key,
26            AuthenticatedUser {
27                id: user_id,
28                role,
29                tenant_id: None,
30            },
31        );
32    }
33
34    /// Looks up an API key and returns the associated user, if valid.
35    pub fn lookup(&self, key: &str) -> Option<AuthenticatedUser> {
36        self.keys.get(key).cloned()
37    }
38
39    /// Returns the number of registered API keys.
40    pub fn len(&self) -> usize {
41        self.keys.len()
42    }
43
44    /// Returns true if no API keys are registered.
45    pub fn is_empty(&self) -> bool {
46        self.keys.is_empty()
47    }
48}
49
50impl Default for ApiKeyStore {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn insert_and_lookup() {
62        let mut store = ApiKeyStore::new();
63        store.insert(
64            "sk-test-123".to_string(),
65            "user-1".to_string(),
66            "admin".to_string(),
67        );
68        let user = store.lookup("sk-test-123");
69        assert!(user.is_some());
70        let user = user.unwrap();
71        assert_eq!(user.id, "user-1");
72        assert_eq!(user.role, "admin");
73    }
74
75    #[test]
76    fn lookup_missing_returns_none() {
77        let store = ApiKeyStore::new();
78        assert!(store.lookup("nonexistent").is_none());
79    }
80
81    #[test]
82    fn len_and_is_empty() {
83        let mut store = ApiKeyStore::new();
84        assert!(store.is_empty());
85        assert_eq!(store.len(), 0);
86
87        store.insert("k1".to_string(), "u1".to_string(), "admin".to_string());
88        assert!(!store.is_empty());
89        assert_eq!(store.len(), 1);
90    }
91}