Skip to main content

alibi_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            _ = storage_map(keys.clone(), move |key| {
240                let storage = Arc::clone(&cache);
241                async move { write_storage(storage, &key, true).await }
242            })
243            .await?;
244            let ids: Vec<_> = keys.iter().map(|key| &key.id).collect();
245            storage
246                .set(&ref_index(reference), &serde_json::to_string(&ids)?, None)
247                .await?;
248        }
249        Ok(keys)
250    }
251}
252
253impl ApiKeyPlugin {
254    pub(super) async fn list_storage_keys(
255        &self,
256        ctx: &AuthContext<impl AuthSchema>,
257        reference: &str,
258        sort_by: Option<&str>,
259        sort_direction: Option<&str>,
260    ) -> AuthResult<Vec<ApiKey>> {
261        let mut operations = Vec::new();
262        let mut stores = std::collections::HashSet::new();
263        for config in &self.configurations {
264            let identifier = if config.storage == ApiKeyStorageMode::Database {
265                "database".to_owned()
266            } else if config.custom_storage.is_some() {
267                format!("custom:{}", config.config_id)
268            } else {
269                format!(
270                    "secondary:{:p}:{}",
271                    config
272                        .secondary_storage
273                        .as_ref()
274                        .map_or(std::ptr::null::<()>(), |storage| Arc::as_ptr(storage)
275                            .cast::<()>()),
276                    config.fallback_to_database
277                )
278            };
279            if stores.insert(identifier) {
280                let config = config.clone();
281                let reference = reference.to_owned();
282                let context = AuthContext {
283                    config: Arc::clone(&ctx.config),
284                    database: Arc::clone(&ctx.database),
285                    email_provider: ctx.email_provider.clone(),
286                    metadata: ctx.metadata.clone(),
287                    extensions: ctx.extensions.clone(),
288                };
289                let sort_by = sort_by.map(str::to_owned);
290                let sort_direction = sort_direction.map(str::to_owned);
291                operations.push(start_storage_task(async move {
292                    config
293                        .list_stored_keys(
294                            &context,
295                            &reference,
296                            sort_by.as_deref(),
297                            sort_direction.as_deref(),
298                        )
299                        .await
300                }));
301            }
302        }
303        let groups = storage_results(operations).await?;
304        let mut keys = Vec::new();
305        let mut seen = std::collections::HashSet::new();
306        for group in groups {
307            for key in group {
308                if seen.insert(key.id.clone()) {
309                    keys.push(key);
310                }
311            }
312        }
313        Ok(keys)
314    }
315}
316
317pub(super) fn timestamp() -> String {
318    Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
319}
320fn hash_index(hash: &str) -> String {
321    format!("api-key:{hash}")
322}
323fn id_index(id: &str) -> String {
324    format!("api-key:by-id:{id}")
325}
326fn ref_index(reference: &str) -> String {
327    format!("api-key:by-ref:{reference}")
328}
329pub(super) async fn read_storage(
330    storage: &dyn ApiKeyStorage,
331    value: &str,
332    by_hash: bool,
333) -> AuthResult<Option<ApiKey>> {
334    let index = if by_hash {
335        hash_index(value)
336    } else {
337        id_index(value)
338    };
339    let Some(data) = storage.get(&index).await? else {
340        return Ok(None);
341    };
342    let Ok(mut value) = serde_json::from_str::<serde_json::Value>(&data) else {
343        return Ok(None);
344    };
345    if let Some(metadata) = value.get_mut("metadata")
346        && !metadata.is_null()
347    {
348        *metadata = serde_json::Value::String(serde_json::to_string(metadata)?);
349    }
350    Ok(serde_json::from_value(value).ok())
351}
352pub(super) async fn write_storage(
353    storage: Arc<dyn ApiKeyStorage>,
354    key: &ApiKey,
355    fallback: bool,
356) -> AuthResult<()> {
357    let ttl = key
358        .expires_at
359        .as_deref()
360        .and_then(|date| chrono::DateTime::parse_from_rfc3339(date).ok())
361        .map(|date| date.signed_duration_since(Utc::now()).num_seconds())
362        .filter(|seconds| *seconds > 0)
363        .map(Duration::seconds);
364    let mut value = serde_json::to_value(key)?;
365    let fields = value
366        .as_object_mut()
367        .ok_or_else(|| AuthError::internal("Invalid API key"))?;
368    _ = fields.insert(
369        "metadata".to_owned(),
370        key.metadata
371            .as_deref()
372            .map(serde_json::from_str)
373            .transpose()?
374            .unwrap_or(serde_json::Value::Null),
375    );
376    if !fallback && key.permissions.is_none() {
377        _ = fields.remove("permissions");
378    }
379    let data = serde_json::to_string(&value)?;
380    let hash = hash_index(&key.key_hash);
381    let id = id_index(&key.id);
382    let mut operations = vec![
383        spawn_set(Arc::clone(&storage), hash, data.clone(), ttl),
384        spawn_set(Arc::clone(&storage), id, data, ttl),
385    ];
386    if fallback {
387        operations.push(spawn_delete(
388            Arc::clone(&storage),
389            ref_index(&key.reference_id),
390        ));
391    }
392    _ = storage_results(operations).await?;
393    if fallback {
394        Ok(())
395    } else {
396        modify_reference(storage.as_ref(), &key.reference_id, &key.id, true).await
397    }
398}
399
400type StorageTask<T> = (
401    tokio::sync::oneshot::Receiver<()>,
402    tokio::task::JoinHandle<AuthResult<T>>,
403);
404fn start_storage_task<T: Send + 'static>(
405    operation: impl std::future::Future<Output = AuthResult<T>> + Send + 'static,
406) -> StorageTask<T> {
407    let (sender, receiver) = tokio::sync::oneshot::channel();
408    let handle = tokio::spawn(async move {
409        let mut operation = Box::pin(operation);
410        let mut sender = Some(sender);
411        std::future::poll_fn(|context| {
412            let result = operation.as_mut().poll(context);
413            if let Some(sender) = sender.take() {
414                let _ = sender.send(());
415            }
416            result
417        })
418        .await
419    });
420    (receiver, handle)
421}
422fn spawn_set(
423    storage: Arc<dyn ApiKeyStorage>,
424    index: String,
425    value: String,
426    ttl: Option<Duration>,
427) -> StorageTask<()> {
428    start_storage_task(async move { storage.set(&index, &value, ttl).await })
429}
430fn spawn_delete(storage: Arc<dyn ApiKeyStorage>, index: String) -> StorageTask<()> {
431    start_storage_task(async move { storage.delete(&index).await })
432}
433async fn storage_results<T: Send + 'static>(operations: Vec<StorageTask<T>>) -> AuthResult<Vec<T>> {
434    use futures_util::StreamExt as _;
435    // Source Promise.all starts every IO, then rejects on the first failure;
436    // the other initiated operations continue. First-poll acknowledgements
437    // preserve initiation, and dropped JoinHandles detach instead of cancel.
438    let mut results: Vec<Option<T>> = std::iter::repeat_with(|| None)
439        .take(operations.len())
440        .collect();
441    let pending = futures_util::stream::FuturesUnordered::new();
442    for (index, (started, handle)) in operations.into_iter().enumerate() {
443        started
444            .await
445            .map_err(|error| AuthError::internal(error.to_string()))?;
446        pending.push(async move {
447            handle
448                .await
449                .map_err(|error| AuthError::internal(error.to_string()))?
450                .map(|value| (index, value))
451        });
452    }
453    let mut pending = pending;
454    while let Some(result) = pending.next().await {
455        let (index, value) = result?;
456        *results
457            .get_mut(index)
458            .ok_or_else(|| AuthError::internal("Invalid storage result index"))? = Some(value);
459    }
460    Ok(results.into_iter().flatten().collect())
461}
462async fn storage_map<T, R, F, Fut>(items: Vec<T>, mapper: F) -> AuthResult<Vec<R>>
463where
464    T: Send + 'static,
465    R: Send + 'static,
466    F: Fn(T) -> Fut + Send + Sync + 'static,
467    Fut: std::future::Future<Output = AuthResult<R>> + Send + 'static,
468{
469    use futures_util::StreamExt as _;
470    let mut results: Vec<Option<R>> = std::iter::repeat_with(|| None).take(items.len()).collect();
471    let mut items = items.into_iter().enumerate();
472    let mapper = Arc::new(mapper);
473    let failed = Arc::new(std::sync::atomic::AtomicBool::new(false));
474    let launch = |(index, item)| {
475        let mapper = Arc::clone(&mapper);
476        let failed = Arc::clone(&failed);
477        start_storage_task(async move {
478            match mapper(item).await {
479                Ok(value) => Ok((index, value)),
480                Err(error) => {
481                    failed.store(true, std::sync::atomic::Ordering::SeqCst);
482                    Err(error)
483                }
484            }
485        })
486    };
487    let mut pending = futures_util::stream::FuturesUnordered::new();
488    let initial: Vec<_> = items.by_ref().take(10).map(&launch).collect();
489    for (started, handle) in initial {
490        started
491            .await
492            .map_err(|error| AuthError::internal(error.to_string()))?;
493        pending.push(handle);
494    }
495    while let Some(result) = pending.next().await {
496        let (index, value) = result.map_err(|error| AuthError::internal(error.to_string()))??;
497        *results
498            .get_mut(index)
499            .ok_or_else(|| AuthError::internal("Invalid storage result index"))? = Some(value);
500        if !failed.load(std::sync::atomic::Ordering::SeqCst)
501            && let Some(item) = items.next()
502        {
503            let (started, handle) = launch(item);
504            started
505                .await
506                .map_err(|error| AuthError::internal(error.to_string()))?;
507            pending.push(handle);
508        }
509    }
510    Ok(results.into_iter().flatten().collect())
511}
512async fn reference_ids(storage: &dyn ApiKeyStorage, reference: &str) -> AuthResult<Vec<String>> {
513    Ok(storage
514        .get(&ref_index(reference))
515        .await?
516        .and_then(|data| serde_json::from_str(&data).ok())
517        .unwrap_or_default())
518}
519async fn modify_reference(
520    storage: &dyn ApiKeyStorage,
521    reference: &str,
522    id: &str,
523    add: bool,
524) -> AuthResult<()> {
525    static LOCKS: OnceLock<
526        tokio::sync::Mutex<HashMap<String, std::sync::Weak<tokio::sync::Mutex<()>>>>,
527    > = OnceLock::new();
528    let index = ref_index(reference);
529    let lock = {
530        let mut locks = LOCKS.get_or_init(Default::default).lock().await;
531        locks.retain(|_, lock| lock.strong_count() > 0);
532        if let Some(lock) = locks.get(&index).and_then(std::sync::Weak::upgrade) {
533            lock
534        } else {
535            let lock = Arc::new(tokio::sync::Mutex::new(()));
536            _ = locks.insert(index.clone(), Arc::downgrade(&lock));
537            lock
538        }
539    };
540    let _guard = lock.lock().await;
541    let mut ids = reference_ids(storage, reference).await?;
542    if add {
543        if !ids.iter().any(|value| value == id) {
544            ids.push(id.to_owned());
545        }
546    } else {
547        ids.retain(|value| value != id);
548    }
549    if ids.is_empty() {
550        storage.delete(&index).await
551    } else {
552        storage
553            .set(&index, &serde_json::to_string(&ids)?, None)
554            .await
555    }
556}
557
558pub(super) async fn remove_storage(
559    storage: Arc<dyn ApiKeyStorage>,
560    key: &ApiKey,
561    fallback: bool,
562) -> AuthResult<()> {
563    let hash = hash_index(&key.key_hash);
564    let id = id_index(&key.id);
565    let reference = ref_index(&key.reference_id);
566    let mut operations = vec![
567        spawn_delete(Arc::clone(&storage), hash),
568        spawn_delete(Arc::clone(&storage), id),
569    ];
570    if fallback {
571        operations.push(spawn_delete(storage, reference));
572    } else {
573        let reference = key.reference_id.clone();
574        let id = key.id.clone();
575        operations.push(start_storage_task(async move {
576            modify_reference(storage.as_ref(), &reference, &id, false).await
577        }));
578    }
579    storage_results(operations).await.map(|_| ())
580}