alibi_core/store/stateless/
api_keys.rs1use crate::store::ApiKeyStore;
3use crate::store::stateless::StatelessStore;
4use crate::{
5 ApiKey, ApiKeyStartText, AuthError, AuthResult, ConsumeApiKeyResult, CreateApiKey, UpdateApiKey,
6};
7use async_trait::async_trait;
8use chrono::{DateTime, Utc};
9
10#[async_trait]
11impl ApiKeyStore for StatelessStore {
12 async fn create_api_key(&self, input: CreateApiKey) -> AuthResult<ApiKey> {
13 let now = Utc::now().to_rfc3339();
14 let start = input.start.map(|start| match start.storage_text() {
15 ApiKeyStartText::Utf8(text) => Ok(text),
16 ApiKeyStartText::Wtf8(_) => Err(AuthError::internal(
17 "Unpaired UTF-16 API-key starting characters cannot be represented by the native String schema",
18 )),
19 }).transpose()?;
20 let row = ApiKey {
21 id: uuid::Uuid::new_v4().to_string(),
22 name: input.name,
23 start,
24 prefix: input.prefix,
25 key_hash: input.key_hash,
26 reference_id: input.reference_id,
27 config_id: input.config_id,
28 refill_interval: input.refill_interval,
29 refill_amount: input.refill_amount,
30 last_refill_at: None,
31 enabled: input.enabled,
32 rate_limit_enabled: input.rate_limit_enabled,
33 rate_limit_time_window: input.rate_limit_time_window,
34 rate_limit_max: input.rate_limit_max,
35 request_count: Some(0.0),
36 remaining: input.remaining,
37 last_request: None,
38 expires_at: validated_date(input.expires_at)?,
39 created_at: now.clone(),
40 updated_at: now,
41 permissions: input.permissions,
42 metadata: input.metadata,
43 };
44 _ = self.lock()?.api_keys.insert(row.id.clone(), row.clone());
45 Ok(row)
46 }
47 async fn get_api_key_by_id(&self, id: &str) -> AuthResult<Option<ApiKey>> {
48 Ok(self.lock()?.api_keys.get(id).cloned())
49 }
50 async fn get_api_key_by_hash(&self, hash: &str) -> AuthResult<Option<ApiKey>> {
51 Ok(self
52 .lock()?
53 .api_keys
54 .values()
55 .find(|row| row.key_hash == hash)
56 .cloned())
57 }
58 async fn list_api_keys_by_reference(&self, reference_id: &str) -> AuthResult<Vec<ApiKey>> {
59 Ok(self
60 .lock()?
61 .api_keys
62 .values()
63 .filter(|row| row.reference_id == reference_id)
64 .cloned()
65 .collect())
66 }
67 async fn update_api_key(&self, id: &str, mut update: UpdateApiKey) -> AuthResult<ApiKey> {
68 update.expires_at = update.expires_at.map(validated_date).transpose()?;
70 update.last_request = update.last_request.map(validated_date).transpose()?;
71 update.last_refill_at = update.last_refill_at.map(validated_date).transpose()?;
72 let mut state = self.lock()?;
73 let row = state.api_keys.get_mut(id).ok_or_else(missing_key)?;
74 macro_rules! optional {
75 ($($field:ident),*) => { $(if let Some(value) = update.$field { row.$field = Some(value); })* };
76 }
77 optional!(
78 name,
79 remaining,
80 rate_limit_time_window,
81 rate_limit_max,
82 refill_interval,
83 refill_amount,
84 permissions,
85 metadata,
86 request_count
87 );
88 macro_rules! direct {
89 ($($field:ident),*) => { $(if let Some(value) = update.$field { row.$field = value; })* };
90 }
91 direct!(
92 enabled,
93 rate_limit_enabled,
94 expires_at,
95 last_request,
96 last_refill_at
97 );
98 row.updated_at = Utc::now().to_rfc3339();
99 Ok(row.clone())
100 }
101 async fn delete_api_key(&self, id: &str) -> AuthResult<()> {
102 _ = self.lock()?.api_keys.shift_remove(id);
103 Ok(())
104 }
105 async fn delete_expired_api_keys(&self) -> AuthResult<usize> {
106 let mut state = self.lock()?;
107 let now = Utc::now();
108 let before = state.api_keys.len();
109 state.api_keys.retain(|_, row| {
111 row.expires_at
112 .as_deref()
113 .is_none_or(|value| stored_date(value).is_ok_and(|date| date >= now))
114 });
115 Ok(before - state.api_keys.len())
116 }
117 async fn consume_api_key_usage(
118 &self,
119 id: &str,
120 global_rate_limit_enabled: bool,
121 ) -> AuthResult<ConsumeApiKeyResult> {
122 let mut state = self.lock()?;
123 let stored = state.api_keys.get_mut(id).ok_or_else(missing_key)?;
124 let mut row = stored.clone();
125 let observed = row.clone();
126 if observed.remaining.is_some() && !consume_remaining(&mut row, &observed, Utc::now())? {
127 return Ok(ConsumeApiKeyResult::UsageExhausted);
128 }
129 loop {
131 let observed = row.clone();
132 match consume_rate(&mut row, &observed, global_rate_limit_enabled, Utc::now())? {
133 RateClaim::Denied(delay) => {
134 *stored = row;
135 return Ok(ConsumeApiKeyResult::RateLimited {
136 try_again_in: delay,
137 });
138 }
139 RateClaim::Written => break,
140 RateClaim::Retry => {}
141 }
142 }
143 row.updated_at = Utc::now().to_rfc3339();
144 *stored = row.clone();
145 Ok(ConsumeApiKeyResult::Allowed(Box::new(row)))
146 }
147
148 async fn consume_api_key_usage_from_snapshot(
149 &self,
150 observed: &ApiKey,
151 global_rate_limit_enabled: bool,
152 ) -> AuthResult<ConsumeApiKeyResult> {
153 let mut row = if observed.remaining.is_some() {
154 let mut state = self.lock()?;
155 let Some(current) = state.api_keys.get_mut(&observed.id) else {
156 return Ok(ConsumeApiKeyResult::UsageExhausted);
157 };
158 if !consume_remaining(current, observed, Utc::now())? {
159 return Ok(ConsumeApiKeyResult::UsageExhausted);
160 }
161 current.clone()
162 } else {
163 observed.clone()
164 };
165 loop {
166 let mut state = self.lock()?;
167 let Some(current) = state.api_keys.get_mut(&row.id) else {
168 return Err(invalid_key());
169 };
170 match consume_rate(current, &row, global_rate_limit_enabled, Utc::now())? {
171 RateClaim::Denied(delay) => {
172 return Ok(ConsumeApiKeyResult::RateLimited {
173 try_again_in: delay,
174 });
175 }
176 RateClaim::Written => break,
177 RateClaim::Retry => row = current.clone(),
178 }
179 }
180 let mut state = self.lock()?;
182 let current = state.api_keys.get_mut(&row.id).ok_or_else(invalid_key)?;
183 current.updated_at = Utc::now().to_rfc3339();
184 Ok(ConsumeApiKeyResult::Allowed(Box::new(current.clone())))
185 }
186}
187
188#[expect(
189 clippy::as_conversions,
190 clippy::cast_precision_loss,
191 reason = "Match JavaScript millisecond arithmetic"
192)]
193fn consume_remaining(
194 current: &mut ApiKey,
195 observed: &ApiKey,
196 now: DateTime<Utc>,
197) -> AuthResult<bool> {
198 if let (Some(interval), Some(amount)) = (observed.refill_interval, observed.refill_amount)
199 && interval != 0.0
200 && amount != 0.0
201 && (now.timestamp_millis()
202 - stored_date(
203 observed
204 .last_refill_at
205 .as_deref()
206 .unwrap_or(&observed.created_at),
207 )?
208 .timestamp_millis()) as f64
209 > interval
210 && same_date(
211 current.last_refill_at.as_deref(),
212 observed.last_refill_at.as_deref(),
213 )?
214 {
215 current.remaining = Some(amount - 1.0);
216 current.last_refill_at = Some(now.to_rfc3339());
217 return Ok(true);
218 }
219 if let Some(remaining) = current.remaining
220 && remaining > 0.0
221 {
222 current.remaining = Some(remaining - 1.0);
223 return Ok(true);
224 }
225 Ok(false)
226}
227
228enum RateClaim {
229 Written,
230 Retry,
231 Denied(f64),
232}
233
234#[expect(
235 clippy::as_conversions,
236 clippy::cast_precision_loss,
237 reason = "Match JavaScript millisecond arithmetic and Date TimeClip"
238)]
239fn consume_rate(
240 current: &mut ApiKey,
241 observed: &ApiKey,
242 enabled: bool,
243 now: DateTime<Utc>,
244) -> AuthResult<RateClaim> {
245 if !enabled || !observed.rate_limit_enabled {
246 current.last_request = Some(now.to_rfc3339());
247 return Ok(RateClaim::Written);
248 }
249 let (Some(window), Some(max)) = (observed.rate_limit_time_window, observed.rate_limit_max)
250 else {
251 return Ok(RateClaim::Written);
252 };
253 let previous = observed
254 .last_request
255 .as_deref()
256 .map(stored_date)
257 .transpose()?;
258 let current_previous = current
259 .last_request
260 .as_deref()
261 .map(stored_date)
262 .transpose()?;
263 let elapsed = previous.map(|last| (now.timestamp_millis() - last.timestamp_millis()) as f64);
264 let window_start = (now.timestamp_millis() as f64 - window).trunc();
265 if !window_start.is_finite() || window_start.abs() > 8_640_000_000_000_000.0 {
266 return Err(AuthError::internal(
267 "Invalid API key rate-limit window date",
268 ));
269 }
270 let guard = current_previous.map(|date| date.timestamp_millis() as f64);
271 match elapsed {
272 None if current_previous.is_none() => current.request_count = Some(1.0),
273 Some(elapsed) if elapsed > window && guard.is_some_and(|last| last <= window_start) => {
274 current.request_count = Some(1.0);
275 }
276 Some(elapsed) if elapsed <= window => {
277 if observed.request_count.unwrap_or(0.0) >= max {
278 return Ok(RateClaim::Denied((window - elapsed).ceil()));
279 }
280 if !guard.is_some_and(|last| last > window_start)
281 || current.request_count.unwrap_or(0.0) >= max
282 {
283 return Ok(RateClaim::Retry);
284 }
285 current.request_count = Some(current.request_count.unwrap_or(0.0) + 1.0);
287 }
288 _ => return Ok(RateClaim::Retry),
289 }
290 current.last_request = Some(now.to_rfc3339());
291 Ok(RateClaim::Written)
292}
293
294fn same_date(left: Option<&str>, right: Option<&str>) -> AuthResult<bool> {
295 match (left, right) {
296 (None, None) => Ok(true),
297 (Some(left), Some(right)) => {
298 Ok(stored_date(left)?.timestamp_millis() == stored_date(right)?.timestamp_millis())
299 }
300 _ => Ok(false),
301 }
302}
303fn validated_date(value: Option<String>) -> AuthResult<Option<String>> {
304 value
305 .map(|value| stored_date(&value).map(|_| value))
306 .transpose()
307}
308fn stored_date(value: &str) -> AuthResult<DateTime<Utc>> {
309 DateTime::parse_from_rfc3339(value)
310 .map(|date| date.with_timezone(&Utc))
311 .map_err(|error| AuthError::internal(format!("Invalid stored API key date: {error}")))
312}
313fn missing_key() -> AuthError {
314 AuthError::not_found("API Key not found")
315}
316const fn invalid_key() -> AuthError {
317 AuthError::Upstream {
318 status: 401,
319 code: "INVALID_API_KEY",
320 message: "Invalid API key.",
321 }
322}