liter_llm_proxy/auth/
key_store.rs1use dashmap::DashMap;
2
3use crate::config::VirtualKeyConfig;
4
5#[derive(Debug, Clone)]
7pub struct KeyContext {
8 pub key_id: String,
9 pub allowed_models: Option<Vec<String>>,
10 pub is_master: bool,
11}
12
13impl KeyContext {
14 pub fn master() -> Self {
16 Self {
17 key_id: "master".into(),
18 allowed_models: None,
19 is_master: true,
20 }
21 }
22
23 pub fn from_config(config: &VirtualKeyConfig) -> Self {
25 let allowed_models = if config.models.is_empty() {
26 None
27 } else {
28 Some(config.models.clone())
29 };
30 Self {
31 key_id: config.key.clone(),
32 allowed_models,
33 is_master: false,
34 }
35 }
36
37 pub fn can_access_model(&self, model: &str) -> bool {
39 match &self.allowed_models {
40 None => true,
41 Some(models) => models.iter().any(|m| m == model),
42 }
43 }
44}
45
46pub struct KeyStore {
48 keys: DashMap<String, VirtualKeyConfig>,
49 master_key: Option<String>,
50}
51
52impl KeyStore {
53 pub fn from_config(master_key: Option<String>, keys: &[VirtualKeyConfig]) -> Self {
55 let map = DashMap::new();
56 for k in keys {
57 map.insert(k.key.clone(), k.clone());
58 }
59 Self { keys: map, master_key }
60 }
61
62 pub fn is_master_key(&self, token: &str) -> bool {
64 self.master_key.as_deref() == Some(token)
65 }
66
67 pub fn get(&self, token: &str) -> Option<VirtualKeyConfig> {
69 self.keys.get(token).map(|r| r.value().clone())
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 fn sample_key_config(key: &str, models: Vec<String>) -> VirtualKeyConfig {
78 VirtualKeyConfig {
79 key: key.to_string(),
80 description: None,
81 models,
82 rpm: Some(60),
83 tpm: Some(100_000),
84 budget_limit: Some(50.0),
85 }
86 }
87
88 #[test]
91 fn master_key_match_returns_true() {
92 let store = KeyStore::from_config(Some("sk-master".into()), &[]);
93 assert!(store.is_master_key("sk-master"));
94 }
95
96 #[test]
97 fn master_key_mismatch_returns_false() {
98 let store = KeyStore::from_config(Some("sk-master".into()), &[]);
99 assert!(!store.is_master_key("sk-wrong"));
100 }
101
102 #[test]
103 fn no_master_key_always_returns_false() {
104 let store = KeyStore::from_config(None, &[]);
105 assert!(!store.is_master_key("sk-anything"));
106 }
107
108 #[test]
109 fn get_existing_key_returns_config() {
110 let cfg = sample_key_config("vk-team-a", vec!["gpt-4o".into()]);
111 let store = KeyStore::from_config(None, std::slice::from_ref(&cfg));
112
113 let result = store.get("vk-team-a");
114 assert!(result.is_some());
115 let found = result.unwrap();
116 assert_eq!(found.key, "vk-team-a");
117 assert_eq!(found.models, vec!["gpt-4o"]);
118 }
119
120 #[test]
121 fn get_nonexistent_key_returns_none() {
122 let store = KeyStore::from_config(None, &[]);
123 assert!(store.get("vk-missing").is_none());
124 }
125
126 #[test]
129 fn master_context_has_no_restrictions() {
130 let ctx = KeyContext::master();
131 assert!(ctx.is_master);
132 assert!(ctx.allowed_models.is_none());
133 assert!(ctx.can_access_model("any-model"));
134 }
135
136 #[test]
137 fn context_with_allowed_models_permits_listed_model() {
138 let cfg = sample_key_config("vk-1", vec!["gpt-4o".into(), "claude-sonnet".into()]);
139 let ctx = KeyContext::from_config(&cfg);
140
141 assert!(!ctx.is_master);
142 assert!(ctx.can_access_model("gpt-4o"));
143 assert!(ctx.can_access_model("claude-sonnet"));
144 }
145
146 #[test]
147 fn context_with_allowed_models_denies_unlisted_model() {
148 let cfg = sample_key_config("vk-1", vec!["gpt-4o".into()]);
149 let ctx = KeyContext::from_config(&cfg);
150
151 assert!(!ctx.can_access_model("claude-sonnet"));
152 }
153
154 #[test]
155 fn context_with_empty_models_allows_all() {
156 let cfg = sample_key_config("vk-1", vec![]);
157 let ctx = KeyContext::from_config(&cfg);
158
159 assert!(ctx.allowed_models.is_none());
160 assert!(ctx.can_access_model("any-model"));
161 }
162}