Skip to main content

alibi_core/store/stateless/
api_keys.rs

1//! Instance-local API-key rows. Each Source write phase locks independently.
2use 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        // Validate before writing: a bad later field cannot leave a partial update.
64        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        // All stored dates were validated at the write boundary.
105        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        // Commit the combined operation only on success or policy denial.
125        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        // A separate final write returns genuine current fields, never the snapshot.
176        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            // Memory incrementOne treats null as zero. SQL NULL arithmetic is separate.
281            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}