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 _ = 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 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}