Skip to main content

alibi_api/plugins/api_key/
storage.rs

1use super::{ApiKeyConfig, ApiKeyPlugin};
2use alibi_core::store::CacheAdapter;
3use alibi_core::{
4    ApiKey, AuthContext, AuthError, AuthResult, AuthSchema, CreateApiKey, UpdateApiKey,
5};
6use async_trait::async_trait;
7use chrono::{Duration, Utc};
8use std::collections::HashMap;
9use std::sync::{Arc, OnceLock};
10
11/// API-key persistence policy. Secondary-only admission is a non-atomic merge;
12/// use database fallback for durable, guarded quota and rate-limit admission.
13#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
14pub enum ApiKeyStorageMode {
15    #[default]
16    Database,
17    SecondaryStorage,
18}
19
20/// Application-owned serialized API-key storage. `None` TTL means permanent.
21/// Implementations must propagate failures; these operations are not atomic admission.
22#[async_trait]
23pub trait ApiKeyStorage: Send + Sync {
24    async fn get(&self, key: &str) -> AuthResult<Option<String>>;
25    async fn set(&self, key: &str, value: &str, ttl: Option<Duration>) -> AuthResult<()>;
26    async fn delete(&self, key: &str) -> AuthResult<()>;
27}
28
29struct CacheStorage(Arc<dyn CacheAdapter>);
30#[async_trait]
31impl ApiKeyStorage for CacheStorage {
32    async fn get(&self, key: &str) -> AuthResult<Option<String>> {
33        self.0.get(key).await
34    }
35    async fn set(&self, key: &str, value: &str, ttl: Option<Duration>) -> AuthResult<()> {
36        match ttl {
37            Some(ttl) => self.0.set(key, value, ttl).await,
38            None => self.0.set_without_expiry(key, value).await,
39        }
40    }
41    async fn delete(&self, key: &str) -> AuthResult<()> {
42        self.0.delete(key).await
43    }
44}
45
46impl ApiKeyConfig {
47    pub(super) fn secondary(&self) -> Option<Arc<dyn ApiKeyStorage>> {
48        self.custom_storage.clone().or_else(|| {
49            self.secondary_storage
50                .as_ref()
51                .map(|cache| Arc::new(CacheStorage(Arc::clone(cache))) as Arc<dyn ApiKeyStorage>)
52        })
53    }
54    pub(super) fn uses_database(&self) -> bool {
55        self.storage == ApiKeyStorageMode::Database || self.fallback_to_database
56    }
57    pub(crate) async fn read_key(
58        &self,
59        ctx: &AuthContext<impl AuthSchema>,
60        value: &str,
61        by_hash: bool,
62    ) -> AuthResult<Option<ApiKey>> {
63        if self.storage == ApiKeyStorageMode::SecondaryStorage
64            && let Some(storage) = self.secondary()
65            && let Some(key) = read_storage(storage.as_ref(), value, by_hash).await?
66        {
67            return Ok(Some(key));
68        }
69        if !self.uses_database() {
70            return Ok(None);
71        }
72        let key = if by_hash {
73            ctx.database.get_api_key_by_hash(value).await?
74        } else {
75            ctx.database.get_api_key_by_id(value).await?
76        };
77        if let Some(key) = &key
78            && self.secondary().is_some()
79        {
80            self.cache_key(key).await?;
81        }
82        Ok(key)
83    }
84    pub(super) async fn cache_key(&self, key: &ApiKey) -> AuthResult<()> {
85        if self.storage == ApiKeyStorageMode::Database {
86            return Ok(());
87        }
88        let storage = self
89            .secondary()
90            .ok_or_else(|| AuthError::internal("Secondary storage is required"))?;
91        write_storage(storage, key, self.fallback_to_database).await
92    }
93    pub(super) async fn remove_key(
94        &self,
95        ctx: &AuthContext<impl AuthSchema>,
96        key: &ApiKey,
97    ) -> AuthResult<()> {
98        if self.storage == ApiKeyStorageMode::SecondaryStorage {
99            let storage = self
100                .secondary()
101                .ok_or_else(|| AuthError::internal("Secondary storage is required"))?;
102            remove_storage(storage, key, self.fallback_to_database).await?;
103        }
104        if self.uses_database() {
105            ctx.database.delete_api_key(&key.id).await?;
106        }
107        Ok(())
108    }
109    pub(super) async fn create_stored_key(
110        &self,
111        ctx: &AuthContext<impl AuthSchema>,
112        input: CreateApiKey,
113    ) -> AuthResult<ApiKey> {
114        let key = if self.uses_database() {
115            ctx.database.create_api_key(input).await?
116        } else {
117            let now = timestamp();
118            ApiKey {
119                id: uuid::Uuid::new_v4().to_string(),
120                name: input.name,
121                start: input
122                    .start
123                    .map(|start| String::from_utf16(start.as_utf16()))
124                    .transpose()
125                    .map_err(|_| AuthError::internal("Invalid UTF-16 starting characters"))?,
126                prefix: input.prefix,
127                key_hash: input.key_hash,
128                reference_id: input.reference_id,
129                config_id: input.config_id,
130                refill_interval: input.refill_interval,
131                refill_amount: input.refill_amount,
132                last_refill_at: None,
133                enabled: input.enabled,
134                rate_limit_enabled: input.rate_limit_enabled,
135                rate_limit_time_window: input.rate_limit_time_window,
136                rate_limit_max: input.rate_limit_max,
137                request_count: Some(0.0),
138                remaining: input.remaining,
139                last_request: None,
140                expires_at: input.expires_at,
141                created_at: now.clone(),
142                updated_at: now,
143                permissions: input.permissions,
144                metadata: input.metadata,
145            }
146        };
147        self.cache_key(&key).await?;
148        Ok(key)
149    }
150    pub(super) async fn update_stored_key(
151        &self,
152        ctx: &AuthContext<impl AuthSchema>,
153        id: &str,
154        update: UpdateApiKey,
155    ) -> AuthResult<ApiKey> {
156        let key = if self.uses_database() {
157            ctx.database.update_api_key(id, update).await?
158        } else {
159            let mut key = self
160                .read_key(ctx, id, false)
161                .await?
162                .ok_or_else(|| AuthError::not_found("API Key not found"))?;
163            macro_rules! assign { ($($field:ident),*) => { $(if let Some(value) = update.$field { key.$field = Some(value); })* }; }
164            assign!(
165                name,
166                remaining,
167                rate_limit_time_window,
168                rate_limit_max,
169                refill_interval,
170                refill_amount,
171                permissions,
172                metadata,
173                request_count
174            );
175            if let Some(value) = update.enabled {
176                key.enabled = value;
177            }
178            if let Some(value) = update.rate_limit_enabled {
179                key.rate_limit_enabled = value;
180            }
181            if let Some(value) = update.expires_at {
182                key.expires_at = value;
183            }
184            if let Some(value) = update.last_request {
185                key.last_request = value;
186            }
187            if let Some(value) = update.last_refill_at {
188                key.last_refill_at = value;
189            }
190            key.updated_at = timestamp();
191            key
192        };
193        self.cache_key(&key).await?;
194        Ok(key)
195    }
196    pub(super) async fn list_stored_keys(
197        &self,
198        ctx: &AuthContext<impl AuthSchema>,
199        reference: &str,
200        sort_by: Option<&str>,
201        sort_direction: Option<&str>,
202    ) -> AuthResult<Vec<ApiKey>> {
203        let sorted = |mut keys: Vec<ApiKey>, from_database| {
204            if let Some(sort_by) = sort_by {
205                super::handlers::sort_keys(&mut keys, sort_by, sort_direction, from_database)?;
206            }
207            Ok(keys)
208        };
209        if self.storage == ApiKeyStorageMode::Database {
210            return sorted(
211                ctx.database.list_api_keys_by_reference(reference).await?,
212                true,
213            );
214        }
215        let storage = self.secondary();
216        if let Some(storage) = &storage {
217            let ids = reference_ids(storage.as_ref(), reference).await?;
218            if !ids.is_empty() {
219                let storage = Arc::clone(storage);
220                let keys = storage_map(ids, move |id| {
221                    let storage = Arc::clone(&storage);
222                    async move { read_storage(storage.as_ref(), &id, false).await }
223                })
224                .await?;
225                return sorted(keys.into_iter().flatten().collect(), false);
226            }
227        }
228        if !self.fallback_to_database {
229            return Ok(Vec::new());
230        }
231        let keys = sorted(
232            ctx.database.list_api_keys_by_reference(reference).await?,
233            true,
234        )?;
235        if let Some(storage) = storage
236            && !keys.is_empty()
237        {
238            let cache = Arc::clone(&storage);
239            drop(
240                storage_map(keys.clone(), move |key| {
241                    let storage = Arc::clone(&cache);
242                    async move { write_storage(storage, &key, true).await }
243                })
244                .await?,
245            );
246            let ids: Vec<_> = keys.iter().map(|key| &key.id).collect();
247            storage
248                .set(&ref_index(reference), &serde_json::to_string(&ids)?, None)
249                .await?;
250        }
251        Ok(keys)
252    }
253}
254
255impl ApiKeyPlugin {
256    pub(super) async fn list_storage_keys(
257        &self,
258        ctx: &AuthContext<impl AuthSchema>,
259        reference: &str,
260        sort_by: Option<&str>,
261        sort_direction: Option<&str>,
262    ) -> AuthResult<Vec<ApiKey>> {
263        let mut operations = Vec::new();
264        let mut stores = std::collections::HashSet::new();
265        for config in &self.configurations {
266            let identifier = if config.storage == ApiKeyStorageMode::Database {
267                "database".to_owned()
268            } else if config.custom_storage.is_some() {
269                format!("custom:{}", config.config_id)
270            } else {
271                format!(
272                    "secondary:{:p}:{}",
273                    config
274                        .secondary_storage
275                        .as_ref()
276                        .map_or(std::ptr::null::<()>(), |storage| Arc::as_ptr(storage)
277                            .cast::<()>()),
278                    config.fallback_to_database
279                )
280            };
281            if stores.insert(identifier) {
282                let config = config.clone();
283                let reference = reference.to_owned();
284                let context = AuthContext {
285                    config: Arc::clone(&ctx.config),
286                    database: Arc::clone(&ctx.database),
287                    email_provider: ctx.email_provider.clone(),
288                    metadata: ctx.metadata.clone(),
289                    extensions: ctx.extensions.clone(),
290                };
291                let sort_by = sort_by.map(str::to_owned);
292                let sort_direction = sort_direction.map(str::to_owned);
293                operations.push(start_storage_task(async move {
294                    config
295                        .list_stored_keys(
296                            &context,
297                            &reference,
298                            sort_by.as_deref(),
299                            sort_direction.as_deref(),
300                        )
301                        .await
302                }));
303            }
304        }
305        let groups = storage_results(operations).await?;
306        let mut keys = Vec::new();
307        let mut seen = std::collections::HashSet::new();
308        for group in groups {
309            for key in group {
310                if seen.insert(key.id.clone()) {
311                    keys.push(key);
312                }
313            }
314        }
315        Ok(keys)
316    }
317}
318
319pub(super) fn timestamp() -> String {
320    Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
321}
322fn hash_index(hash: &str) -> String {
323    format!("api-key:{hash}")
324}
325fn id_index(id: &str) -> String {
326    format!("api-key:by-id:{id}")
327}
328fn ref_index(reference: &str) -> String {
329    format!("api-key:by-ref:{reference}")
330}
331pub(super) async fn read_storage(
332    storage: &dyn ApiKeyStorage,
333    value: &str,
334    by_hash: bool,
335) -> AuthResult<Option<ApiKey>> {
336    let index = if by_hash {
337        hash_index(value)
338    } else {
339        id_index(value)
340    };
341    let Some(data) = storage.get(&index).await? else {
342        return Ok(None);
343    };
344    let Ok(mut value) = serde_json::from_str::<serde_json::Value>(&data) else {
345        return Ok(None);
346    };
347    if let Some(metadata) = value.get_mut("metadata")
348        && !metadata.is_null()
349    {
350        *metadata = serde_json::Value::String(serde_json::to_string(metadata)?);
351    }
352    Ok(serde_json::from_value(value).ok())
353}
354pub(super) async fn write_storage(
355    storage: Arc<dyn ApiKeyStorage>,
356    key: &ApiKey,
357    fallback: bool,
358) -> AuthResult<()> {
359    let ttl = key
360        .expires_at
361        .as_deref()
362        .and_then(|date| chrono::DateTime::parse_from_rfc3339(date).ok())
363        .map(|date| date.signed_duration_since(Utc::now()).num_seconds())
364        .filter(|seconds| *seconds > 0)
365        .map(Duration::seconds);
366    let mut value = serde_json::to_value(key)?;
367    let fields = value
368        .as_object_mut()
369        .ok_or_else(|| AuthError::internal("Invalid API key"))?;
370    drop(
371        fields.insert(
372            "metadata".to_owned(),
373            key.metadata
374                .as_deref()
375                .map(serde_json::from_str)
376                .transpose()?
377                .unwrap_or(serde_json::Value::Null),
378        ),
379    );
380    if !fallback && key.permissions.is_none() {
381        drop(fields.remove("permissions"));
382    }
383    let data = serde_json::to_string(&value)?;
384    let hash = hash_index(&key.key_hash);
385    let id = id_index(&key.id);
386    let mut operations = vec![
387        spawn_set(Arc::clone(&storage), hash, data.clone(), ttl),
388        spawn_set(Arc::clone(&storage), id, data, ttl),
389    ];
390    if fallback {
391        operations.push(spawn_delete(
392            Arc::clone(&storage),
393            ref_index(&key.reference_id),
394        ));
395    }
396    drop(storage_results(operations).await?);
397    if fallback {
398        Ok(())
399    } else {
400        modify_reference(storage.as_ref(), &key.reference_id, &key.id, true).await
401    }
402}
403
404type StorageTask<T> = (
405    tokio::sync::oneshot::Receiver<()>,
406    tokio::task::JoinHandle<AuthResult<T>>,
407);
408fn start_storage_task<T: Send + 'static>(
409    operation: impl std::future::Future<Output = AuthResult<T>> + Send + 'static,
410) -> StorageTask<T> {
411    let (sender, receiver) = tokio::sync::oneshot::channel();
412    let handle = tokio::spawn(async move {
413        let mut operation = Box::pin(operation);
414        let mut sender = Some(sender);
415        std::future::poll_fn(|context| {
416            let result = operation.as_mut().poll(context);
417            if let Some(sender) = sender.take() {
418                let _ = sender.send(());
419            }
420            result
421        })
422        .await
423    });
424    (receiver, handle)
425}
426fn spawn_set(
427    storage: Arc<dyn ApiKeyStorage>,
428    index: String,
429    value: String,
430    ttl: Option<Duration>,
431) -> StorageTask<()> {
432    start_storage_task(async move { storage.set(&index, &value, ttl).await })
433}
434fn spawn_delete(storage: Arc<dyn ApiKeyStorage>, index: String) -> StorageTask<()> {
435    start_storage_task(async move { storage.delete(&index).await })
436}
437async fn storage_results<T: Send + 'static>(operations: Vec<StorageTask<T>>) -> AuthResult<Vec<T>> {
438    use futures_util::StreamExt as _;
439    // Source Promise.all starts every IO, then rejects on the first failure;
440    // the other initiated operations continue. First-poll acknowledgements
441    // preserve initiation, and dropped JoinHandles detach instead of cancel.
442    let mut results: Vec<Option<T>> = std::iter::repeat_with(|| None)
443        .take(operations.len())
444        .collect();
445    let pending = futures_util::stream::FuturesUnordered::new();
446    for (index, (started, handle)) in operations.into_iter().enumerate() {
447        started
448            .await
449            .map_err(|error| AuthError::internal(error.to_string()))?;
450        pending.push(async move {
451            handle
452                .await
453                .map_err(|error| AuthError::internal(error.to_string()))?
454                .map(|value| (index, value))
455        });
456    }
457    let mut pending = pending;
458    while let Some(result) = pending.next().await {
459        let (index, value) = result?;
460        *results
461            .get_mut(index)
462            .ok_or_else(|| AuthError::internal("Invalid storage result index"))? = Some(value);
463    }
464    Ok(results.into_iter().flatten().collect())
465}
466async fn storage_map<T, R, F, Fut>(items: Vec<T>, mapper: F) -> AuthResult<Vec<R>>
467where
468    T: Send + 'static,
469    R: Send + 'static,
470    F: Fn(T) -> Fut + Send + Sync + 'static,
471    Fut: std::future::Future<Output = AuthResult<R>> + Send + 'static,
472{
473    use futures_util::StreamExt as _;
474    let mut results: Vec<Option<R>> = std::iter::repeat_with(|| None).take(items.len()).collect();
475    let mut items = items.into_iter().enumerate();
476    let mapper = Arc::new(mapper);
477    let failed = Arc::new(std::sync::atomic::AtomicBool::new(false));
478    let launch = |(index, item)| {
479        let mapper = Arc::clone(&mapper);
480        let failed = Arc::clone(&failed);
481        start_storage_task(async move {
482            match mapper(item).await {
483                Ok(value) => Ok((index, value)),
484                Err(error) => {
485                    failed.store(true, std::sync::atomic::Ordering::SeqCst);
486                    Err(error)
487                }
488            }
489        })
490    };
491    let mut pending = futures_util::stream::FuturesUnordered::new();
492    let initial: Vec<_> = items.by_ref().take(10).map(&launch).collect();
493    for (started, handle) in initial {
494        started
495            .await
496            .map_err(|error| AuthError::internal(error.to_string()))?;
497        pending.push(handle);
498    }
499    while let Some(result) = pending.next().await {
500        let (index, value) = result.map_err(|error| AuthError::internal(error.to_string()))??;
501        *results
502            .get_mut(index)
503            .ok_or_else(|| AuthError::internal("Invalid storage result index"))? = Some(value);
504        if !failed.load(std::sync::atomic::Ordering::SeqCst)
505            && let Some(item) = items.next()
506        {
507            let (started, handle) = launch(item);
508            started
509                .await
510                .map_err(|error| AuthError::internal(error.to_string()))?;
511            pending.push(handle);
512        }
513    }
514    Ok(results.into_iter().flatten().collect())
515}
516async fn reference_ids(storage: &dyn ApiKeyStorage, reference: &str) -> AuthResult<Vec<String>> {
517    Ok(storage
518        .get(&ref_index(reference))
519        .await?
520        .and_then(|data| serde_json::from_str(&data).ok())
521        .unwrap_or_default())
522}
523async fn modify_reference(
524    storage: &dyn ApiKeyStorage,
525    reference: &str,
526    id: &str,
527    add: bool,
528) -> AuthResult<()> {
529    static LOCKS: OnceLock<
530        tokio::sync::Mutex<HashMap<String, std::sync::Weak<tokio::sync::Mutex<()>>>>,
531    > = OnceLock::new();
532    let index = ref_index(reference);
533    let lock = {
534        let mut locks = LOCKS.get_or_init(Default::default).lock().await;
535        locks.retain(|_, lock| lock.strong_count() > 0);
536        if let Some(lock) = locks.get(&index).and_then(std::sync::Weak::upgrade) {
537            lock
538        } else {
539            let lock = Arc::new(tokio::sync::Mutex::new(()));
540            drop(locks.insert(index.clone(), Arc::downgrade(&lock)));
541            lock
542        }
543    };
544    let _guard = lock.lock().await;
545    let mut ids = reference_ids(storage, reference).await?;
546    if add {
547        if !ids.iter().any(|value| value == id) {
548            ids.push(id.to_owned());
549        }
550    } else {
551        ids.retain(|value| value != id);
552    }
553    if ids.is_empty() {
554        storage.delete(&index).await
555    } else {
556        storage
557            .set(&index, &serde_json::to_string(&ids)?, None)
558            .await
559    }
560}
561
562pub(super) async fn remove_storage(
563    storage: Arc<dyn ApiKeyStorage>,
564    key: &ApiKey,
565    fallback: bool,
566) -> AuthResult<()> {
567    let hash = hash_index(&key.key_hash);
568    let id = id_index(&key.id);
569    let reference = ref_index(&key.reference_id);
570    let mut operations = vec![
571        spawn_delete(Arc::clone(&storage), hash),
572        spawn_delete(Arc::clone(&storage), id),
573    ];
574    if fallback {
575        operations.push(spawn_delete(storage, reference));
576    } else {
577        let reference = key.reference_id.clone();
578        let id = key.id.clone();
579        operations.push(start_storage_task(async move {
580            modify_reference(storage.as_ref(), &reference, &id, false).await
581        }));
582    }
583    storage_results(operations).await.map(|_| ())
584}