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