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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
14pub enum ApiKeyStorageMode {
15 #[default]
16 Database,
17 SecondaryStorage,
18}
19
20#[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 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}