1mod cleanup;
2mod config;
3
4pub use config::ApiKeyConfig;
5pub use config::ApiKeyReferences;
6pub use config::KeyExpirationConfig;
7pub use config::RateLimitDefaults;
8mod callbacks;
9mod secondary_usage;
10mod storage;
11pub use storage::{ApiKeyStorage, ApiKeyStorageMode};
12mod endpoint;
13pub use endpoint::{
14 ApiKeyVerificationInput, ApiKeyVerificationOutput, DeleteKeyResponse, GetKeyQuery,
15};
16
17pub(super) mod handlers;
18
19pub(super) mod types;
20
21mod verification;
22
23use alibi_core::entity::AuthUser;
24use alibi_core::{AuthContext, AuthError, AuthResult, BeforeRequestAction};
25use alibi_core::{AuthRequest, AuthResponse};
26use base64::Engine;
27use base64::engine::general_purpose::URL_SAFE_NO_PAD;
28pub use callbacks::{
29 ApiKeyCallbackContext, ApiKeyDefaultPermissions, ApiKeyGenerationOptions, ApiKeyGenerator,
30 ApiKeyGetter, ApiKeyPermissions, ApiKeyValidator,
31};
32use handlers::{create_key_core, delete_key_core, get_key_core, list_keys_core, update_key_core};
33use rand::seq::IndexedRandom;
34use sha2::{Digest, Sha256};
35use std::sync::{Arc, Mutex};
36use types::parse_api_key_body;
37pub use types::{
38 CreateKeyRequest, CreateKeyResponse, DeleteExpiredApiKeysResponse, DeleteKeyRequest,
39 ListKeysQuery, ListKeysResponse, UpdateKeyRequest,
40};
41pub use verification::{
42 ApiKeyErrorDetails, ApiKeyErrorMessage, ApiKeyValidationError, ApiKeyVerificationError,
43 VerifyApiKey,
44};
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum ApiKeyErrorCode {
49 InvalidApiKey,
50 KeyDisabled,
51 KeyExpired,
52 UsageExceeded,
53 KeyNotFound,
54 RateLimited,
55 UnauthorizedSession,
56 InvalidPrefixLength,
57 InvalidNameLength,
58 MetadataDisabled,
59 NoValuesToUpdate,
60 KeyDisabledExpiration,
61 ExpiresInTooSmall,
62 ExpiresInTooLarge,
63 InvalidRemaining,
64 RefillAmountAndIntervalRequired,
65 RefillIntervalAndAmountRequired,
66 NameRequired,
67 InvalidUserIdFromApiKey,
68 InvalidReferenceIdFromApiKey,
69 NoDefaultConfiguration,
70 OrganizationIdRequired,
71 OrganizationPluginRequired,
72 UserNotMemberOfOrganization,
73 InsufficientApiKeyPermissions,
74 ServerOnlyProperty,
75 FailedToUpdateApiKey,
76 InvalidMetadataType,
77}
78
79impl ApiKeyErrorCode {
80 #[must_use]
81 pub const fn as_str(self) -> &'static str {
82 match self {
83 Self::InvalidApiKey => "INVALID_API_KEY",
84 Self::KeyDisabled => "KEY_DISABLED",
85 Self::KeyExpired => "KEY_EXPIRED",
86 Self::UsageExceeded => "USAGE_EXCEEDED",
87 Self::KeyNotFound => "KEY_NOT_FOUND",
88 Self::RateLimited => "RATE_LIMITED",
89 Self::UnauthorizedSession => "UNAUTHORIZED_SESSION",
90 Self::InvalidPrefixLength => "INVALID_PREFIX_LENGTH",
91 Self::InvalidNameLength => "INVALID_NAME_LENGTH",
92 Self::MetadataDisabled => "METADATA_DISABLED",
93 Self::NoValuesToUpdate => "NO_VALUES_TO_UPDATE",
94 Self::KeyDisabledExpiration => "KEY_DISABLED_EXPIRATION",
95 Self::ExpiresInTooSmall => "EXPIRES_IN_IS_TOO_SMALL",
96 Self::ExpiresInTooLarge => "EXPIRES_IN_IS_TOO_LARGE",
97 Self::InvalidRemaining => "INVALID_REMAINING",
98 Self::RefillAmountAndIntervalRequired => "REFILL_AMOUNT_AND_INTERVAL_REQUIRED",
99 Self::RefillIntervalAndAmountRequired => "REFILL_INTERVAL_AND_AMOUNT_REQUIRED",
100 Self::NameRequired => "NAME_REQUIRED",
101 Self::InvalidUserIdFromApiKey => "INVALID_USER_ID_FROM_API_KEY",
102 Self::InvalidReferenceIdFromApiKey => "INVALID_REFERENCE_ID_FROM_API_KEY",
103 Self::NoDefaultConfiguration => "NO_DEFAULT_API_KEY_CONFIGURATION_FOUND",
104 Self::OrganizationIdRequired => "ORGANIZATION_ID_REQUIRED",
105 Self::OrganizationPluginRequired => "ORGANIZATION_PLUGIN_REQUIRED",
106 Self::UserNotMemberOfOrganization => "USER_NOT_MEMBER_OF_ORGANIZATION",
107 Self::InsufficientApiKeyPermissions => "INSUFFICIENT_API_KEY_PERMISSIONS",
108 Self::ServerOnlyProperty => "SERVER_ONLY_PROPERTY",
109 Self::FailedToUpdateApiKey => "FAILED_TO_UPDATE_API_KEY",
110 Self::InvalidMetadataType => "INVALID_METADATA_TYPE",
111 }
112 }
113
114 #[must_use]
115 pub const fn message(self) -> &'static str {
116 match self {
117 Self::InvalidApiKey => "Invalid API key.",
118 Self::KeyDisabled => "API Key is disabled",
119 Self::KeyExpired => "API Key has expired",
120 Self::UsageExceeded => "API Key has reached its usage limit",
121 Self::KeyNotFound => "API Key not found",
122 Self::RateLimited => "Rate limit exceeded.",
123 Self::UnauthorizedSession => "Unauthorized or invalid session",
124 Self::InvalidPrefixLength => "The prefix length is either too large or too small.",
125 Self::InvalidNameLength => "The name length is either too large or too small.",
126 Self::MetadataDisabled => "Metadata is disabled.",
127 Self::NoValuesToUpdate => "No values to update.",
128 Self::KeyDisabledExpiration => "Custom key expiration values are disabled.",
129 Self::ExpiresInTooSmall => {
130 "The expiresIn is smaller than the predefined minimum value."
131 }
132 Self::ExpiresInTooLarge => "The expiresIn is larger than the predefined maximum value.",
133 Self::InvalidRemaining => "The remaining count is either too large or too small.",
134 Self::RefillAmountAndIntervalRequired => {
135 "refillAmount is required when refillInterval is provided"
136 }
137 Self::RefillIntervalAndAmountRequired => {
138 "refillInterval is required when refillAmount is provided"
139 }
140 Self::NameRequired => "API Key name is required.",
141 Self::InvalidUserIdFromApiKey => "The user id from the API key is invalid.",
142 Self::InvalidReferenceIdFromApiKey => "The reference id from the API key is invalid.",
143 Self::NoDefaultConfiguration => "No default api-key configuration found.",
144 Self::OrganizationIdRequired => {
145 "Organization ID is required for organization-owned API keys."
146 }
147 Self::OrganizationPluginRequired => {
148 "Organization plugin is required for organization-owned API keys. Please install and configure the organization plugin."
149 }
150 Self::UserNotMemberOfOrganization => {
151 "You are not a member of the organization that owns this API key."
152 }
153 Self::InsufficientApiKeyPermissions => {
154 "You do not have permission to perform this action on organization API keys."
155 }
156 Self::ServerOnlyProperty => {
157 "The property you're trying to set can only be set from the server auth instance only."
158 }
159 Self::FailedToUpdateApiKey => "Failed to update API key",
160 Self::InvalidMetadataType => "metadata must be an object or undefined",
161 }
162 }
163}
164
165impl serde::Serialize for ApiKeyErrorCode {
166 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
167 serializer.serialize_str(self.as_str())
168 }
169}
170
171static LAST_EXPIRED_CHECK: Mutex<Option<i64>> = Mutex::new(None);
173
174#[derive(Clone)]
176pub struct ApiKeyPlugin {
177 pub(super) configurations: Vec<ApiKeyConfig>,
179}
180
181impl ApiKeyPlugin {
182 #[must_use]
187 pub fn configuration(mut self, config: ApiKeyConfig) -> Self {
188 self.configurations.push(config.normalized());
189 self
190 }
191
192 pub(super) fn resolve_configuration(
196 &self,
197 config_id: Option<&str>,
198 ) -> AuthResult<&ApiKeyConfig> {
199 if let Some(config_id) = config_id
200 && let Some(found) = self
201 .configurations
202 .iter()
203 .find(|config| config.config_id == config_id)
204 {
205 return Ok(found);
206 }
207
208 self.configurations
209 .iter()
210 .find(|config| is_default_config_id(&config.config_id))
211 .ok_or_else(|| api_key_error(ApiKeyErrorCode::NoDefaultConfiguration))
212 }
213}
214
215#[bon::bon]
228impl ApiKeyPlugin {
229 #[builder]
230 #[must_use]
231 pub fn new(
232 #[builder(default = "default".to_owned())] config_id: String,
233 #[builder(default)] references: ApiKeyReferences,
234 #[builder(default)] storage: ApiKeyStorageMode,
235 #[builder(default)] fallback_to_database: bool,
236 custom_storage: Option<Arc<dyn ApiKeyStorage>>,
237 secondary_storage: Option<Arc<dyn alibi_core::store::CacheAdapter>>,
238 #[builder(default = 64.0, into)] key_length: f64,
239 prefix: Option<String>,
240 default_permissions: Option<ApiKeyPermissions>,
241 custom_key_generator: Option<Arc<dyn ApiKeyGenerator>>,
242 default_permissions_callback: Option<Arc<dyn ApiKeyDefaultPermissions>>,
243 #[builder(default = vec!["x-api-key".to_owned()])] api_key_headers: Vec<String>,
244 custom_api_key_getter: Option<Arc<dyn ApiKeyGetter>>,
245 custom_api_key_validator: Option<Arc<dyn ApiKeyValidator>>,
246 #[builder(default = false)] disable_key_hashing: bool,
247 #[builder(default = 6.0, into)] starting_characters_length: f64,
248 #[builder(default = true)] store_starting_characters: bool,
249 #[builder(default = 32.0, into)] max_prefix_length: f64,
250 #[builder(default = 1.0, into)] min_prefix_length: f64,
251 #[builder(default = 32.0, into)] max_name_length: f64,
252 #[builder(default = 1.0, into)] min_name_length: f64,
253 #[builder(default = false)] require_name: bool,
254 #[builder(default = false)] enable_metadata: bool,
255 #[builder(default)] key_expiration: KeyExpirationConfig,
256 #[builder(default)] rate_limit: RateLimitDefaults,
257 #[builder(default = false)] enable_session_for_api_keys: bool,
258 #[builder(default = false)] defer_updates: bool,
259 ) -> Self {
260 Self {
261 configurations: vec![
262 ApiKeyConfig {
263 config_id,
264 references,
265 storage,
266 fallback_to_database,
267 secondary_storage,
268 custom_storage,
269 key_length,
270 prefix,
271 default_permissions,
272 custom_key_generator,
273 default_permissions_callback,
274 api_key_headers,
275 custom_api_key_getter,
276 custom_api_key_validator,
277 disable_key_hashing,
278 starting_characters_length,
279 store_starting_characters,
280 max_prefix_length,
281 min_prefix_length,
282 max_name_length,
283 min_name_length,
284 require_name,
285 enable_metadata,
286 key_expiration,
287 rate_limit,
288 enable_session_for_api_keys,
289 defer_updates,
290 }
291 .normalized(),
292 ],
293 }
294 }
295
296 #[must_use]
297 pub fn with_config(config: ApiKeyConfig) -> Self {
298 Self {
299 configurations: vec![config.normalized()],
300 }
301 }
302
303 pub(super) fn generate_key(
304 config: &ApiKeyConfig,
305 custom_prefix: Option<&str>,
306 ) -> AuthResult<(String, String, alibi_core::ApiKeyStartingCharacters)> {
307 const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
309 if config.key_length <= 0.0 {
310 return Err(AuthError::internal("Length must be a positive integer."));
311 }
312 if !config.key_length.is_finite() || config.key_length < 0.5 {
315 return Err(AuthError::internal("Unsupported random key length"));
316 }
317 let capacity = config
318 .key_length
319 .ceil()
320 .to_string()
321 .parse::<u32>()
322 .map_err(|error| AuthError::internal(error.to_string()))?;
323 let mut rng = rand::rng();
324 let mut raw = String::new();
325 raw.try_reserve_exact(
326 usize::try_from(capacity).map_err(|error| AuthError::internal(error.to_string()))?,
327 )
328 .map_err(|error| AuthError::internal(error.to_string()))?;
329 while f64::from(
330 u32::try_from(raw.len()).map_err(|error| AuthError::internal(error.to_string()))?,
331 ) < config.key_length
332 {
333 raw.push(ALPHABET.choose(&mut rng).copied().map_or('a', char::from));
334 }
335
336 let prefix = custom_prefix
337 .filter(|prefix| !prefix.is_empty())
338 .or(config.prefix.as_deref())
339 .unwrap_or("");
340 let full_key = format!("{prefix}{raw}");
341
342 let start = Self::starting_characters(&full_key, config.starting_characters_length);
345
346 let hash = if config.disable_key_hashing {
347 full_key.clone()
348 } else {
349 Self::hash_key(&full_key)
350 };
351
352 Ok((full_key, hash, start))
353 }
354
355 pub(super) fn starting_characters(
356 key: &str,
357 length: f64,
358 ) -> alibi_core::ApiKeyStartingCharacters {
359 let end = if length.is_nan() || length < 0.0 {
362 0.0
363 } else {
364 length.trunc()
365 };
366 let units = key
367 .encode_utf16()
368 .scan(0_u32, |position, unit| {
369 let keep = f64::from(*position) < end;
370 *position = position.saturating_add(1);
371 keep.then_some(unit)
372 })
373 .collect();
374 alibi_core::ApiKeyStartingCharacters::from_utf16(units)
375 }
376
377 pub(super) fn hash_key(key: &str) -> String {
378 let mut hasher = Sha256::new();
379 hasher.update(key.as_bytes());
380 let digest = hasher.finalize();
381 URL_SAFE_NO_PAD.encode(digest)
382 }
383
384 pub(super) fn validate_prefix(config: &ApiKeyConfig, prefix: Option<&str>) -> AuthResult<()> {
385 if let Some(p) = prefix.filter(|prefix| !prefix.is_empty()) {
386 let len = f64::from(
387 u32::try_from(p.encode_utf16().count())
388 .map_err(|error| AuthError::internal(error.to_string()))?,
389 );
390 if len < config.min_prefix_length || len > config.max_prefix_length {
391 return Err(api_key_error(ApiKeyErrorCode::InvalidPrefixLength));
392 }
393 }
394 Ok(())
395 }
396
397 pub(super) fn validate_name(
403 config: &ApiKeyConfig,
404 name: Option<&str>,
405 is_create: bool,
406 ) -> AuthResult<()> {
407 if is_create && config.require_name && name.is_none_or(str::is_empty) {
408 return Err(api_key_error(ApiKeyErrorCode::NameRequired));
409 }
410 if let Some(n) = name.filter(|name| !is_create || !name.is_empty()) {
411 let len = f64::from(
412 u32::try_from(n.encode_utf16().count())
413 .map_err(|error| AuthError::internal(error.to_string()))?,
414 );
415 if len < config.min_name_length || len > config.max_name_length {
416 return Err(api_key_error(ApiKeyErrorCode::InvalidNameLength));
417 }
418 }
419 Ok(())
420 }
421
422 pub(super) fn validate_expires_in(
423 config: &ApiKeyConfig,
424 expires_in: Option<f64>,
425 ) -> AuthResult<Option<f64>> {
426 let cfg = &config.key_expiration;
427 if let Some(secs) = expires_in {
428 if cfg.disable_custom_expires_time {
429 return Err(api_key_error(ApiKeyErrorCode::KeyDisabledExpiration));
430 }
431 let days = secs / 86_400.0;
433 if days < cfg.min_expires_in {
434 return Err(api_key_error(ApiKeyErrorCode::ExpiresInTooSmall));
435 }
436 if days > cfg.max_expires_in {
437 return Err(api_key_error(ApiKeyErrorCode::ExpiresInTooLarge));
438 }
439 Ok(Some(secs))
440 } else {
441 Ok(cfg.default_expires_in)
442 }
443 }
444
445 pub(super) fn validate_metadata(
446 config: &ApiKeyConfig,
447 metadata: Option<&alibi_core::utils::json::JsValue>,
448 ) -> AuthResult<()> {
449 if let Some(value) = metadata.filter(|value| match value {
450 alibi_core::utils::json::JsValue::Null => false,
451 alibi_core::utils::json::JsValue::Bool(value) => *value,
452 alibi_core::utils::json::JsValue::Number(value) => *value != 0.0 && !value.is_nan(),
453 alibi_core::utils::json::JsValue::String(value) => !value.is_empty(),
454 alibi_core::utils::json::JsValue::Array(_)
455 | alibi_core::utils::json::JsValue::Object(_) => true,
456 }) {
457 if !config.enable_metadata {
458 return Err(api_key_error(ApiKeyErrorCode::MetadataDisabled));
459 }
460 if !value.is_object() && !value.is_array() {
461 return Err(api_key_error(ApiKeyErrorCode::InvalidMetadataType));
462 }
463 }
464 Ok(())
465 }
466
467 pub(super) fn validate_refill(
468 refill_interval: Option<f64>,
469 refill_amount: Option<f64>,
470 ) -> AuthResult<()> {
471 match (
472 refill_interval.filter(|value| *value != 0.0),
473 refill_amount.filter(|value| *value != 0.0),
474 ) {
475 (None, Some(_)) => Err(api_key_error(
476 ApiKeyErrorCode::RefillAmountAndIntervalRequired,
477 )),
478 (Some(_), None) => Err(api_key_error(
479 ApiKeyErrorCode::RefillIntervalAndAmountRequired,
480 )),
481 _ => Ok(()),
482 }
483 }
484
485 async fn handle_create(
486 &self,
487 req: &AuthRequest,
488 ctx: &AuthContext<impl alibi_core::AuthSchema>,
489 ) -> AuthResult<AuthResponse> {
490 let mut resolution = req.clone();
491 _ = resolution
492 .query
493 .insert("disableCookieCache".into(), "true".into());
494 let (user, _session) = ctx
495 .require_cached_session(&resolution)
496 .await
497 .map_err(|error| match error {
498 AuthError::Unauthenticated => api_key_error(ApiKeyErrorCode::UnauthorizedSession),
499 error @ (AuthError::Api { .. }
500 | AuthError::Upstream { .. }
501 | AuthError::BadRequest(_)
502 | AuthError::InvalidRequest(_)
503 | AuthError::Validation(_)
504 | AuthError::InvalidCredentials
505 | AuthError::AuthenticationFailed(_)
506 | AuthError::SessionNotFound
507 | AuthError::Forbidden(_)
508 | AuthError::UserCreationCancelled
509 | AuthError::SessionCreationCancelled
510 | AuthError::BannedUser(_)
511 | AuthError::Unauthorized
512 | AuthError::UserNotFound
513 | AuthError::NotFound(_)
514 | AuthError::Conflict(_)
515 | AuthError::MethodNotAllowed(_)
516 | AuthError::PayloadTooLarge(_)
517 | AuthError::UnprocessableEntity(_)
518 | AuthError::RateLimited { .. }
519 | AuthError::NotImplemented(_)
520 | AuthError::Config(_)
521 | AuthError::Database(_)
522 | AuthError::Serialization(_)
523 | AuthError::Plugin { .. }
524 | AuthError::CallbackFailure(_)
525 | AuthError::Internal(_)
526 | AuthError::Encryption(_)
527 | AuthError::PasswordHash(_)
528 | AuthError::Jwt(_)) => error,
529 })?;
530 let body: CreateKeyRequest = match parse_api_key_body(req) {
531 Ok(v) => v,
532 Err(resp) => return Ok(resp),
533 };
534 let response = match create_key_core(&body, user.id(), self, ctx, Some(req)).await {
535 Ok(response) => response,
536 Err(error)
537 if error.status_code() >= 500
538 && !matches!(error, AuthError::Upstream { .. } | AuthError::Api { .. }) =>
539 {
540 return Ok(AuthResponse::new(500));
541 }
542 Err(error) => return Err(error),
543 };
544 Ok(AuthResponse::json(200, &response)?)
545 }
546
547 async fn handle_get(
548 &self,
549 req: &AuthRequest,
550 ctx: &AuthContext<impl alibi_core::AuthSchema>,
551 ) -> AuthResult<AuthResponse> {
552 let mut resolution = req.clone();
553 _ = resolution.query.remove("disableCookieCache");
554 _ = resolution.query.remove("disableRefresh");
555 let (user, _session) = super::helpers::ordinary_session(&resolution, ctx).await?;
556 let id = req
557 .query
558 .get("id")
559 .ok_or_else(|| AuthError::bad_request("Query parameter 'id' is required"))?;
560 let config_id = req.query.get("configId").map(String::as_str);
561 key_json_response(get_key_core(id, config_id, user.id(), self, ctx).await)
562 }
563
564 async fn handle_list(
565 &self,
566 req: &AuthRequest,
567 ctx: &AuthContext<impl alibi_core::AuthSchema>,
568 ) -> AuthResult<AuthResponse> {
569 let mut resolution = req.clone();
570 _ = resolution.query.remove("disableCookieCache");
571 _ = resolution.query.remove("disableRefresh");
572 let (user, _session) = super::helpers::ordinary_session(&resolution, ctx).await?;
573 let query = match ListKeysQuery::from_request(req) {
574 Ok(query) => query,
575 Err(response) => return Ok(response),
576 };
577 key_json_response(list_keys_core(user.id(), &query, self, ctx).await)
578 }
579
580 async fn handle_update(
581 &self,
582 req: &AuthRequest,
583 ctx: &AuthContext<impl alibi_core::AuthSchema>,
584 ) -> AuthResult<AuthResponse> {
585 let mut resolution = req.clone();
586 _ = resolution
587 .query
588 .insert("disableCookieCache".into(), "true".into());
589 let (user, _session) = ctx
590 .require_cached_session(&resolution)
591 .await
592 .map_err(|error| match error {
593 AuthError::Unauthenticated => api_key_error(ApiKeyErrorCode::UnauthorizedSession),
594 error @ (AuthError::Api { .. }
595 | AuthError::Upstream { .. }
596 | AuthError::BadRequest(_)
597 | AuthError::InvalidRequest(_)
598 | AuthError::Validation(_)
599 | AuthError::InvalidCredentials
600 | AuthError::AuthenticationFailed(_)
601 | AuthError::SessionNotFound
602 | AuthError::Forbidden(_)
603 | AuthError::UserCreationCancelled
604 | AuthError::SessionCreationCancelled
605 | AuthError::BannedUser(_)
606 | AuthError::Unauthorized
607 | AuthError::UserNotFound
608 | AuthError::NotFound(_)
609 | AuthError::Conflict(_)
610 | AuthError::MethodNotAllowed(_)
611 | AuthError::PayloadTooLarge(_)
612 | AuthError::UnprocessableEntity(_)
613 | AuthError::RateLimited { .. }
614 | AuthError::NotImplemented(_)
615 | AuthError::Config(_)
616 | AuthError::Database(_)
617 | AuthError::Serialization(_)
618 | AuthError::Plugin { .. }
619 | AuthError::CallbackFailure(_)
620 | AuthError::Internal(_)
621 | AuthError::Encryption(_)
622 | AuthError::PasswordHash(_)
623 | AuthError::Jwt(_)) => error,
624 })?;
625 let body: UpdateKeyRequest = match parse_api_key_body(req) {
626 Ok(v) => v,
627 Err(resp) => return Ok(resp),
628 };
629 let response = update_key_core(&body, user.id(), self, ctx).await?;
630 Ok(AuthResponse::json(200, &response)?)
631 }
632
633 async fn handle_delete(
634 &self,
635 req: &AuthRequest,
636 ctx: &AuthContext<impl alibi_core::AuthSchema>,
637 ) -> AuthResult<AuthResponse> {
638 let (user, _session) = super::helpers::ordinary_session(req, ctx).await?;
639 if user.banned() {
640 return Err(AuthError::authentication_failed("User is banned"));
641 }
642 let body: DeleteKeyRequest = match parse_api_key_body(req) {
643 Ok(v) => v,
644 Err(resp) => return Ok(resp),
645 };
646 let response = delete_key_core(&body, user.id(), self, ctx).await?;
647 Ok(AuthResponse::json(200, &response)?)
648 }
649}
650
651fn key_json_response<T: serde::Serialize>(result: AuthResult<T>) -> AuthResult<AuthResponse> {
652 match result {
653 Ok(response) => Ok(AuthResponse::json(200, &response)?),
654 Err(error)
655 if error.status_code() >= 500
656 && !matches!(error, AuthError::Upstream { .. } | AuthError::Api { .. }) =>
657 {
658 Ok(AuthResponse::new(500))
659 }
660 Err(error) => Err(error),
661 }
662}
663
664alibi_core::impl_auth_plugin! {
665 ApiKeyPlugin, "api-key";
666 routes {
667 post "/api-key/create" => handle_create, "api_key_create";
668 get "/api-key/get" => handle_get, "api_key_get";
669 post "/api-key/update" => handle_update, "api_key_update";
670 post "/api-key/delete" => handle_delete, "api_key_delete";
671 get "/api-key/list" => handle_list, "api_key_list";
672 }
673 extra {
674 fn static_openapi_metadata(&self) -> alibi_core::PluginOpenApiMetadata {
675 crate::metadata::plugin_metadata(<Self as alibi_core::AuthPlugin<S>>::name(self), &<Self as alibi_core::AuthPlugin<S>>::routes(self))
676 }
677
678 fn openapi_metadata(&self,ctx:&alibi_core::AuthInitContext<S>)->alibi_core::PluginOpenApiMetadata {
679 let mut metadata=crate::metadata::instance_plugin_metadata("api-key",&<Self as alibi_core::AuthPlugin<S>>::routes(self),ctx);
680 let defaults=self.configurations.first().filter(|_|self.configurations.len()==1).map(|config|config.rate_limit.clone()).unwrap_or_default();
681 for model in &mut metadata.models {
682 if model.name!="Apikey" {continue;}
683 for field in &mut model.fields {
684 let value=match field.name.as_str() {"rateLimitTimeWindow"=>defaults.time_window,"rateLimitMax"=>defaults.max_requests,_=>continue};
685 if let Some(schema)=field.schema.as_object_mut() {_ = schema.insert("default".into(),serde_json::json!(value));}
686 }
687 }
688 metadata
689 }
690 async fn on_init(&self, _ctx: &mut alibi_core::AuthInitContext<S>) -> AuthResult<()> {
691 if self.configurations.len() > 1 {
692 let mut ids = std::collections::HashSet::new();
693 for config in &self.configurations {
694 if config.config_id.is_empty() {
695 return Err(AuthError::config("configId is required for each API key configuration in the api-key plugin."));
696 }
697 if !ids.insert(&config.config_id) {
698 return Err(AuthError::config("configId must be unique for each API key configuration in the api-key plugin."));
699 }
700 }
701 }
702 Ok(())
703 }
704
705 fn server_endpoints(&self) -> Vec<alibi_core::endpoint::EndpointDefinition> { endpoint::definitions() }
706
707 fn endpoint_hooks(&self) -> Vec<&dyn alibi_core::endpoint::EndpointHook<S>> { vec![self] }
708
709 fn validate_endpoint(&self, call: &alibi_core::endpoint::EndpointCall, _ctx: &AuthContext<S>) -> AuthResult<alibi_core::endpoint::EndpointInput> { endpoint::validate(call) }
710
711 async fn on_endpoint(&self, call: &alibi_core::endpoint::EndpointCall, ctx: &AuthContext<S>) -> AuthResult<alibi_core::endpoint::EndpointResponse> { self.call_endpoint(call, ctx).await }
712
713 async fn before_request(
714 &self,
715 req: &AuthRequest,
716 ctx: &AuthContext<S>,
717 ) -> AuthResult<Option<BeforeRequestAction>> {
718 self.api_key_session(req, ctx).await
719 }
720 }
721}
722
723impl std::fmt::Debug for ApiKeyPlugin {
724 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
725 f.debug_struct("ApiKeyPlugin").finish_non_exhaustive()
726 }
727}
728
729pub(super) const fn api_key_error(code: ApiKeyErrorCode) -> AuthError {
730 let status = match code {
731 ApiKeyErrorCode::UnauthorizedSession => 401,
732 ApiKeyErrorCode::OrganizationPluginRequired => 500,
733 ApiKeyErrorCode::UserNotMemberOfOrganization
734 | ApiKeyErrorCode::InsufficientApiKeyPermissions => 403,
735 ApiKeyErrorCode::InvalidApiKey
736 | ApiKeyErrorCode::KeyDisabled
737 | ApiKeyErrorCode::KeyExpired
738 | ApiKeyErrorCode::UsageExceeded
739 | ApiKeyErrorCode::KeyNotFound
740 | ApiKeyErrorCode::RateLimited
741 | ApiKeyErrorCode::InvalidPrefixLength
742 | ApiKeyErrorCode::InvalidNameLength
743 | ApiKeyErrorCode::MetadataDisabled
744 | ApiKeyErrorCode::NoValuesToUpdate
745 | ApiKeyErrorCode::KeyDisabledExpiration
746 | ApiKeyErrorCode::ExpiresInTooSmall
747 | ApiKeyErrorCode::ExpiresInTooLarge
748 | ApiKeyErrorCode::InvalidRemaining
749 | ApiKeyErrorCode::RefillAmountAndIntervalRequired
750 | ApiKeyErrorCode::RefillIntervalAndAmountRequired
751 | ApiKeyErrorCode::NameRequired
752 | ApiKeyErrorCode::InvalidUserIdFromApiKey
753 | ApiKeyErrorCode::InvalidReferenceIdFromApiKey
754 | ApiKeyErrorCode::NoDefaultConfiguration
755 | ApiKeyErrorCode::OrganizationIdRequired
756 | ApiKeyErrorCode::ServerOnlyProperty
757 | ApiKeyErrorCode::FailedToUpdateApiKey
758 | ApiKeyErrorCode::InvalidMetadataType => 400,
759 };
760 AuthError::Upstream {
761 status,
762 code: code.as_str(),
763 message: code.message(),
764 }
765}
766
767fn admit_expired_cleanup(bypass: bool) -> bool {
768 let mut last = LAST_EXPIRED_CHECK
769 .lock()
770 .unwrap_or_else(std::sync::PoisonError::into_inner);
771 let now = chrono::Utc::now().timestamp_millis();
772 if !bypass && last.is_some_and(|previous| now.saturating_sub(previous) < 10_000) {
773 return false;
774 }
775 *last = Some(now);
776 true
777}
778
779pub(super) fn is_default_config_id(config_id: &str) -> bool {
782 config_id.is_empty() || config_id == "default"
783}
784
785pub(super) fn config_id_matches(key_config_id: &str, expected: &str) -> bool {
787 if is_default_config_id(key_config_id) && is_default_config_id(expected) {
788 return true;
789 }
790 key_config_id == expected
791}
792
793#[cfg(test)]
795mod tests {
796
797 use super::*;
798 use alibi_core::wire::{SessionView, UserView};
799 use alibi_core::{
800 AuthContext, AuthPlugin, CreateSession, CreateUser, HttpMethod, UpdateApiKey,
801 };
802 use chrono::{Duration, Utc};
803 use std::collections::HashMap;
804 use std::sync::Arc;
805
806 type TestSchema = alibi_seaorm::store::__private_test_support::bundled_schema::BundledSchema;
807
808 async fn create_test_context_with_user() -> (AuthContext<TestSchema>, UserView, SessionView) {
809 let config = Arc::new(alibi_core::AuthConfig::new(
810 "test-secret-key-at-least-32-chars-long",
811 ));
812 let database = crate::test_helpers::create_test_database().await;
813 let ctx = AuthContext::new(config, Arc::clone(&database));
814
815 let user = database
816 .create_user(
817 CreateUser::new()
818 .with_email("test@example.com")
819 .with_name("Test User"),
820 )
821 .await
822 .unwrap();
823 let wire_user = UserView::from(&user);
824
825 let session = database
826 .create_session(CreateSession {
827 additional_fields: alibi_core::field_policy::FieldValues::default(),
828 token: None,
829 active_team_id: None,
830 user_id: user.id().to_string(),
831 expires_at: Utc::now() + Duration::hours(24),
832 ip_address: Some("127.0.0.1".to_owned()),
833 user_agent: Some("test-agent".to_owned()),
834 impersonated_by: None,
835 active_organization_id: None,
836 })
837 .await
838 .unwrap();
839 let wire_session = SessionView::from(&session);
840
841 (ctx, wire_user, wire_session)
842 }
843
844 async fn create_user_with_session(
845 ctx: &AuthContext<impl alibi_core::AuthSchema>,
846 email: &str,
847 ) -> (UserView, SessionView) {
848 let user = ctx
849 .database
850 .create_user(
851 CreateUser::new()
852 .with_email(email.to_owned())
853 .with_name("Another User"),
854 )
855 .await
856 .unwrap();
857 let wire_user = UserView::from(&user);
858
859 let session = ctx
860 .database
861 .create_session(CreateSession {
862 additional_fields: alibi_core::field_policy::FieldValues::default(),
863 token: None,
864 active_team_id: None,
865 user_id: user.id().to_string(),
866 expires_at: Utc::now() + Duration::hours(24),
867 ip_address: None,
868 user_agent: None,
869 impersonated_by: None,
870 active_organization_id: None,
871 })
872 .await
873 .unwrap();
874 let wire_session = SessionView::from(&session);
875
876 (wire_user, wire_session)
877 }
878
879 fn create_auth_request(
880 method: HttpMethod,
881 path: &str,
882 token: Option<&str>,
883 body: Option<serde_json::Value>,
884 query: Option<HashMap<String, String>>,
885 ) -> AuthRequest {
886 let mut headers = HashMap::new();
887 if let Some(token) = token {
888 headers.insert(
889 "cookie".to_owned(),
890 format!(
891 "better-auth.session_token={}",
892 alibi_core::utils::cookie_utils::sign_cookie_value(
893 token,
894 &crate::test_helpers::create_test_config().secret
895 )
896 ),
897 );
898 }
899
900 AuthRequest::from_parts(
901 method,
902 path.to_owned(),
903 headers,
904 body.map(|b| serde_json::to_vec(&b).unwrap()),
905 query.unwrap_or_default(),
906 )
907 }
908
909 fn json_body(response: &AuthResponse) -> serde_json::Value {
910 serde_json::from_slice(&response.body).unwrap()
911 }
912
913 async fn verify_key(
916 plugin: &ApiKeyPlugin,
917 ctx: &AuthContext<impl alibi_core::AuthSchema>,
918 raw_key: &str,
919 permissions: Option<&serde_json::Value>,
920 ) -> serde_json::Value {
921 match plugin
922 .verify_api_key(
923 &VerifyApiKey {
924 key: raw_key,
925 config_id: None,
926 permissions,
927 },
928 ctx,
929 )
930 .await
931 {
932 Ok(view) => serde_json::json!({ "valid": true, "error": null, "key": view }),
933 Err(ApiKeyVerificationError::Validation(error)) => serde_json::json!({
934 "valid": false,
935 "error": error,
936 "key": null,
937 }),
938 Err(
939 ApiKeyVerificationError::Internal(error)
940 | ApiKeyVerificationError::ExplicitValidator(error),
941 ) => panic!("Verification failed: {error}"),
942 }
943 }
944
945 async fn create_key_with_server_fields(
951 plugin: &ApiKeyPlugin,
952 ctx: &AuthContext<impl alibi_core::AuthSchema>,
953 token: &str,
954 client_body: serde_json::Value,
955 server_fields: UpdateApiKey,
956 ) -> (String, String) {
957 let (id, raw_key) = create_key_and_get_raw(plugin, ctx, token, client_body).await;
958 ctx.database
959 .update_api_key(&id, server_fields)
960 .await
961 .unwrap();
962 (id, raw_key)
963 }
964
965 async fn delete_all_expired(
967 ctx: &AuthContext<impl alibi_core::AuthSchema>,
968 ) -> serde_json::Value {
969 _ = ctx.database.delete_expired_api_keys().await.unwrap();
970 serde_json::json!({ "success": true, "error": null })
971 }
972
973 async fn create_key_and_get_id(
974 plugin: &ApiKeyPlugin,
975 ctx: &AuthContext<impl alibi_core::AuthSchema>,
976 token: &str,
977 name: &str,
978 ) -> String {
979 let req = create_auth_request(
980 HttpMethod::Post,
981 "/api-key/create",
982 Some(token),
983 Some(serde_json::json!({ "name": name })),
984 None,
985 );
986 let response = plugin.handle_create(&req, ctx).await.unwrap();
987 assert_eq!(response.status, 200);
988 (*(json_body(&response))
989 .get("id")
990 .unwrap_or(&serde_json::Value::Null))
991 .as_str()
992 .unwrap()
993 .to_owned()
994 }
995
996 async fn create_key_and_get_raw(
998 plugin: &ApiKeyPlugin,
999 ctx: &AuthContext<impl alibi_core::AuthSchema>,
1000 token: &str,
1001 body: serde_json::Value,
1002 ) -> (String, String) {
1003 let req = create_auth_request(
1004 HttpMethod::Post,
1005 "/api-key/create",
1006 Some(token),
1007 Some(body),
1008 None,
1009 );
1010 let response = plugin.handle_create(&req, ctx).await.unwrap();
1011 assert_eq!(response.status, 200);
1012 let b = json_body(&response);
1013 (
1014 (*(b).get("id").expect("fixture contains the requested index"))
1015 .as_str()
1016 .unwrap()
1017 .to_owned(),
1018 (*(b)
1019 .get("key")
1020 .expect("fixture contains the requested index"))
1021 .as_str()
1022 .unwrap()
1023 .to_owned(),
1024 )
1025 }
1026
1027 #[tokio::test]
1029 async fn test_create_and_get_do_not_expose_hash() {
1030 let plugin = ApiKeyPlugin::builder().prefix("ba_".to_owned()).build();
1031 let (ctx, _user, session) = create_test_context_with_user().await;
1032
1033 let create_req = create_auth_request(
1034 HttpMethod::Post,
1035 "/api-key/create",
1036 Some(&session.token),
1037 Some(serde_json::json!({ "name": "primary" })),
1038 None,
1039 );
1040 let create_response = plugin.handle_create(&create_req, &ctx).await.unwrap();
1041 assert_eq!(create_response.status, 200);
1042
1043 let body = json_body(&create_response);
1044 assert!(body.get("key").is_some());
1045 assert!(body.get("key_hash").is_none());
1046 assert!(body.get("hash").is_none());
1047
1048 let id = (*(body).get("id").unwrap_or(&serde_json::Value::Null))
1049 .as_str()
1050 .unwrap();
1051 let mut query = HashMap::new();
1052 query.insert("id".to_owned(), id.to_owned());
1053
1054 let get_req = create_auth_request(
1055 HttpMethod::Get,
1056 "/api-key/get",
1057 Some(&session.token),
1058 None,
1059 Some(query),
1060 );
1061 let get_response = plugin.handle_get(&get_req, &ctx).await.unwrap();
1062 assert_eq!(get_response.status, 200);
1063
1064 let get_body = json_body(&get_response);
1065 assert!(get_body.get("key").is_none());
1066 assert!(get_body.get("key_hash").is_none());
1067 }
1068
1069 #[tokio::test]
1071 async fn test_create_rejects_invalid_expires_in() {
1072 let plugin = ApiKeyPlugin::builder().build();
1073 let (ctx, _user, session) = create_test_context_with_user().await;
1074
1075 let req = create_auth_request(
1076 HttpMethod::Post,
1077 "/api-key/create",
1078 Some(&session.token),
1079 Some(serde_json::json!({ "expiresIn": -1 })),
1080 None,
1081 );
1082 let response = plugin.handle_create(&req, &ctx).await;
1083 assert!(response.is_err() || response.unwrap().status != 200);
1085 }
1086
1087 #[tokio::test]
1089 async fn test_get_update_delete_return_404_for_non_owner() {
1090 let plugin = ApiKeyPlugin::builder().build();
1091 let (ctx, _user1, session1) = create_test_context_with_user().await;
1092 let (_user2, session2) = create_user_with_session(&ctx, "other@example.com").await;
1093 let key_id = create_key_and_get_id(&plugin, &ctx, &session1.token, "owner-key").await;
1094
1095 let mut get_query = HashMap::new();
1096 get_query.insert("id".to_owned(), key_id.clone());
1097 let get_req = create_auth_request(
1098 HttpMethod::Get,
1099 "/api-key/get",
1100 Some(&session2.token),
1101 None,
1102 Some(get_query),
1103 );
1104 let get_err = plugin.handle_get(&get_req, &ctx).await.unwrap_err();
1105 assert_eq!(get_err.status_code(), 404);
1106
1107 let update_req = create_auth_request(
1108 HttpMethod::Post,
1109 "/api-key/update",
1110 Some(&session2.token),
1111 Some(serde_json::json!({ "keyId": key_id, "name": "new-name" })),
1112 None,
1113 );
1114 let update_err = plugin.handle_update(&update_req, &ctx).await.unwrap_err();
1115 assert_eq!(update_err.status_code(), 404);
1116
1117 let delete_req = create_auth_request(
1118 HttpMethod::Post,
1119 "/api-key/delete",
1120 Some(&session2.token),
1121 Some(serde_json::json!({ "keyId": key_id })),
1122 None,
1123 );
1124 let delete_err = plugin.handle_delete(&delete_req, &ctx).await.unwrap_err();
1125 assert_eq!(delete_err.status_code(), 404);
1126 }
1127
1128 #[tokio::test]
1130 async fn test_list_returns_only_user_keys() {
1131 let plugin = ApiKeyPlugin::builder().build();
1132 let (ctx, user1, session1) = create_test_context_with_user().await;
1133 let (_user2, session2) = create_user_with_session(&ctx, "other@example.com").await;
1134
1135 _ = create_key_and_get_id(&plugin, &ctx, &session1.token, "u1-key").await;
1136 _ = create_key_and_get_id(&plugin, &ctx, &session2.token, "u2-key").await;
1137
1138 let list_req = create_auth_request(
1139 HttpMethod::Get,
1140 "/api-key/list",
1141 Some(&session1.token),
1142 None,
1143 None,
1144 );
1145 let list_response = plugin.handle_list(&list_req, &ctx).await.unwrap();
1146 assert_eq!(list_response.status, 200);
1147
1148 let list_body = json_body(&list_response);
1149 let list = (*(list_body)
1151 .get("apiKeys")
1152 .unwrap_or(&serde_json::Value::Null))
1153 .as_array()
1154 .unwrap();
1155 assert_eq!(list.len(), 1);
1156 assert_eq!(
1157 (*(list_body).get("total").unwrap_or(&serde_json::Value::Null)),
1158 1
1159 );
1160 assert_eq!(
1163 (*(*(list)
1164 .first()
1165 .expect("fixture contains the requested index"))
1166 .get("referenceId")
1167 .expect("fixture contains the requested index"))
1168 .as_str()
1169 .unwrap(),
1170 user1.id
1171 );
1172 assert_eq!(
1173 (*(*(list)
1174 .first()
1175 .expect("fixture contains the requested index"))
1176 .get("configId")
1177 .expect("fixture contains the requested index"))
1178 .as_str()
1179 .unwrap(),
1180 "default"
1181 );
1182 assert!(
1183 (*(list)
1184 .first()
1185 .expect("fixture contains the requested index"))
1186 .get("userId")
1187 .is_none()
1188 );
1189 assert!(
1190 (*(list)
1191 .first()
1192 .expect("fixture contains the requested index"))
1193 .get("key")
1194 .is_none()
1195 );
1196 assert!(
1197 (*(list)
1198 .first()
1199 .expect("fixture contains the requested index"))
1200 .get("key_hash")
1201 .is_none()
1202 );
1203 }
1204
1205 #[tokio::test]
1207 async fn test_owner_can_delete_key() {
1208 let plugin = ApiKeyPlugin::builder().build();
1209 let (ctx, _user, session) = create_test_context_with_user().await;
1210 let key_id = create_key_and_get_id(&plugin, &ctx, &session.token, "to-delete").await;
1211
1212 let delete_req = create_auth_request(
1213 HttpMethod::Post,
1214 "/api-key/delete",
1215 Some(&session.token),
1216 Some(serde_json::json!({ "keyId": key_id })),
1217 None,
1218 );
1219 let delete_response = plugin.handle_delete(&delete_req, &ctx).await.unwrap();
1220 assert_eq!(delete_response.status, 200);
1221
1222 let deleted = ctx.database.get_api_key_by_id(&key_id).await.unwrap();
1223 assert!(deleted.is_none());
1224 }
1225
1226 #[tokio::test]
1228 async fn test_verify_valid_key() {
1229 let plugin = ApiKeyPlugin::builder().build();
1230 let (ctx, _user, session) = create_test_context_with_user().await;
1231
1232 let (_id, raw_key) = create_key_and_get_raw(
1233 &plugin,
1234 &ctx,
1235 &session.token,
1236 serde_json::json!({ "name": "verify-test" }),
1237 )
1238 .await;
1239
1240 let body = verify_key(&plugin, &ctx, &raw_key, None).await;
1241 assert_eq!(
1242 (*(body).get("valid").unwrap_or(&serde_json::Value::Null)),
1243 true
1244 );
1245 assert!((*(body).get("key").unwrap_or(&serde_json::Value::Null)).is_object());
1246 }
1247
1248 #[tokio::test]
1249 async fn test_verify_invalid_key() {
1250 let plugin = ApiKeyPlugin::builder().build();
1251 let (ctx, _user, _session) = create_test_context_with_user().await;
1252
1253 let body = verify_key(&plugin, &ctx, "definitely-not-a-valid-key", None).await;
1254 assert_eq!(
1255 (*(body).get("valid").unwrap_or(&serde_json::Value::Null)),
1256 false
1257 );
1258 assert!((*(body).get("error").unwrap_or(&serde_json::Value::Null)).is_object());
1259 }
1260
1261 #[tokio::test]
1262 async fn test_verify_disabled_key() {
1263 let plugin = ApiKeyPlugin::builder().build();
1264 let (ctx, _user, session) = create_test_context_with_user().await;
1265
1266 let (id, raw_key) = create_key_and_get_raw(
1267 &plugin,
1268 &ctx,
1269 &session.token,
1270 serde_json::json!({ "name": "disable-test" }),
1271 )
1272 .await;
1273
1274 let update = UpdateApiKey {
1275 enabled: Some(false),
1276 ..Default::default()
1277 };
1278 ctx.database.update_api_key(&id, update).await.unwrap();
1279
1280 let body = verify_key(&plugin, &ctx, &raw_key, None).await;
1281 assert_eq!(
1282 (*(body).get("valid").unwrap_or(&serde_json::Value::Null)),
1283 false
1284 );
1285 assert_eq!(
1286 (*(*(body).get("error").unwrap_or(&serde_json::Value::Null))
1287 .get("code")
1288 .unwrap_or(&serde_json::Value::Null)),
1289 "KEY_DISABLED"
1290 );
1291 }
1292
1293 #[tokio::test]
1294 async fn test_verify_expired_key() {
1295 let plugin = ApiKeyPlugin::builder().build();
1296 let (ctx, _user, session) = create_test_context_with_user().await;
1297
1298 let (id, raw_key) = create_key_and_get_raw(
1299 &plugin,
1300 &ctx,
1301 &session.token,
1302 serde_json::json!({ "name": "expire-test" }),
1303 )
1304 .await;
1305
1306 let past = (Utc::now() - Duration::hours(1)).to_rfc3339();
1307 let update = UpdateApiKey {
1308 expires_at: Some(Some(past)),
1309 ..Default::default()
1310 };
1311 ctx.database.update_api_key(&id, update).await.unwrap();
1312
1313 let body = verify_key(&plugin, &ctx, &raw_key, None).await;
1314 assert_eq!(
1315 (*(body).get("valid").unwrap_or(&serde_json::Value::Null)),
1316 false
1317 );
1318 assert_eq!(
1319 (*(*(body).get("error").unwrap_or(&serde_json::Value::Null))
1320 .get("code")
1321 .unwrap_or(&serde_json::Value::Null)),
1322 "KEY_EXPIRED"
1323 );
1324
1325 let deleted = ctx.database.get_api_key_by_id(&id).await.unwrap();
1327 assert!(deleted.is_none());
1328 }
1329
1330 #[tokio::test]
1332 async fn test_verify_remaining_consumption() {
1333 let plugin = ApiKeyPlugin::builder().build();
1334 let (ctx, _user, session) = create_test_context_with_user().await;
1335
1336 let (_id, raw_key) = create_key_with_server_fields(
1337 &plugin,
1338 &ctx,
1339 &session.token,
1340 serde_json::json!({ "name": "remain-test" }),
1341 UpdateApiKey {
1342 remaining: Some(2.0),
1343 ..Default::default()
1344 },
1345 )
1346 .await;
1347
1348 let r1 = verify_key(&plugin, &ctx, &raw_key, None).await;
1350 assert_eq!(
1351 (*(r1).get("valid").unwrap_or(&serde_json::Value::Null)),
1352 true
1353 );
1354 assert_eq!(
1355 (*(*(r1).get("key").unwrap_or(&serde_json::Value::Null))
1356 .get("remaining")
1357 .unwrap_or(&serde_json::Value::Null)),
1358 1
1359 );
1360
1361 let r2 = verify_key(&plugin, &ctx, &raw_key, None).await;
1363 assert_eq!(
1364 (*(r2).get("valid").unwrap_or(&serde_json::Value::Null)),
1365 true
1366 );
1367 assert_eq!(
1368 (*(*(r2).get("key").unwrap_or(&serde_json::Value::Null))
1369 .get("remaining")
1370 .unwrap_or(&serde_json::Value::Null)),
1371 0
1372 );
1373
1374 let r3 = verify_key(&plugin, &ctx, &raw_key, None).await;
1376 assert_eq!(
1377 (*(r3).get("valid").unwrap_or(&serde_json::Value::Null)),
1378 false
1379 );
1380 assert_eq!(
1381 (*(*(r3).get("error").unwrap_or(&serde_json::Value::Null))
1382 .get("code")
1383 .unwrap_or(&serde_json::Value::Null)),
1384 "USAGE_EXCEEDED"
1385 );
1386 }
1387
1388 #[tokio::test]
1390 async fn test_verify_rate_limiting() {
1391 let plugin = ApiKeyPlugin::builder()
1392 .rate_limit(RateLimitDefaults {
1393 enabled: true,
1394 time_window: 60_000.0,
1395 max_requests: 2.0,
1396 })
1397 .build();
1398 let (ctx, _user, session) = create_test_context_with_user().await;
1399
1400 let (_id, raw_key) = create_key_with_server_fields(
1401 &plugin,
1402 &ctx,
1403 &session.token,
1404 serde_json::json!({ "name": "rl-test" }),
1405 UpdateApiKey {
1406 rate_limit_enabled: Some(true),
1407 rate_limit_time_window: Some(60_000.0),
1408 rate_limit_max: Some(2.0),
1409 ..Default::default()
1410 },
1411 )
1412 .await;
1413
1414 let r1 = verify_key(&plugin, &ctx, &raw_key, None).await;
1416 assert_eq!(
1417 (*(r1).get("valid").unwrap_or(&serde_json::Value::Null)),
1418 true
1419 );
1420
1421 let r2 = verify_key(&plugin, &ctx, &raw_key, None).await;
1422 assert_eq!(
1423 (*(r2).get("valid").unwrap_or(&serde_json::Value::Null)),
1424 true
1425 );
1426
1427 let r3 = verify_key(&plugin, &ctx, &raw_key, None).await;
1429 assert_eq!(
1430 (*(r3).get("valid").unwrap_or(&serde_json::Value::Null)),
1431 false
1432 );
1433 assert_eq!(
1434 (*(*(r3).get("error").unwrap_or(&serde_json::Value::Null))
1435 .get("code")
1436 .unwrap_or(&serde_json::Value::Null)),
1437 "RATE_LIMITED"
1438 );
1439 }
1440
1441 #[tokio::test]
1442 async fn test_delete_all_expired() {
1443 let plugin = ApiKeyPlugin::builder().build();
1444 let (ctx, fixture_user, session) = create_test_context_with_user().await;
1445
1446 let (id1, _) = create_key_and_get_raw(
1448 &plugin,
1449 &ctx,
1450 &session.token,
1451 serde_json::json!({ "name": "will-expire" }),
1452 )
1453 .await;
1454 let (_id2, _) = create_key_and_get_raw(
1455 &plugin,
1456 &ctx,
1457 &session.token,
1458 serde_json::json!({ "name": "wont-expire" }),
1459 )
1460 .await;
1461
1462 let past = (Utc::now() - Duration::hours(1)).to_rfc3339();
1464 ctx.database
1465 .update_api_key(
1466 &id1,
1467 UpdateApiKey {
1468 expires_at: Some(Some(past)),
1469 ..Default::default()
1470 },
1471 )
1472 .await
1473 .unwrap();
1474
1475 let body = delete_all_expired(&ctx).await;
1476 assert_eq!(
1477 (*(body).get("success").unwrap_or(&serde_json::Value::Null)),
1478 true
1479 );
1480
1481 let remaining_keys = ctx
1483 .database
1484 .list_api_keys_by_reference(&fixture_user.id)
1485 .await
1486 .unwrap();
1487 assert_eq!(remaining_keys.len(), 1);
1488 }
1489
1490 #[tokio::test]
1492 async fn test_verify_permissions() {
1493 let plugin = ApiKeyPlugin::builder().build();
1494 let (ctx, _user, session) = create_test_context_with_user().await;
1495
1496 let (_id, raw_key) = create_key_with_server_fields(
1497 &plugin,
1498 &ctx,
1499 &session.token,
1500 serde_json::json!({ "name": "perm-test" }),
1501 UpdateApiKey {
1502 permissions: Some(
1503 serde_json::to_string(&serde_json::json!({
1504 "admin": ["read", "write"],
1505 "user": ["read"]
1506 }))
1507 .unwrap(),
1508 ),
1509 ..Default::default()
1510 },
1511 )
1512 .await;
1513
1514 let perms_ok = serde_json::json!({ "admin": ["read"] });
1516 let r1 = verify_key(&plugin, &ctx, &raw_key, Some(&perms_ok)).await;
1517 assert_eq!(
1518 (*(r1).get("valid").unwrap_or(&serde_json::Value::Null)),
1519 true
1520 );
1521
1522 let perms_fail = serde_json::json!({ "superadmin": ["delete"] });
1524 let r2 = verify_key(&plugin, &ctx, &raw_key, Some(&perms_fail)).await;
1525 assert_eq!(
1526 (*(r2).get("valid").unwrap_or(&serde_json::Value::Null)),
1527 false
1528 );
1529 }
1530
1531 #[tokio::test]
1533 async fn test_config_validation_prefix_length() {
1534 let plugin = ApiKeyPlugin::builder()
1535 .min_prefix_length(2)
1536 .max_prefix_length(5)
1537 .build();
1538 let (ctx, _user, session) = create_test_context_with_user().await;
1539
1540 let req = create_auth_request(
1542 HttpMethod::Post,
1543 "/api-key/create",
1544 Some(&session.token),
1545 Some(serde_json::json!({ "name": "test", "prefix": "a" })),
1546 None,
1547 );
1548 let err = plugin.handle_create(&req, &ctx).await.unwrap_err();
1549 assert!(err.to_string().contains("prefix length"));
1550
1551 let req2 = create_auth_request(
1553 HttpMethod::Post,
1554 "/api-key/create",
1555 Some(&session.token),
1556 Some(serde_json::json!({ "name": "test", "prefix": "toolong" })),
1557 None,
1558 );
1559 let err2 = plugin.handle_create(&req2, &ctx).await.unwrap_err();
1560 assert!(err2.to_string().contains("prefix length"));
1561 }
1562
1563 #[tokio::test]
1565 async fn test_config_require_name() {
1566 let plugin = ApiKeyPlugin::builder().require_name(true).build();
1567 let (ctx, _user, session) = create_test_context_with_user().await;
1568
1569 let req = create_auth_request(
1571 HttpMethod::Post,
1572 "/api-key/create",
1573 Some(&session.token),
1574 Some(serde_json::json!({})),
1575 None,
1576 );
1577 let err = plugin.handle_create(&req, &ctx).await.unwrap_err();
1578 assert!(err.to_string().contains("name is required"));
1579 }
1580
1581 #[tokio::test]
1583 async fn test_config_metadata_disabled() {
1584 let plugin = ApiKeyPlugin::builder().build(); let (ctx, _user, session) = create_test_context_with_user().await;
1586
1587 let req = create_auth_request(
1588 HttpMethod::Post,
1589 "/api-key/create",
1590 Some(&session.token),
1591 Some(serde_json::json!({ "name": "test", "metadata": { "env": "prod" } })),
1592 None,
1593 );
1594 let err = plugin.handle_create(&req, &ctx).await.unwrap_err();
1595 assert!(err.to_string().contains("Metadata is disabled"));
1596 }
1597
1598 #[tokio::test]
1600 async fn test_config_metadata_enabled() {
1601 let plugin = ApiKeyPlugin::builder().enable_metadata(true).build();
1602 let (ctx, _user, session) = create_test_context_with_user().await;
1603
1604 let req = create_auth_request(
1605 HttpMethod::Post,
1606 "/api-key/create",
1607 Some(&session.token),
1608 Some(serde_json::json!({ "name": "test", "metadata": { "env": "prod" } })),
1609 None,
1610 );
1611 let resp = plugin.handle_create(&req, &ctx).await.unwrap();
1612 assert_eq!(resp.status, 200);
1613 let body = json_body(&resp);
1614 assert_eq!(
1615 (*(*(body).get("metadata").unwrap_or(&serde_json::Value::Null))
1616 .get("env")
1617 .unwrap_or(&serde_json::Value::Null)),
1618 "prod"
1619 );
1620 }
1621
1622 #[tokio::test]
1624 async fn test_update_with_expires_in() {
1625 let plugin = ApiKeyPlugin::builder().build();
1626 let (ctx, _user, session) = create_test_context_with_user().await;
1627 let key_id = create_key_and_get_id(&plugin, &ctx, &session.token, "update-exp").await;
1628
1629 let update_req = create_auth_request(
1630 HttpMethod::Post,
1631 "/api-key/update",
1632 Some(&session.token),
1633 Some(serde_json::json!({
1634 "keyId": key_id,
1635 "expiresIn": 86400
1636 })),
1637 None,
1638 );
1639 let resp = plugin.handle_update(&update_req, &ctx).await.unwrap();
1640 assert_eq!(resp.status, 200);
1641 let body = json_body(&resp);
1642 assert!((*(body).get("expiresAt").unwrap_or(&serde_json::Value::Null)).is_string());
1643 }
1644
1645 #[tokio::test]
1651 async fn test_refill_logic() {
1652 let result = ApiKeyPlugin::validate_refill(Some(60_000.0), None);
1653 assert!(result.is_err());
1654 assert!(result.unwrap_err().to_string().contains("refillAmount"));
1655
1656 let result_2 = ApiKeyPlugin::validate_refill(None, Some(10.0));
1657 assert!(result_2.is_err());
1658
1659 let result_3 = ApiKeyPlugin::validate_refill(Some(60_000.0), Some(10.0));
1660 assert!(result_3.is_ok());
1661 }
1662
1663 mod session_tests {
1664 use super::*;
1665
1666 #[tokio::test]
1667 async fn test_virtual_session_answers_get_and_post_get_session() {
1668 use alibi_core::AuthPlugin;
1669
1670 let plugin = ApiKeyPlugin::builder()
1671 .enable_session_for_api_keys(true)
1672 .build();
1673 let (ctx, user, session) = create_test_context_with_user().await;
1674 let (id, raw_key) = create_key_and_get_raw(
1675 &plugin,
1676 &ctx,
1677 &session.token,
1678 serde_json::json!({ "name": "get-session" }),
1679 )
1680 .await;
1681
1682 for method in [HttpMethod::Get, HttpMethod::Post] {
1683 let mut request = create_auth_request(method, "/get-session", None, None, None);
1684 _ = request
1685 .headers
1686 .insert("x-api-key".to_owned(), raw_key.clone());
1687 let action = AuthPlugin::<TestSchema>::before_request(&plugin, &request, &ctx)
1688 .await
1689 .unwrap();
1690 let Some(BeforeRequestAction::Respond(response)) = action else {
1691 panic!("API key sessions must answer GET and POST before route dispatch")
1692 };
1693 assert_eq!(response.status, 200);
1694 let body = json_body(&response);
1695 assert_eq!(
1696 (*(*(body).get("session").unwrap_or(&serde_json::Value::Null))
1697 .get("id")
1698 .unwrap_or(&serde_json::Value::Null)),
1699 id
1700 );
1701 assert_eq!(
1702 (*(*(body).get("session").unwrap_or(&serde_json::Value::Null))
1703 .get("token")
1704 .unwrap_or(&serde_json::Value::Null)),
1705 raw_key
1706 );
1707 assert_eq!(
1708 (*(*(body).get("session").unwrap_or(&serde_json::Value::Null))
1709 .get("userId")
1710 .unwrap_or(&serde_json::Value::Null)),
1711 user.id
1712 );
1713 }
1714 }
1715
1716 #[tokio::test]
1719 async fn test_virtual_session_creates_no_db_session() {
1720 let plugin = ApiKeyPlugin::builder()
1721 .enable_session_for_api_keys(true)
1722 .build();
1723 let (ctx, fixture_user, session) = create_test_context_with_user().await;
1724
1725 let (_id, raw_key) = create_key_and_get_raw(
1727 &plugin,
1728 &ctx,
1729 &session.token,
1730 serde_json::json!({ "name": "virtual-session-test" }),
1731 )
1732 .await;
1733
1734 let sessions_before = ctx
1736 .database
1737 .get_user_sessions(&fixture_user.id)
1738 .await
1739 .unwrap()
1740 .len();
1741
1742 let mut headers = HashMap::new();
1744 headers.insert("x-api-key".to_owned(), raw_key.clone());
1745 let req = AuthRequest::from_parts(
1746 HttpMethod::Post,
1747 "/update-user".to_owned(),
1748 headers,
1749 None,
1750 HashMap::new(),
1751 );
1752
1753 let action = plugin.before_request(&req, &ctx).await.unwrap();
1755 assert!(action.is_some(), "before_request should return an action");
1756 match action.unwrap() {
1757 BeforeRequestAction::InjectSession { session: session_2 } => {
1758 assert_eq!(session_2.user_id, fixture_user.id);
1759 }
1760 BeforeRequestAction::Respond(_) | BeforeRequestAction::ReplaceHeaders { .. } => {
1761 panic!("Expected InjectSession, got Respond");
1762 }
1763 }
1764
1765 let sessions_after = ctx
1767 .database
1768 .get_user_sessions(&fixture_user.id)
1769 .await
1770 .unwrap()
1771 .len();
1772 assert_eq!(
1773 sessions_before, sessions_after,
1774 "No new sessions should be created in the database"
1775 );
1776 }
1777
1778 #[tokio::test]
1781 async fn test_virtual_session_on_get_session() {
1782 let plugin = ApiKeyPlugin::builder()
1783 .enable_session_for_api_keys(true)
1784 .build();
1785 let (ctx, user, session) = create_test_context_with_user().await;
1786
1787 let (_id, raw_key) = create_key_and_get_raw(
1788 &plugin,
1789 &ctx,
1790 &session.token,
1791 serde_json::json!({ "name": "get-session-test" }),
1792 )
1793 .await;
1794
1795 let mut headers = HashMap::new();
1797 headers.insert("x-api-key".to_owned(), raw_key.clone());
1798 let req = AuthRequest::from_parts(
1799 HttpMethod::Get,
1800 "/get-session".to_owned(),
1801 headers,
1802 None,
1803 HashMap::new(),
1804 );
1805
1806 let action = plugin.before_request(&req, &ctx).await.unwrap();
1807 assert!(action.is_some());
1808 match action.unwrap() {
1809 BeforeRequestAction::Respond(resp) => {
1810 assert_eq!(resp.status, 200);
1811 let body: serde_json::Value = serde_json::from_slice(&resp.body).unwrap();
1812 assert_eq!(
1814 (*(*(body).get("user").unwrap_or(&serde_json::Value::Null))
1815 .get("id")
1816 .unwrap_or(&serde_json::Value::Null)),
1817 user.id
1818 );
1819 assert_eq!(
1820 (*(*(body).get("user").unwrap_or(&serde_json::Value::Null))
1821 .get("email")
1822 .unwrap_or(&serde_json::Value::Null)),
1823 "test@example.com"
1824 );
1825 assert!(
1827 (*(*(body).get("session").unwrap_or(&serde_json::Value::Null))
1828 .get("id")
1829 .unwrap_or(&serde_json::Value::Null))
1830 .is_string()
1831 );
1832 assert_eq!(
1833 (*(*(body).get("session").unwrap_or(&serde_json::Value::Null))
1834 .get("userId")
1835 .unwrap_or(&serde_json::Value::Null)),
1836 user.id
1837 );
1838 }
1839 BeforeRequestAction::InjectSession { .. }
1840 | BeforeRequestAction::ReplaceHeaders { .. } => {
1841 panic!("Expected Respond for /get-session, got InjectSession");
1842 }
1843 }
1844 }
1845
1846 #[tokio::test]
1849 async fn test_rate_limiting_third_call_fails() {
1850 let plugin = ApiKeyPlugin::builder()
1851 .rate_limit(RateLimitDefaults {
1852 enabled: true,
1853 time_window: 60_000.0,
1854 max_requests: 2.0,
1855 })
1856 .build();
1857 let (ctx, _user, session) = create_test_context_with_user().await;
1858
1859 let (_id, raw_key) = create_key_with_server_fields(
1860 &plugin,
1861 &ctx,
1862 &session.token,
1863 serde_json::json!({ "name": "rl-integration" }),
1864 UpdateApiKey {
1865 rate_limit_enabled: Some(true),
1866 rate_limit_time_window: Some(60_000.0),
1867 rate_limit_max: Some(2.0),
1868 ..Default::default()
1869 },
1870 )
1871 .await;
1872
1873 let r1 = verify_key(&plugin, &ctx, &raw_key, None).await;
1875 assert_eq!(
1876 (*(r1).get("valid").unwrap_or(&serde_json::Value::Null)),
1877 true,
1878 "1st request should pass"
1879 );
1880
1881 let r2 = verify_key(&plugin, &ctx, &raw_key, None).await;
1882 assert_eq!(
1883 (*(r2).get("valid").unwrap_or(&serde_json::Value::Null)),
1884 true,
1885 "2nd request should pass"
1886 );
1887
1888 let r3 = verify_key(&plugin, &ctx, &raw_key, None).await;
1890 assert_eq!(
1891 (*(r3).get("valid").unwrap_or(&serde_json::Value::Null)),
1892 false,
1893 "3rd request should be rate-limited"
1894 );
1895 assert_eq!(
1896 (*(*(r3).get("error").unwrap_or(&serde_json::Value::Null))
1897 .get("code")
1898 .unwrap_or(&serde_json::Value::Null)),
1899 "RATE_LIMITED"
1900 );
1901 }
1902
1903 #[tokio::test]
1906 async fn test_remaining_consumption_no_refill() {
1907 let plugin = ApiKeyPlugin::builder().build();
1908 let (ctx, _user, session) = create_test_context_with_user().await;
1909
1910 let (_id, raw_key) = create_key_with_server_fields(
1911 &plugin,
1912 &ctx,
1913 &session.token,
1914 serde_json::json!({ "name": "remaining-test" }),
1915 UpdateApiKey {
1916 remaining: Some(2.0),
1917 ..Default::default()
1918 },
1919 )
1920 .await;
1921
1922 let r1 = verify_key(&plugin, &ctx, &raw_key, None).await;
1924 assert_eq!(
1925 (*(r1).get("valid").unwrap_or(&serde_json::Value::Null)),
1926 true
1927 );
1928 assert_eq!(
1929 (*(*(r1).get("key").unwrap_or(&serde_json::Value::Null))
1930 .get("remaining")
1931 .unwrap_or(&serde_json::Value::Null)),
1932 1
1933 );
1934
1935 let r2 = verify_key(&plugin, &ctx, &raw_key, None).await;
1937 assert_eq!(
1938 (*(r2).get("valid").unwrap_or(&serde_json::Value::Null)),
1939 true
1940 );
1941 assert_eq!(
1942 (*(*(r2).get("key").unwrap_or(&serde_json::Value::Null))
1943 .get("remaining")
1944 .unwrap_or(&serde_json::Value::Null)),
1945 0
1946 );
1947
1948 let r3 = verify_key(&plugin, &ctx, &raw_key, None).await;
1950 assert_eq!(
1951 (*(r3).get("valid").unwrap_or(&serde_json::Value::Null)),
1952 false
1953 );
1954 assert_eq!(
1955 (*(*(r3).get("error").unwrap_or(&serde_json::Value::Null))
1956 .get("code")
1957 .unwrap_or(&serde_json::Value::Null)),
1958 "USAGE_EXCEEDED"
1959 );
1960 }
1961
1962 #[tokio::test]
1967 async fn test_refill_resets_remaining_after_interval() {
1968 let plugin = ApiKeyPlugin::builder().build();
1969 let (ctx, _user, session) = create_test_context_with_user().await;
1970
1971 let (_id, raw_key) = create_key_with_server_fields(
1973 &plugin,
1974 &ctx,
1975 &session.token,
1976 serde_json::json!({ "name": "refill-test" }),
1977 UpdateApiKey {
1978 remaining: Some(1.0),
1979 refill_interval: Some(100.0),
1980 refill_amount: Some(10.0),
1981 ..Default::default()
1982 },
1983 )
1984 .await;
1985
1986 let r1 = verify_key(&plugin, &ctx, &raw_key, None).await;
1988 assert_eq!(
1989 (*(r1).get("valid").unwrap_or(&serde_json::Value::Null)),
1990 true
1991 );
1992 assert_eq!(
1993 (*(*(r1).get("key").unwrap_or(&serde_json::Value::Null))
1994 .get("remaining")
1995 .unwrap_or(&serde_json::Value::Null)),
1996 0
1997 );
1998
1999 tokio::time::sleep(std::time::Duration::from_millis(150)).await;
2001
2002 let r2 = verify_key(&plugin, &ctx, &raw_key, None).await;
2004 assert_eq!(
2005 (*(r2).get("valid").unwrap_or(&serde_json::Value::Null)),
2006 true,
2007 "Should succeed after refill"
2008 );
2009 assert_eq!(
2010 (*(*(r2).get("key").unwrap_or(&serde_json::Value::Null))
2011 .get("remaining")
2012 .unwrap_or(&serde_json::Value::Null)),
2013 9,
2014 "Should be refillAmount - 1 = 9"
2015 );
2016 }
2017
2018 #[tokio::test]
2022 async fn test_permissions_mismatch_fails() {
2023 let plugin = ApiKeyPlugin::builder().build();
2024 let (ctx, _user, session) = create_test_context_with_user().await;
2025
2026 let (_id, raw_key) = create_key_with_server_fields(
2027 &plugin,
2028 &ctx,
2029 &session.token,
2030 serde_json::json!({ "name": "perm-mismatch" }),
2031 UpdateApiKey {
2032 permissions: Some(
2033 serde_json::to_string(&serde_json::json!({ "admin": ["read"] })).unwrap(),
2034 ),
2035 ..Default::default()
2036 },
2037 )
2038 .await;
2039
2040 let perms_ok = serde_json::json!({ "admin": ["read"] });
2042 let r1 = verify_key(&plugin, &ctx, &raw_key, Some(&perms_ok)).await;
2043 assert_eq!(
2044 (*(r1).get("valid").unwrap_or(&serde_json::Value::Null)),
2045 true
2046 );
2047
2048 let perms_fail = serde_json::json!({ "admin": ["write"] });
2050 let r2 = verify_key(&plugin, &ctx, &raw_key, Some(&perms_fail)).await;
2051 assert_eq!(
2052 (*(r2).get("valid").unwrap_or(&serde_json::Value::Null)),
2053 false
2054 );
2055 }
2056
2057 #[tokio::test]
2062 async fn test_concurrent_rate_limiting() {
2063 let plugin = ApiKeyPlugin::builder()
2064 .rate_limit(RateLimitDefaults {
2065 enabled: true,
2066 time_window: 60_000.0,
2067 max_requests: 2.0,
2068 })
2069 .build();
2070 let (ctx, _user, session) = create_test_context_with_user().await;
2071
2072 let (_id, raw_key) = create_key_with_server_fields(
2073 &plugin,
2074 &ctx,
2075 &session.token,
2076 serde_json::json!({ "name": "concurrent-rl" }),
2077 UpdateApiKey {
2078 rate_limit_enabled: Some(true),
2079 rate_limit_time_window: Some(60_000.0),
2080 rate_limit_max: Some(2.0),
2081 ..Default::default()
2082 },
2083 )
2084 .await;
2085
2086 let mut success_count = 0;
2087 let mut fail_count = 0;
2088
2089 for _ in 0..5 {
2090 let body = verify_key(&plugin, &ctx, &raw_key, None).await;
2091 if (*(body).get("valid").unwrap_or(&serde_json::Value::Null)) == true {
2092 success_count += 1;
2093 } else {
2094 fail_count += 1;
2095 assert_eq!(
2096 (*(*(body).get("error").unwrap_or(&serde_json::Value::Null))
2097 .get("code")
2098 .unwrap_or(&serde_json::Value::Null)),
2099 "RATE_LIMITED"
2100 );
2101 }
2102 }
2103
2104 assert_eq!(success_count, 2, "Only 2 out of 5 should succeed");
2105 assert_eq!(fail_count, 3, "3 out of 5 should be rate-limited");
2106 }
2107
2108 #[tokio::test]
2112 async fn test_delete_expired_api_keys_memory_adapter() {
2113 let (ctx, fixture_user, session) = create_test_context_with_user().await;
2114 let plugin = ApiKeyPlugin::builder().build();
2115
2116 let (id1, _) = create_key_and_get_raw(
2118 &plugin,
2119 &ctx,
2120 &session.token,
2121 serde_json::json!({ "name": "will-expire" }),
2122 )
2123 .await;
2124 let (_id2, _) = create_key_and_get_raw(
2125 &plugin,
2126 &ctx,
2127 &session.token,
2128 serde_json::json!({ "name": "wont-expire" }),
2129 )
2130 .await;
2131
2132 let past = (Utc::now() - Duration::hours(1)).to_rfc3339();
2134 ctx.database
2135 .update_api_key(
2136 &id1,
2137 UpdateApiKey {
2138 expires_at: Some(Some(past)),
2139 ..Default::default()
2140 },
2141 )
2142 .await
2143 .unwrap();
2144
2145 let deleted = ctx.database.delete_expired_api_keys().await.unwrap();
2147 assert_eq!(deleted, 1, "Should delete exactly 1 expired key");
2148
2149 let remaining = ctx
2151 .database
2152 .list_api_keys_by_reference(&fixture_user.id)
2153 .await
2154 .unwrap();
2155 assert_eq!(remaining.len(), 1);
2156 }
2157
2158 #[tokio::test]
2160 async fn test_delete_expired_removes_only_expired() {
2161 let plugin = ApiKeyPlugin::builder().build();
2162 let (ctx, fixture_user, session) = create_test_context_with_user().await;
2163
2164 let (id1, _) = create_key_and_get_raw(
2166 &plugin,
2167 &ctx,
2168 &session.token,
2169 serde_json::json!({ "name": "expired" }),
2170 )
2171 .await;
2172 let (_id2, _) = create_key_and_get_raw(
2173 &plugin,
2174 &ctx,
2175 &session.token,
2176 serde_json::json!({ "name": "active" }),
2177 )
2178 .await;
2179
2180 let past = (Utc::now() - Duration::hours(1)).to_rfc3339();
2181 ctx.database
2182 .update_api_key(
2183 &id1,
2184 UpdateApiKey {
2185 expires_at: Some(Some(past)),
2186 ..Default::default()
2187 },
2188 )
2189 .await
2190 .unwrap();
2191
2192 let deleted = ctx.database.delete_expired_api_keys().await.unwrap();
2193 assert_eq!(deleted, 1);
2194
2195 let remaining = ctx
2196 .database
2197 .list_api_keys_by_reference(&fixture_user.id)
2198 .await
2199 .unwrap();
2200 assert_eq!(remaining.len(), 1);
2201 }
2202
2203 #[tokio::test]
2206 async fn test_before_request_disabled_returns_none() {
2207 let plugin = ApiKeyPlugin::builder().build(); let (ctx, _user, session) = create_test_context_with_user().await;
2209
2210 let (_id, raw_key) = create_key_and_get_raw(
2211 &plugin,
2212 &ctx,
2213 &session.token,
2214 serde_json::json!({ "name": "disabled-session" }),
2215 )
2216 .await;
2217
2218 let mut headers = HashMap::new();
2219 headers.insert("x-api-key".to_owned(), raw_key);
2220 let req = AuthRequest::from_parts(
2221 HttpMethod::Get,
2222 "/get-session".to_owned(),
2223 headers,
2224 None,
2225 HashMap::new(),
2226 );
2227
2228 let action = plugin.before_request(&req, &ctx).await.unwrap();
2229 assert!(
2230 action.is_none(),
2231 "before_request should return None when session emulation is disabled"
2232 );
2233 }
2234
2235 #[tokio::test]
2238 async fn test_resolve_configuration_falls_back_to_default() {
2239 let plugin = ApiKeyPlugin::builder().build().configuration(ApiKeyConfig {
2240 config_id: "billing".to_owned(),
2241 ..ApiKeyConfig::default()
2242 });
2243
2244 assert_eq!(
2245 plugin.resolve_configuration(None).unwrap().config_id,
2246 "default"
2247 );
2248 assert_eq!(
2249 plugin
2250 .resolve_configuration(Some("billing"))
2251 .unwrap()
2252 .config_id,
2253 "billing"
2254 );
2255 assert_eq!(
2257 plugin
2258 .resolve_configuration(Some("nope"))
2259 .unwrap()
2260 .config_id,
2261 "default"
2262 );
2263 }
2264
2265 #[tokio::test]
2268 async fn test_resolve_configuration_without_default_is_an_error() {
2269 let plugin = ApiKeyPlugin::builder()
2270 .config_id("billing".to_owned())
2271 .build();
2272
2273 let err = plugin.resolve_configuration(None).unwrap_err();
2274 assert_eq!(err.status_code(), 400);
2275 assert_eq!(err.to_string(), "No default api-key configuration found.");
2276 }
2277
2278 #[tokio::test]
2281 async fn test_config_id_matches_treats_missing_as_default() {
2282 assert!(config_id_matches("", "default"));
2283 assert!(config_id_matches("default", ""));
2284 assert!(config_id_matches("billing", "billing"));
2285 assert!(!config_id_matches("billing", "default"));
2286 }
2287
2288 #[tokio::test]
2291 async fn test_create_for_organization_requires_organization_id() {
2292 let plugin = ApiKeyPlugin::builder()
2293 .references(ApiKeyReferences::Organization)
2294 .build();
2295 let (ctx, _user, session) = create_test_context_with_user().await;
2296
2297 let req = create_auth_request(
2298 HttpMethod::Post,
2299 "/api-key/create",
2300 Some(&session.token),
2301 Some(serde_json::json!({ "name": "org-key" })),
2302 None,
2303 );
2304 let err = plugin.handle_create(&req, &ctx).await.unwrap_err();
2305
2306 assert_eq!(err.status_code(), 400);
2307 assert_eq!(
2308 err.to_string(),
2309 "Organization ID is required for organization-owned API keys."
2310 );
2311 }
2312
2313 #[tokio::test]
2316 async fn test_create_for_organization_requires_the_organization_plugin() {
2317 let plugin = ApiKeyPlugin::builder()
2318 .references(ApiKeyReferences::Organization)
2319 .build();
2320 let (ctx, _user, session) = create_test_context_with_user().await;
2321
2322 let req = create_auth_request(
2323 HttpMethod::Post,
2324 "/api-key/create",
2325 Some(&session.token),
2326 Some(serde_json::json!({ "name": "org-key", "organizationId": "org-1" })),
2327 None,
2328 );
2329 let err = plugin.handle_create(&req, &ctx).await.unwrap_err();
2330
2331 assert_eq!(
2332 err.to_string(),
2333 "Organization plugin is required for organization-owned API keys. Please install and configure the organization plugin."
2334 );
2335 }
2336
2337 #[tokio::test]
2340 async fn test_create_for_organization_rejects_non_member() {
2341 let plugin = ApiKeyPlugin::builder()
2342 .references(ApiKeyReferences::Organization)
2343 .build();
2344 let (ctx, _user, session) = create_test_context_with_user().await;
2345
2346 let mut metadata: HashMap<String, serde_json::Value> = HashMap::new();
2348 _ = metadata.insert(
2349 crate::organization::METADATA_ENABLED.to_owned(),
2350 serde_json::Value::Bool(true),
2351 );
2352 let ctx = AuthContext::with_metadata(
2353 Arc::clone(&ctx.config),
2354 Arc::clone(&ctx.database),
2355 metadata,
2356 );
2357
2358 let req = create_auth_request(
2359 HttpMethod::Post,
2360 "/api-key/create",
2361 Some(&session.token),
2362 Some(
2363 serde_json::json!({ "name": "org-key", "organizationId": "org-the-user-is-not-in" }),
2364 ),
2365 None,
2366 );
2367 let err = plugin.handle_create(&req, &ctx).await.unwrap_err();
2368
2369 assert_eq!(
2370 err.to_string(),
2371 "You are not a member of the organization that owns this API key."
2372 );
2373 }
2374 }
2375
2376 mod verification_tests {
2377 use super::*;
2378
2379 #[tokio::test]
2380 async fn scoped_verification_uses_the_configuration_hashing_and_rejects_other_configs() {
2381 let plugin = ApiKeyPlugin::builder().build().configuration(ApiKeyConfig {
2382 config_id: "machines".to_owned(),
2383 disable_key_hashing: true,
2384 ..Default::default()
2385 });
2386 let (ctx, _, session) = create_test_context_with_user().await;
2387 let (id, key) = create_key_and_get_raw(
2388 &plugin,
2389 &ctx,
2390 &session.token,
2391 serde_json::json!({ "configId": "machines", "name": "machine" }),
2392 )
2393 .await;
2394 let scoped = VerifyApiKey {
2395 key: &key,
2396 config_id: Some("machines"),
2397 permissions: None,
2398 };
2399 let verified = plugin.verify_api_key(&scoped, &ctx).await.unwrap();
2400 assert_eq!(verified.id, id);
2401 assert_eq!(verified.config_id, "machines");
2402 let serialized = serde_json::to_value(verified).unwrap();
2403 assert!(serialized.get("key").is_none());
2404 assert!(serialized.get("keyHash").is_none());
2405
2406 for config_id in [None, Some("default"), Some("unknown")] {
2407 let error = plugin
2408 .verify_api_key(
2409 &VerifyApiKey {
2410 config_id,
2411 ..scoped
2412 },
2413 &ctx,
2414 )
2415 .await
2416 .unwrap_err();
2417 assert!(matches!(
2418 error,
2419 ApiKeyVerificationError::Validation(ApiKeyValidationError {
2420 code: ApiKeyErrorCode::InvalidApiKey,
2421 ..
2422 })
2423 ));
2424 }
2425 assert_eq!(
2426 ctx.database
2427 .get_api_key_by_id(&id)
2428 .await
2429 .unwrap()
2430 .unwrap()
2431 .request_count,
2432 Some(1.0)
2433 );
2434 }
2435
2436 #[tokio::test]
2437 async fn unscoped_verification_uses_the_issuing_configuration_limits() {
2438 let plugin = ApiKeyPlugin::builder().build().configuration(ApiKeyConfig {
2439 config_id: "machines".to_owned(),
2440 rate_limit: RateLimitDefaults {
2441 enabled: false,
2442 ..Default::default()
2443 },
2444 ..Default::default()
2445 });
2446 let (ctx, _, session) = create_test_context_with_user().await;
2447 let (id, key) = create_key_and_get_raw(
2448 &plugin,
2449 &ctx,
2450 &session.token,
2451 serde_json::json!({ "configId": "machines" }),
2452 )
2453 .await;
2454 ctx.database
2455 .update_api_key(
2456 &id,
2457 UpdateApiKey {
2458 rate_limit_enabled: Some(true),
2459 rate_limit_max: Some(1.0),
2460 ..Default::default()
2461 },
2462 )
2463 .await
2464 .unwrap();
2465
2466 let input = VerifyApiKey {
2467 key: &key,
2468 config_id: None,
2469 permissions: None,
2470 };
2471 for _ in 0..3 {
2472 assert_eq!(plugin.verify_api_key(&input, &ctx).await.unwrap().id, id);
2473 }
2474 let error = plugin
2475 .verify_api_key(
2476 &VerifyApiKey {
2477 config_id: Some("default"),
2478 ..input
2479 },
2480 &ctx,
2481 )
2482 .await
2483 .unwrap_err();
2484 assert!(matches!(
2485 error,
2486 ApiKeyVerificationError::Validation(ApiKeyValidationError {
2487 code: ApiKeyErrorCode::InvalidApiKey,
2488 ..
2489 })
2490 ));
2491 assert_eq!(
2492 ctx.database
2493 .get_api_key_by_id(&id)
2494 .await
2495 .unwrap()
2496 .unwrap()
2497 .request_count,
2498 Some(0.0)
2499 );
2500 }
2501
2502 #[tokio::test]
2503 async fn permissions_failure_preserves_usage_and_rate_limit_reports_retry_time() {
2504 let plugin = ApiKeyPlugin::builder().build();
2505 let (ctx, _, session) = create_test_context_with_user().await;
2506 let (id, key) = create_key_with_server_fields(
2507 &plugin,
2508 &ctx,
2509 &session.token,
2510 serde_json::json!({}),
2511 UpdateApiKey {
2512 remaining: Some(3.0),
2513 permissions: Some(serde_json::json!({ "nodes": ["read"] }).to_string()),
2514 rate_limit_max: Some(1.0),
2515 rate_limit_time_window: Some(60_000.0),
2516 ..Default::default()
2517 },
2518 )
2519 .await;
2520 let denied = serde_json::json!({ "nodes": ["delete"] });
2521 let allowed = serde_json::json!({ "nodes": ["read"] });
2522 let input = VerifyApiKey {
2523 key: &key,
2524 config_id: None,
2525 permissions: Some(&denied),
2526 };
2527 let error = plugin.verify_api_key(&input, &ctx).await.unwrap_err();
2528 assert!(matches!(
2529 error,
2530 ApiKeyVerificationError::Validation(ApiKeyValidationError {
2531 code: ApiKeyErrorCode::KeyNotFound,
2532 ..
2533 })
2534 ));
2535 assert_eq!(
2536 ctx.database
2537 .get_api_key_by_id(&id)
2538 .await
2539 .unwrap()
2540 .unwrap()
2541 .remaining,
2542 Some(3.0)
2543 );
2544 let input = VerifyApiKey {
2545 permissions: Some(&allowed),
2546 ..input
2547 };
2548 assert_eq!(
2549 plugin.verify_api_key(&input, &ctx).await.unwrap().remaining,
2550 Some(2.0)
2551 );
2552 let error_2 = plugin.verify_api_key(&input, &ctx).await.unwrap_err();
2553 let ApiKeyVerificationError::Validation(error_2_3) = error_2 else {
2554 panic!("Expected rate limit rejection")
2555 };
2556 assert_eq!(error_2_3.code, ApiKeyErrorCode::RateLimited);
2557 let retry = error_2_3.details.unwrap().try_again_in;
2558 assert!(retry > 0.0 && retry <= 60_000.0);
2559 assert_eq!(
2560 ctx.database
2561 .get_api_key_by_id(&id)
2562 .await
2563 .unwrap()
2564 .unwrap()
2565 .remaining,
2566 Some(1.0)
2567 );
2568 }
2569
2570 #[tokio::test]
2571 async fn session_header_selects_a_named_config_without_a_default() {
2572 let plugin = ApiKeyPlugin::builder()
2573 .config_id("machines".to_owned())
2574 .api_key_headers(vec![
2575 "x-machine-key".to_owned(),
2576 "x-alternate-key".to_owned(),
2577 ])
2578 .enable_session_for_api_keys(true)
2579 .build();
2580 let (ctx, user, session) = create_test_context_with_user().await;
2581 let (_, key) = create_key_and_get_raw(
2582 &plugin,
2583 &ctx,
2584 &session.token,
2585 serde_json::json!({ "configId": "machines" }),
2586 )
2587 .await;
2588 let mut request =
2589 create_auth_request(HttpMethod::Get, "/get-session", None, None, None);
2590 request
2591 .headers
2592 .insert("x-alternate-key".to_owned(), key.clone());
2593 let result = plugin
2594 .before_request(&request, &ctx)
2595 .await
2596 .unwrap()
2597 .unwrap();
2598 let BeforeRequestAction::Respond(response) = result else {
2599 panic!("Expected virtual session")
2600 };
2601 assert_eq!(response.status, 200);
2602 let body = json_body(&response);
2603 assert_eq!(
2604 (*(*(body).get("session").unwrap_or(&serde_json::Value::Null))
2605 .get("token")
2606 .unwrap_or(&serde_json::Value::Null)),
2607 key
2608 );
2609 assert_eq!(
2610 (*(*(body).get("session").unwrap_or(&serde_json::Value::Null))
2611 .get("userId")
2612 .unwrap_or(&serde_json::Value::Null)),
2613 user.id
2614 );
2615 assert_eq!(
2616 (*(*(body).get("user").unwrap_or(&serde_json::Value::Null))
2617 .get("emailVerified")
2618 .unwrap_or(&serde_json::Value::Null)),
2619 false
2620 );
2621 assert!(
2622 (*(*(body).get("session").unwrap_or(&serde_json::Value::Null))
2623 .get("createdAt")
2624 .unwrap_or(&serde_json::Value::Null))
2625 .is_string()
2626 );
2627 assert!(
2628 (*(*(body).get("session").unwrap_or(&serde_json::Value::Null))
2629 .get("expiresAt")
2630 .unwrap_or(&serde_json::Value::Null))
2631 .is_string()
2632 );
2633 }
2634
2635 #[tokio::test]
2636 async fn session_header_cannot_authenticate_a_different_configuration() {
2637 let plugin = ApiKeyPlugin::builder()
2638 .enable_session_for_api_keys(true)
2639 .build()
2640 .configuration(ApiKeyConfig {
2641 config_id: "machines".to_owned(),
2642 api_key_headers: vec!["x-machine-key".to_owned()],
2643 enable_session_for_api_keys: true,
2644 ..Default::default()
2645 });
2646 let (ctx, _, session) = create_test_context_with_user().await;
2647 let (id, key) = create_key_and_get_raw(
2648 &plugin,
2649 &ctx,
2650 &session.token,
2651 serde_json::json!({ "configId": "machines" }),
2652 )
2653 .await;
2654 let mut request =
2655 create_auth_request(HttpMethod::Get, "/get-session", None, None, None);
2656 request.headers.insert("x-api-key".to_owned(), key);
2657 let result = plugin
2658 .before_request(&request, &ctx)
2659 .await
2660 .unwrap()
2661 .unwrap();
2662 let BeforeRequestAction::Respond(response) = result else {
2663 panic!("Expected credential rejection")
2664 };
2665 assert_eq!(response.status, 401);
2666 assert_eq!(
2667 (*(json_body(&response))
2668 .get("code")
2669 .unwrap_or(&serde_json::Value::Null)),
2670 "INVALID_API_KEY"
2671 );
2672 assert_eq!(
2673 ctx.database
2674 .get_api_key_by_id(&id)
2675 .await
2676 .unwrap()
2677 .unwrap()
2678 .request_count,
2679 Some(0.0)
2680 );
2681 }
2682
2683 #[tokio::test]
2684 async fn organization_key_verifies_without_emulating_a_user_session() {
2685 let plugin = ApiKeyPlugin::builder()
2686 .references(ApiKeyReferences::Organization)
2687 .enable_session_for_api_keys(true)
2688 .build();
2689 let (ctx, user, _) = create_test_context_with_user().await;
2690 let (key, key_hash, start) =
2691 ApiKeyPlugin::generate_key(&ApiKeyConfig::default(), None).unwrap();
2692 ctx.database
2693 .create_api_key(alibi_core::CreateApiKey {
2694 reference_id: user.id,
2696 config_id: "default".to_owned(),
2697 key_hash,
2698 start: Some(start),
2699 enabled: true,
2700 name: None,
2701 prefix: None,
2702 expires_at: None,
2703 remaining: None,
2704 rate_limit_enabled: false,
2705 rate_limit_time_window: None,
2706 rate_limit_max: None,
2707 refill_interval: None,
2708 refill_amount: None,
2709 permissions: None,
2710 metadata: None,
2711 })
2712 .await
2713 .unwrap();
2714 plugin
2715 .verify_api_key(
2716 &VerifyApiKey {
2717 key: &key,
2718 config_id: None,
2719 permissions: None,
2720 },
2721 &ctx,
2722 )
2723 .await
2724 .unwrap();
2725 let mut request =
2726 create_auth_request(HttpMethod::Get, "/get-session", None, None, None);
2727 request.headers.insert("x-api-key".to_owned(), key);
2728 let result = plugin
2729 .before_request(&request, &ctx)
2730 .await
2731 .unwrap()
2732 .unwrap();
2733 let BeforeRequestAction::Respond(response) = result else {
2734 panic!("Expected organization rejection")
2735 };
2736 assert_eq!(response.status, 401);
2737 assert_eq!(
2738 (*(json_body(&response))
2739 .get("code")
2740 .unwrap_or(&serde_json::Value::Null)),
2741 "INVALID_REFERENCE_ID_FROM_API_KEY"
2742 );
2743 }
2744
2745 #[tokio::test]
2746 async fn api_key_initialization_rejects_ambiguous_configurations() {
2747 let (ctx, _, _) = create_test_context_with_user().await;
2748 for config_id in ["", "default"] {
2749 let plugin = ApiKeyPlugin::builder().build().configuration(ApiKeyConfig {
2750 config_id: config_id.to_owned(),
2751 ..Default::default()
2752 });
2753 let mut init = alibi_core::AuthInitContext::new(
2754 Arc::clone(&ctx.config),
2755 Arc::clone(&ctx.database),
2756 );
2757 assert!(matches!(
2758 plugin.on_init(&mut init).await,
2759 Err(AuthError::Config(_))
2760 ));
2761 }
2762 }
2763
2764 #[tokio::test]
2765 async fn verification_preserves_database_failures() {
2766 let connection = alibi_seaorm::Database::connect("sqlite::memory:")
2767 .await
2768 .unwrap();
2769 connection.clone().close().await.unwrap();
2770 let config = Arc::new(crate::test_helpers::create_test_config());
2771 let database = Arc::new(alibi_seaorm::SeaOrmStore::<TestSchema>::new(
2772 Arc::clone(&config),
2773 connection,
2774 ));
2775 let ctx = AuthContext::new(config, database);
2776 let plugin = ApiKeyPlugin::builder().build();
2777 let error = plugin
2778 .verify_api_key(
2779 &VerifyApiKey {
2780 key: "not-looked-up",
2781 config_id: None,
2782 permissions: None,
2783 },
2784 &ctx,
2785 )
2786 .await
2787 .unwrap_err();
2788 assert!(matches!(
2789 error,
2790 ApiKeyVerificationError::Internal(AuthError::Database(_))
2791 ));
2792 }
2793
2794 #[tokio::test]
2795 async fn verified_session_authenticates_a_protected_plugin_route_without_a_database_session()
2796 {
2797 let plugin = ApiKeyPlugin::builder()
2798 .enable_session_for_api_keys(true)
2799 .build();
2800 let (ctx, user, session) = create_test_context_with_user().await;
2801 let (id, key) =
2802 create_key_and_get_raw(&plugin, &ctx, &session.token, serde_json::json!({})).await;
2803 let mut request =
2804 create_auth_request(HttpMethod::Get, "/api-key/list", None, None, None);
2805 request.headers.insert("x-api-key".to_owned(), key.clone());
2806 let action = plugin
2807 .before_request(&request, &ctx)
2808 .await
2809 .unwrap()
2810 .unwrap();
2811 let BeforeRequestAction::InjectSession { session: session_2 } = action else {
2812 panic!("Expected virtual session")
2813 };
2814 assert_eq!(session_2.token, key);
2815 request.set_virtual_session(session_2);
2816 let response = plugin.on_request(&request, &ctx).await.unwrap().unwrap();
2817 assert_eq!(response.status, 200);
2818 assert_eq!(
2819 (*(*(*(json_body(&response))
2820 .get("apiKeys")
2821 .unwrap_or(&serde_json::Value::Null))
2822 .get(0)
2823 .unwrap_or(&serde_json::Value::Null))
2824 .get("id")
2825 .unwrap_or(&serde_json::Value::Null)),
2826 id
2827 );
2828 assert_eq!(
2829 ctx.database
2830 .get_user_sessions(&user.id)
2831 .await
2832 .unwrap()
2833 .len(),
2834 1
2835 );
2836 }
2837
2838 #[tokio::test]
2842 async fn programmatic_validator_uses_typed_policy_without_a_request() {
2843 use std::sync::atomic::{AtomicBool, Ordering};
2844 struct Policy(AtomicBool);
2845 struct Predicate {
2846 private_policy_secret: String,
2847 }
2848 #[async_trait::async_trait]
2849 impl ApiKeyValidator for Predicate {
2850 async fn validate(
2851 &self,
2852 context: &ApiKeyCallbackContext<'_>,
2853 _key: &str,
2854 ) -> AuthResult<bool> {
2855 Ok(context.request.is_none()
2856 && context.configuration_id == "programmatic"
2857 && context.auth_config.secret == "test-secret-key-at-least-32-chars-long"
2858 && self.private_policy_secret == "application-private-policy-secret"
2859 && context
2860 .extensions
2861 .get::<Policy>()
2862 .is_some_and(|policy| policy.0.load(Ordering::SeqCst)))
2863 }
2864 }
2865 let (mut ctx, user, _) = create_test_context_with_user().await;
2866 ctx.extensions.insert(Policy(AtomicBool::new(false)));
2867 let configuration = ApiKeyConfig {
2868 config_id: "programmatic".to_owned(),
2869 custom_api_key_validator: Some(Arc::new(Predicate {
2870 private_policy_secret: "application-private-policy-secret".to_owned(),
2871 })),
2872 rate_limit: RateLimitDefaults {
2873 enabled: false,
2874 ..Default::default()
2875 },
2876 ..Default::default()
2877 };
2878 assert!(!format!("{configuration:?}").contains("application-private-policy-secret"));
2879 let plugin = ApiKeyPlugin::with_config(configuration);
2880 let created = plugin
2881 .create_key(
2882 &ctx,
2883 &CreateKeyRequest {
2884 config_id: Some("programmatic".to_owned()),
2885 user_id: Some(user.id.clone()),
2886 remaining: Some(2.0),
2887 ..Default::default()
2888 },
2889 )
2890 .await
2891 .unwrap();
2892 let input = VerifyApiKey {
2893 key: &created.key,
2894 config_id: Some("programmatic"),
2895 permissions: None,
2896 };
2897 let rejected = plugin.verify_api_key(&input, &ctx).await.unwrap_err();
2898 assert!(matches!(
2899 rejected,
2900 ApiKeyVerificationError::Validation(ApiKeyValidationError {
2901 code: ApiKeyErrorCode::KeyNotFound,
2902 ..
2903 })
2904 ));
2905 assert_eq!(
2906 ctx.database
2907 .get_api_key_by_id(&created.api_key.id)
2908 .await
2909 .unwrap()
2910 .unwrap()
2911 .remaining,
2912 Some(2.0)
2913 );
2914 ctx.extensions
2915 .get::<Policy>()
2916 .unwrap()
2917 .0
2918 .store(true, Ordering::SeqCst);
2919 let accepted = plugin.verify_api_key(&input, &ctx).await.unwrap();
2920 assert_eq!(accepted.reference_id, user.id);
2921 assert_eq!(accepted.id, created.api_key.id);
2922 assert_eq!(accepted.remaining, Some(1.0));
2923 assert_eq!(
2924 ctx.database
2925 .get_api_key_by_id(&created.api_key.id)
2926 .await
2927 .unwrap()
2928 .unwrap()
2929 .remaining,
2930 Some(1.0)
2931 );
2932 }
2933 }
2934}
2935#[cfg(test)]
2939mod crud_tests {
2940 use super::*;
2941 use alibi_core::{AuthConfig, CreateSession, CreateUser, HttpMethod};
2942 use chrono::{Duration, Utc};
2943 use serde_json::json;
2944 use std::collections::HashMap;
2945 use std::sync::Arc;
2946
2947 type TestSchema = alibi_seaorm::store::__private_test_support::bundled_schema::BundledSchema;
2948
2949 async fn context() -> (AuthContext<TestSchema>, String, String) {
2950 let database = crate::test_helpers::create_test_database().await;
2951 let user = database
2952 .create_user(
2953 CreateUser::new()
2954 .with_email("crud@example.com")
2955 .with_name("CRUD user"),
2956 )
2957 .await
2958 .unwrap();
2959 let user_id = user.id().to_string();
2960 let session = database
2961 .create_session(CreateSession {
2962 additional_fields: alibi_core::field_policy::FieldValues::default(),
2963 token: None,
2964 active_team_id: None,
2965 user_id: user_id.clone(),
2966 expires_at: Utc::now() + Duration::hours(1),
2967 ip_address: None,
2968 user_agent: None,
2969 impersonated_by: None,
2970 active_organization_id: None,
2971 })
2972 .await
2973 .unwrap();
2974 let token = alibi_core::entity::AuthSession::token(&session).to_owned();
2975 (
2976 AuthContext::new(
2977 Arc::new(AuthConfig::new("a-secret-that-is-at-least-32-characters")),
2978 database,
2979 ),
2980 user_id,
2981 token,
2982 )
2983 }
2984
2985 fn request(token: &str, path: &str, body: &serde_json::Value) -> AuthRequest {
2986 AuthRequest::from_parts(
2987 HttpMethod::Post,
2988 path.to_owned(),
2989 HashMap::from([(
2990 "cookie".to_owned(),
2991 format!(
2992 "better-auth.session_token={}",
2993 alibi_core::utils::cookie_utils::sign_cookie_value(
2994 token,
2995 "a-secret-that-is-at-least-32-characters"
2996 )
2997 ),
2998 )]),
2999 Some(serde_json::to_vec(&body).unwrap()),
3000 HashMap::new(),
3001 )
3002 }
3003
3004 async fn server_key(
3005 plugin: &ApiKeyPlugin,
3006 ctx: &AuthContext<TestSchema>,
3007 user_id: &str,
3008 config_id: &str,
3009 ) -> CreateKeyResponse {
3010 plugin
3011 .create_key(
3012 ctx,
3013 &CreateKeyRequest {
3014 user_id: Some(user_id.to_owned()),
3015 config_id: Some(config_id.to_owned()),
3016 ..Default::default()
3017 },
3018 )
3019 .await
3020 .unwrap()
3021 }
3022
3023 #[tokio::test]
3024 async fn list_without_configuration_includes_all_user_configurations() {
3025 let (ctx, user_id, _) = context().await;
3026 let plugin = ApiKeyPlugin::builder().build().configuration(ApiKeyConfig {
3027 config_id: "secondary".to_owned(),
3028 ..Default::default()
3029 });
3030 server_key(&plugin, &ctx, &user_id, "default").await;
3031 server_key(&plugin, &ctx, &user_id, "secondary").await;
3032 let list = list_keys_core(&user_id, &ListKeysQuery::default(), &plugin, &ctx)
3033 .await
3034 .unwrap();
3035 assert_eq!(list.total, 2);
3036 let list_2 = list_keys_core(
3037 &user_id,
3038 &ListKeysQuery {
3039 config_id: Some("secondary".to_owned()),
3040 ..Default::default()
3041 },
3042 &plugin,
3043 &ctx,
3044 )
3045 .await
3046 .unwrap();
3047 assert_eq!(list_2.total, 1);
3048 assert_eq!(
3049 (list_2.api_keys)
3050 .first()
3051 .expect("fixture contains the requested index")
3052 .config_id,
3053 "secondary"
3054 );
3055 let list_3 = list_keys_core(
3056 &user_id,
3057 &ListKeysQuery {
3058 config_id: Some("missing".to_owned()),
3059 ..Default::default()
3060 },
3061 &plugin,
3062 &ctx,
3063 )
3064 .await
3065 .unwrap();
3066 assert_eq!(list_3.total, 0);
3067
3068 let no_default = ApiKeyPlugin::builder()
3069 .config_id("secondary".to_owned())
3070 .build();
3071 assert_eq!(
3072 list_keys_core(&user_id, &ListKeysQuery::default(), &no_default, &ctx)
3073 .await
3074 .unwrap()
3075 .total,
3076 2
3077 );
3078 }
3079
3080 #[tokio::test]
3081 async fn http_requests_cannot_impersonate_users_or_change_server_permissions() {
3082 let (ctx, user_id, token) = context().await;
3083 let plugin = ApiKeyPlugin::builder().build();
3084 let create = request(&token, "/api-key/create", &(json!({"userId":user_id})));
3085 assert_eq!(
3086 plugin
3087 .handle_create(&create, &ctx)
3088 .await
3089 .unwrap_err()
3090 .status_code(),
3091 401
3092 );
3093 let key = server_key(&plugin, &ctx, &user_id, "default").await;
3094 let update = request(
3095 &token,
3096 "/api-key/update",
3097 &(json!({"keyId":key.api_key.id,"userId":"someone-else","enabled":false})),
3098 );
3099 assert_eq!(
3100 plugin
3101 .handle_update(&update, &ctx)
3102 .await
3103 .unwrap_err()
3104 .status_code(),
3105 401
3106 );
3107 let update_2 = request(
3108 &token,
3109 "/api-key/update",
3110 &(json!({"keyId":key.api_key.id,"permissions":null})),
3111 );
3112 assert_eq!(
3113 plugin
3114 .handle_update(&update_2, &ctx)
3115 .await
3116 .unwrap_err()
3117 .to_string(),
3118 ApiKeyErrorCode::ServerOnlyProperty.message()
3119 );
3120 let update_3 = request(&token, "/api-key/update", &(json!({"keyId":"missing"})));
3121 assert_eq!(
3122 plugin
3123 .handle_update(&update_3, &ctx)
3124 .await
3125 .unwrap_err()
3126 .status_code(),
3127 404
3128 );
3129 }
3130
3131 #[tokio::test]
3132 async fn metadata_can_be_cleared_and_disabled_metadata_is_ignored_on_update() {
3133 let (ctx, user_id, token) = context().await;
3134 let enabled = ApiKeyPlugin::builder().enable_metadata(true).build();
3135 let key = enabled
3136 .create_key(
3137 &ctx,
3138 &CreateKeyRequest {
3139 user_id: Some(user_id.clone()),
3140 metadata: Some(json!(["initial"]).into()),
3141 ..Default::default()
3142 },
3143 )
3144 .await
3145 .unwrap();
3146 assert_eq!(key.api_key.metadata, Some(json!(["initial"])));
3147 let update = request(
3148 &token,
3149 "/api-key/update",
3150 &(json!({"keyId":key.api_key.id,"metadata":null})),
3151 );
3152 let response = enabled.handle_update(&update, &ctx).await.unwrap();
3153 assert!(
3154 (*(serde_json::from_slice::<serde_json::Value>(&response.body).unwrap())
3155 .get("metadata")
3156 .unwrap_or(&serde_json::Value::Null))
3157 .is_null()
3158 );
3159 let disabled = ApiKeyPlugin::builder().build();
3160 let update_2 = request(
3161 &token,
3162 "/api-key/update",
3163 &(json!({"keyId":key.api_key.id,"metadata":{"ignored":true},"enabled":false})),
3164 );
3165 let response_2 = disabled.handle_update(&update_2, &ctx).await.unwrap();
3166 let result: serde_json::Value = serde_json::from_slice(&response_2.body).unwrap();
3167 assert!((*(result).get("metadata").unwrap_or(&serde_json::Value::Null)).is_null());
3168 assert_eq!(
3169 (*(result).get("enabled").unwrap_or(&serde_json::Value::Null)),
3170 false
3171 );
3172 let update_3 = request(
3173 &token,
3174 "/api-key/update",
3175 &(json!({"keyId":key.api_key.id,"metadata":null})),
3176 );
3177 assert_eq!(
3178 disabled
3179 .handle_update(&update_3, &ctx)
3180 .await
3181 .unwrap_err()
3182 .to_string(),
3183 ApiKeyErrorCode::NoValuesToUpdate.message()
3184 );
3185 }
3186
3187 #[tokio::test]
3188 async fn trusted_creation_and_update_preserve_permissions_and_fractional_expiration() {
3189 let (ctx, user_id, _) = context().await;
3190 let plugin = ApiKeyPlugin::builder()
3191 .key_expiration(KeyExpirationConfig {
3192 min_expires_in: 0.0,
3193 ..Default::default()
3194 })
3195 .build();
3196 let before = Utc::now();
3197 let key = plugin
3198 .create_key(
3199 &ctx,
3200 &CreateKeyRequest {
3201 user_id: Some(user_id.clone()),
3202 expires_in: Some(86400.25),
3203 remaining: Some(3.0),
3204 permissions: Some(ApiKeyPermissions::from([(
3205 "device".to_owned(),
3206 vec!["read".to_owned()],
3207 )])),
3208 ..Default::default()
3209 },
3210 )
3211 .await
3212 .unwrap();
3213 assert_eq!(key.api_key.remaining, Some(3.0));
3214 assert_eq!(key.api_key.permissions, Some(json!({"device":["read"]})));
3215 let expires_at =
3216 chrono::DateTime::parse_from_rfc3339(key.api_key.expires_at.as_deref().unwrap())
3217 .unwrap()
3218 .with_timezone(&Utc);
3219 assert!((expires_at - before).num_milliseconds() >= 86_400_249);
3220 assert!((expires_at - Utc::now()).num_milliseconds() <= 86_400_250);
3221 let updated = plugin
3222 .update_key(
3223 &ctx,
3224 &UpdateKeyRequest {
3225 key_id: key.api_key.id.clone(),
3226 user_id: Some(user_id),
3227 remaining: Some(5.0),
3228 permissions: Some(None),
3229 expires_in: Some(None),
3230 ..Default::default()
3231 },
3232 )
3233 .await
3234 .unwrap();
3235 assert_eq!(updated.remaining, Some(5.0));
3236 assert_eq!(updated.permissions, Some(serde_json::Value::Null));
3237 assert_eq!(updated.expires_at, None);
3238 let stored = ctx
3239 .database
3240 .get_api_key_by_id(&key.api_key.id)
3241 .await
3242 .unwrap()
3243 .unwrap();
3244 assert_ne!(stored.key_hash, key.key);
3245 }
3246
3247 #[tokio::test]
3248 async fn list_rejects_invalid_pagination_instead_of_ignoring_it() {
3249 let (ctx, _, token) = context().await;
3250 let plugin = ApiKeyPlugin::builder().build();
3251 for (field, value) in [
3252 ("limit", "-1"),
3253 ("offset", "1.5"),
3254 ("limit", "bad"),
3255 ("sortDirection", "sideways"),
3256 ] {
3257 let mut req = request(&token, "/api-key/list", &(json!({})));
3258 req.query.insert(field.to_owned(), value.to_owned());
3259 let response = plugin.handle_list(&req, &ctx).await.unwrap();
3260 assert_eq!(response.status, 400);
3261 assert_eq!(
3262 (*(serde_json::from_slice::<serde_json::Value>(&response.body).unwrap())
3263 .get("code")
3264 .unwrap_or(&serde_json::Value::Null)),
3265 "VALIDATION_ERROR"
3266 );
3267 }
3268 }
3269
3270 #[tokio::test]
3271 async fn forced_cleanup_preserves_rows_on_store_failure_and_retries_without_throttle() {
3272 use alibi_seaorm::sea_orm::{ConnectionTrait, DbBackend, Statement};
3273 let config = Arc::new(AuthConfig::new("forced-cleanup-application-secret32"));
3274 let connection = alibi_seaorm::Database::connect("sqlite::memory:")
3275 .await
3276 .unwrap();
3277 alibi_seaorm::store::__private_test_support::migrator::run_migrations(&connection)
3278 .await
3279 .unwrap();
3280 let database = Arc::new(alibi_seaorm::SeaOrmStore::<TestSchema>::new(
3281 std::sync::Arc::clone(&config),
3282 connection.clone(),
3283 ));
3284 let ctx = AuthContext::new(config, database);
3285 let owner = ctx
3286 .database
3287 .create_user(CreateUser::new().with_email("cleanup@native.local"))
3288 .await
3289 .unwrap();
3290 let plugin = ApiKeyPlugin::with_config(ApiKeyConfig {
3291 key_expiration: KeyExpirationConfig {
3292 min_expires_in: 0.0,
3293 ..Default::default()
3294 },
3295 ..Default::default()
3296 });
3297 let key = plugin
3298 .create_key(
3299 &ctx,
3300 &CreateKeyRequest {
3301 user_id: Some(owner.id().to_string()),
3302 name: Some("expired".into()),
3303 ..Default::default()
3304 },
3305 )
3306 .await
3307 .unwrap();
3308 let expired = ctx
3309 .database
3310 .update_api_key(
3311 &key.api_key.id,
3312 alibi_core::UpdateApiKey {
3313 expires_at: Some(Some("1970-01-01T00:00:00.000Z".into())),
3314 ..Default::default()
3315 },
3316 )
3317 .await
3318 .unwrap();
3319 connection.execute_raw(Statement::from_string(DbBackend::Sqlite,"CREATE TRIGGER reject_api_key_cleanup BEFORE DELETE ON \"apikey\" BEGIN SELECT RAISE(ABORT,'application cleanup rejected'); END")).await.unwrap();
3320 let failed = plugin.delete_all_expired_api_keys(&ctx).await;
3321 assert!(failed.success);
3322 assert!(failed.error.is_none());
3323 let retained = ctx
3324 .database
3325 .get_api_key_by_id(&key.api_key.id)
3326 .await
3327 .unwrap()
3328 .unwrap();
3329 assert_eq!(
3330 alibi_core::utils::json::to_value(&retained).unwrap(),
3331 alibi_core::utils::json::to_value(&expired).unwrap()
3332 );
3333 connection
3334 .execute_raw(Statement::from_string(
3335 DbBackend::Sqlite,
3336 "DROP TRIGGER reject_api_key_cleanup",
3337 ))
3338 .await
3339 .unwrap();
3340 let retry = plugin.delete_all_expired_api_keys(&ctx).await;
3341 assert!(retry.success);
3342 assert!(retry.error.is_none());
3343 assert!(
3344 ctx.database
3345 .get_api_key_by_id(&key.api_key.id)
3346 .await
3347 .unwrap()
3348 .is_none()
3349 );
3350 assert_eq!(
3351 ctx.database
3352 .get_user_by_id(owner.id().as_ref())
3353 .await
3354 .unwrap()
3355 .unwrap(),
3356 owner
3357 );
3358 }
3359 struct ApplicationKeyStorage {
3360 cache: Arc<alibi_core::store::MemoryCacheAdapter>,
3361 fail_writes: std::sync::atomic::AtomicBool,
3362 }
3363 #[async_trait::async_trait]
3364 impl ApiKeyStorage for ApplicationKeyStorage {
3365 async fn get(&self, key: &str) -> AuthResult<Option<String>> {
3366 use alibi_core::store::CacheAdapter as _;
3367 self.cache.get(key).await
3368 }
3369 async fn set(&self, key: &str, value: &str, ttl: Option<Duration>) -> AuthResult<()> {
3370 use alibi_core::store::CacheAdapter as _;
3371 if self.fail_writes.load(std::sync::atomic::Ordering::SeqCst) {
3372 return Err(AuthError::internal("application storage unavailable"));
3373 }
3374 match ttl {
3375 Some(ttl) => self.cache.set(key, value, ttl).await,
3376 None => self.cache.set_without_expiry(key, value).await,
3377 }
3378 }
3379 async fn delete(&self, key: &str) -> AuthResult<()> {
3380 use alibi_core::store::CacheAdapter as _;
3381 self.cache.delete(key).await
3382 }
3383 }
3384 #[derive(Default)]
3385 struct ApplicationCompletions(std::sync::Mutex<Vec<alibi_core::BackgroundTaskCompletion>>);
3386 impl alibi_core::BackgroundTaskHandler for ApplicationCompletions {
3387 fn handle(&self, completion: alibi_core::BackgroundTaskCompletion) -> AuthResult<()> {
3388 self.0.lock().unwrap().push(completion);
3389 Ok(())
3390 }
3391 }
3392 impl ApplicationCompletions {
3393 async fn drain(&self) {
3394 let tasks = std::mem::take(&mut *self.0.lock().unwrap());
3395 for task in tasks {
3396 task.await.unwrap();
3397 }
3398 }
3399 }
3400
3401 #[tokio::test]
3402 async fn source_boxed_metadata_rows_list_successfully_without_mutating_storage() {
3403 use alibi_core::store::{CacheAdapter, MemoryCacheAdapter};
3404 let (ctx, owner, token) = context().await;
3405 let cache = Arc::new(MemoryCacheAdapter::new());
3406 let plugin = ApiKeyPlugin::with_config(ApiKeyConfig {
3407 storage: ApiKeyStorageMode::SecondaryStorage,
3408 secondary_storage: Some(cache.clone()),
3409 ..Default::default()
3410 });
3411 let mut rows = Vec::new();
3412 let mut expected = std::collections::BTreeMap::new();
3413 for index in 0..64 {
3418 let created = server_key(&plugin, &ctx, &owner, "default").await;
3419 let id = format!("api-key:by-id:{}", created.api_key.id);
3420 let hash = format!("api-key:{}", ApiKeyPlugin::hash_key(&created.key));
3421 let mut row: serde_json::Value =
3422 serde_json::from_str(&cache.get(&id).await.unwrap().unwrap()).unwrap();
3423 *row.get_mut("metadata").unwrap() = match index % 6 {
3424 0 => json!("10"),
3425 1 => json!("2"),
3426 2 => json!(3),
3427 3 => json!(null),
3428 4 => json!("0"),
3429 _ => json!({"literal":"object"}),
3430 };
3431 let value = serde_json::to_string(&row).unwrap();
3432 cache.set_without_expiry(&id, &value).await.unwrap();
3433 cache.set_without_expiry(&hash, &value).await.unwrap();
3434 rows.push((id, value.clone()));
3435 rows.push((hash, value));
3436 expected.insert(
3437 created.api_key.id,
3438 [
3439 json!(10),
3440 json!(2),
3441 json!(3),
3442 json!(null),
3443 json!(0),
3444 json!({"literal":"object"}),
3445 ]
3446 .get(index % 6)
3447 .unwrap()
3448 .clone(),
3449 );
3450 }
3451 let reference = format!("api-key:by-ref:{owner}");
3452 let reference_before = cache.get(&reference).await.unwrap();
3453 for direction in ["asc", "desc"] {
3454 let mut req = request(&token, "/api-key/list", &json!(null));
3455 req.method = HttpMethod::Get;
3456 req.body = None;
3457 req.query = HashMap::from([
3458 ("sortBy".to_owned(), "metadata".to_owned()),
3459 ("sortDirection".to_owned(), direction.to_owned()),
3460 ]);
3461 let response = plugin.handle_list(&req, &ctx).await.unwrap();
3462 assert_eq!(response.status, 200);
3463 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
3464 assert_eq!(body.get("total").unwrap(), 64);
3465 let actual: std::collections::BTreeMap<_, _> = body
3466 .get("apiKeys")
3467 .unwrap()
3468 .as_array()
3469 .unwrap()
3470 .iter()
3471 .map(|row| {
3472 (
3473 row.get("id").unwrap().as_str().unwrap().to_owned(),
3474 row.get("metadata").unwrap().clone(),
3475 )
3476 })
3477 .collect();
3478 assert_eq!(actual, expected);
3479 for (index, value) in &rows {
3480 assert_eq!(
3481 cache.get(index).await.unwrap().as_deref(),
3482 Some(value.as_str())
3483 );
3484 }
3485 assert_eq!(cache.get(&reference).await.unwrap(), reference_before);
3486 assert!(
3487 ctx.database
3488 .list_api_keys_by_reference(&owner)
3489 .await
3490 .unwrap()
3491 .is_empty()
3492 );
3493 }
3494 for _ in 64..96 {
3497 let created = server_key(&plugin, &ctx, &owner, "default").await;
3498 let id = format!("api-key:by-id:{}", created.api_key.id);
3499 let hash = format!("api-key:{}", ApiKeyPlugin::hash_key(&created.key));
3500 let value = cache.get(&id).await.unwrap().unwrap();
3501 rows.push((id, value.clone()));
3502 rows.push((hash, value));
3503 }
3504 let mut ordered = std::collections::BTreeMap::new();
3505 for (index, pair) in rows.as_chunks_mut::<2>().0.iter_mut().enumerate() {
3506 let rank = (index * 37) % 96;
3507 let mut row: serde_json::Value =
3508 serde_json::from_str(&pair.first().unwrap().1).unwrap();
3509 _ = ordered.insert(rank, row.get("id").unwrap().clone());
3510 *row.get_mut("metadata").unwrap() = json!(rank);
3511 let serialized = serde_json::to_string(&row).unwrap();
3512 for (key, value) in pair {
3513 cache.set_without_expiry(key, &serialized).await.unwrap();
3514 *value = serialized.clone();
3515 }
3516 }
3517 let reference_before = cache.get(&reference).await.unwrap();
3518 for direction in ["asc", "desc"] {
3519 let mut req = request(&token, "/api-key/list", &json!(null));
3520 req.method = HttpMethod::Get;
3521 req.body = None;
3522 req.query = HashMap::from([
3523 ("sortBy".into(), "metadata".into()),
3524 ("sortDirection".into(), direction.into()),
3525 ("offset".into(), "11".into()),
3526 ("limit".into(), "9".into()),
3527 ]);
3528 let response = plugin.handle_list(&req, &ctx).await.unwrap();
3529 assert_eq!(response.status, 200);
3530 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
3531 assert_eq!(body.get("total").unwrap(), 96);
3532 let page = body.get("apiKeys").unwrap().as_array().unwrap();
3533 assert_eq!(page.len(), 9);
3534 for (offset, row) in page.iter().enumerate() {
3535 let rank = if direction == "asc" {
3536 11 + offset
3537 } else {
3538 84 - offset
3539 };
3540 assert_eq!(row.get("id").unwrap(), ordered.get(&rank).unwrap());
3541 assert_eq!(row.get("metadata").unwrap(), &json!(rank));
3542 }
3543 for (key, value) in &rows {
3544 assert_eq!(
3545 cache.get(key).await.unwrap().as_deref(),
3546 Some(value.as_str())
3547 );
3548 }
3549 assert_eq!(cache.get(&reference).await.unwrap(), reference_before);
3550 }
3551 }
3552
3553 #[tokio::test]
3554 async fn application_storage_preserves_authority_indexes_and_usage() {
3555 use alibi_core::store::{CacheAdapter, MemoryCacheAdapter};
3556 for (fallback, custom, deferred) in [
3559 (false, false, false),
3560 (true, false, false),
3561 (false, true, false),
3562 (true, true, false),
3563 (false, true, true),
3564 (true, true, true),
3565 ] {
3566 let (mut ctx, owner, token) = context().await;
3567 let completions = Arc::new(ApplicationCompletions::default());
3568 Arc::make_mut(&mut ctx.config).background_tasks = Some(completions.clone());
3569 let outsider = ctx
3570 .database
3571 .create_user(
3572 CreateUser::new()
3573 .with_email("outsider@example.com")
3574 .with_name("outsider"),
3575 )
3576 .await
3577 .unwrap();
3578 let cache = Arc::new(MemoryCacheAdapter::new());
3579 let decoy = Arc::new(MemoryCacheAdapter::new());
3580 let application = Arc::new(ApplicationKeyStorage {
3581 cache: cache.clone(),
3582 fail_writes: std::sync::atomic::AtomicBool::new(false),
3583 });
3584 let plugin = ApiKeyPlugin::with_config(ApiKeyConfig {
3585 storage: ApiKeyStorageMode::SecondaryStorage,
3586 secondary_storage: Some(
3587 if custom { decoy.clone() } else { cache.clone() } as Arc<dyn CacheAdapter>
3588 ),
3589 custom_storage: custom.then(|| application.clone() as Arc<dyn ApiKeyStorage>),
3590 fallback_to_database: fallback,
3591 defer_updates: deferred,
3592 rate_limit: RateLimitDefaults {
3593 enabled: false,
3594 ..Default::default()
3595 },
3596 ..Default::default()
3597 });
3598 let response = plugin
3599 .handle_create(
3600 &request(&token, "/api-key/create", &json!({"name":"stored"})),
3601 &ctx,
3602 )
3603 .await
3604 .unwrap();
3605 let created: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
3606 let id = created.get("id").unwrap().as_str().unwrap();
3607 let raw = created.get("key").unwrap().as_str().unwrap();
3608 plugin
3609 .update_key(
3610 &ctx,
3611 &UpdateKeyRequest {
3612 key_id: id.to_owned(),
3613 user_id: Some(owner.clone()),
3614 remaining: Some(2.0),
3615 ..Default::default()
3616 },
3617 )
3618 .await
3619 .unwrap();
3620 let hash = ApiKeyPlugin::hash_key(raw);
3621 let by_id = format!("api-key:by-id:{id}");
3622 let by_hash = format!("api-key:{hash}");
3623 let by_ref = format!("api-key:by-ref:{owner}");
3624 let initial = cache.get(&by_id).await.unwrap().unwrap();
3625 assert_eq!(cache.get(&by_hash).await.unwrap(), Some(initial.clone()));
3626 assert_eq!(
3627 ctx.database.get_api_key_by_id(id).await.unwrap().is_some(),
3628 fallback
3629 );
3630 assert_eq!(cache.get(&by_ref).await.unwrap().is_none(), fallback);
3631 assert!(
3632 get_key_core(id, None, &outsider.id().to_string(), &plugin, &ctx)
3633 .await
3634 .is_err()
3635 );
3636 assert_eq!(cache.get(&by_id).await.unwrap(), Some(initial.clone()));
3637 assert!(
3638 plugin
3639 .verify_api_key(
3640 &VerifyApiKey {
3641 key: raw,
3642 config_id: Some("wrong"),
3643 permissions: None
3644 },
3645 &ctx
3646 )
3647 .await
3648 .is_err()
3649 );
3650 assert_eq!(cache.get(&by_id).await.unwrap(), Some(initial));
3651 let list = list_keys_core(&owner, &ListKeysQuery::default(), &plugin, &ctx)
3652 .await
3653 .unwrap();
3654 assert_eq!(list.total, 1);
3655 let input = VerifyApiKey {
3656 key: raw,
3657 config_id: None,
3658 permissions: None,
3659 };
3660 assert_eq!(
3661 plugin.verify_api_key(&input, &ctx).await.unwrap().remaining,
3662 Some(1.0)
3663 );
3664 completions.drain().await;
3665 assert!(decoy.get(&by_id).await.unwrap().is_none());
3666 let row: alibi_core::ApiKey =
3667 serde_json::from_str(&cache.get(&by_id).await.unwrap().unwrap()).unwrap();
3668 assert_eq!(row.remaining, Some(1.0));
3669 assert_eq!(row.reference_id, owner);
3670 assert_eq!(
3671 cache.get(&by_hash).await.unwrap(),
3672 cache.get(&by_id).await.unwrap()
3673 );
3674 if fallback {
3675 let persisted = ctx.database.get_api_key_by_id(id).await.unwrap().unwrap();
3676 let mut persisted_json = serde_json::to_value(&persisted).unwrap();
3677 *persisted_json.get_mut("metadata").unwrap() =
3680 serde_json::from_str(persisted.metadata.as_deref().unwrap_or("null")).unwrap();
3681 assert_eq!(persisted_json, serde_json::to_value(&row).unwrap());
3682 cache.delete(&by_id).await.unwrap();
3683 cache.delete(&by_hash).await.unwrap();
3684 assert_eq!(
3685 get_key_core(id, None, &owner, &plugin, &ctx)
3686 .await
3687 .unwrap()
3688 .remaining,
3689 Some(1.0)
3690 );
3691 assert_eq!(
3692 cache.get(&by_hash).await.unwrap(),
3693 cache.get(&by_id).await.unwrap()
3694 );
3695 }
3696 assert_eq!(
3697 plugin.verify_api_key(&input, &ctx).await.unwrap().remaining,
3698 Some(0.0)
3699 );
3700 completions.drain().await;
3701 assert!(plugin.verify_api_key(&input, &ctx).await.is_err());
3702 completions.drain().await;
3703 assert!(cache.get(&by_id).await.unwrap().is_none());
3704 assert!(cache.get(&by_hash).await.unwrap().is_none());
3705 assert!(cache.get(&by_ref).await.unwrap().is_none());
3706 assert!(ctx.database.get_api_key_by_id(id).await.unwrap().is_none());
3707 if custom {
3708 let target = plugin
3709 .create_key(
3710 &ctx,
3711 &CreateKeyRequest {
3712 user_id: Some(owner.clone()),
3713 remaining: Some(4.0),
3714 ..Default::default()
3715 },
3716 )
3717 .await
3718 .unwrap();
3719 let raw = target.key.as_str();
3720 let key_id = &target.api_key.id;
3721 let id_index = format!("api-key:by-id:{key_id}");
3722 let hash_index = format!("api-key:{}", ApiKeyPlugin::hash_key(raw));
3723 let before = cache.get(&id_index).await.unwrap();
3724 application
3725 .fail_writes
3726 .store(true, std::sync::atomic::Ordering::SeqCst);
3727 let input = VerifyApiKey {
3728 key: raw,
3729 config_id: None,
3730 permissions: None,
3731 };
3732 let failed = plugin.verify_api_key(&input, &ctx).await;
3733 assert_eq!(failed.is_ok(), deferred && !fallback);
3734 completions.drain().await;
3735 assert_eq!(cache.get(&id_index).await.unwrap(), before);
3736 assert_eq!(cache.get(&hash_index).await.unwrap(), before);
3737 if fallback {
3738 assert_eq!(
3739 ctx.database
3740 .get_api_key_by_id(key_id)
3741 .await
3742 .unwrap()
3743 .unwrap()
3744 .remaining,
3745 Some(3.0)
3746 );
3747 } else {
3748 assert!(
3749 ctx.database
3750 .get_api_key_by_id(key_id)
3751 .await
3752 .unwrap()
3753 .is_none()
3754 );
3755 }
3756 application
3757 .fail_writes
3758 .store(false, std::sync::atomic::Ordering::SeqCst);
3759 let retried = plugin.verify_api_key(&input, &ctx).await.unwrap();
3760 assert_eq!(retried.remaining, Some(if fallback { 2.0 } else { 3.0 }));
3761 completions.drain().await;
3762 assert_eq!(
3763 cache.get(&hash_index).await.unwrap(),
3764 cache.get(&id_index).await.unwrap()
3765 );
3766 let row: alibi_core::ApiKey =
3767 serde_json::from_str(&cache.get(&id_index).await.unwrap().unwrap()).unwrap();
3768 assert_eq!(row.remaining, retried.remaining);
3769 }
3770 }
3771 let (ctx, owner, _) = context().await;
3774 let cache = Arc::new(MemoryCacheAdapter::new());
3775 let plugin = ApiKeyPlugin::with_config(ApiKeyConfig {
3776 storage: ApiKeyStorageMode::SecondaryStorage,
3777 secondary_storage: Some(cache.clone()),
3778 ..Default::default()
3779 });
3780 let created = plugin
3781 .create_key(
3782 &ctx,
3783 &CreateKeyRequest {
3784 user_id: Some(owner.clone()),
3785 remaining: Some(2.0),
3786 rate_limit_enabled: Some(true),
3787 rate_limit_time_window: Some(60_000.0),
3788 rate_limit_max: Some(1.0),
3789 refill_interval: Some(60_000.0),
3790 refill_amount: Some(5.0),
3791 ..Default::default()
3792 },
3793 )
3794 .await
3795 .unwrap();
3796 let by_id = format!("api-key:by-id:{}", created.api_key.id);
3797 let by_hash = format!("api-key:{}", ApiKeyPlugin::hash_key(&created.key));
3798 let input = VerifyApiKey {
3799 key: &created.key,
3800 config_id: None,
3801 permissions: None,
3802 };
3803 let first = plugin.verify_api_key(&input, &ctx).await.unwrap();
3804 assert_eq!(first.remaining, Some(1.0));
3805 assert_eq!(first.request_count, Some(1.0));
3806 let before = cache.get(&by_id).await.unwrap();
3807 assert_eq!(cache.get(&by_hash).await.unwrap(), before);
3808 let denied = plugin.verify_api_key(&input, &ctx).await.unwrap_err();
3809 let ApiKeyVerificationError::Validation(denied) = denied else {
3810 panic!("Expected rate-limit validation");
3811 };
3812 assert_eq!(denied.code, ApiKeyErrorCode::RateLimited);
3813 assert!(denied.details.unwrap().try_again_in > 0.0);
3814 assert_eq!(cache.get(&by_id).await.unwrap(), before);
3815 assert_eq!(cache.get(&by_hash).await.unwrap(), before);
3816 let mut elapsed: alibi_core::ApiKey =
3817 serde_json::from_str(before.as_deref().unwrap()).unwrap();
3818 let previous = (chrono::Utc::now() - chrono::Duration::minutes(2)).to_rfc3339();
3819 elapsed.last_request = Some(previous.clone());
3820 elapsed.last_refill_at = Some(previous.clone());
3821 let seeded = serde_json::to_string(&elapsed).unwrap();
3822 cache.set_without_expiry(&by_id, &seeded).await.unwrap();
3823 cache.set_without_expiry(&by_hash, &seeded).await.unwrap();
3824 let refilled = plugin.verify_api_key(&input, &ctx).await.unwrap();
3825 assert_eq!(refilled.remaining, Some(4.0));
3826 assert_eq!(refilled.request_count, Some(1.0));
3827 assert_ne!(refilled.last_refill_at.as_deref(), Some(previous.as_str()));
3828 let observed: alibi_core::ApiKey =
3829 serde_json::from_str(&cache.get(&by_id).await.unwrap().unwrap()).unwrap();
3830 assert_eq!(observed.remaining, Some(4.0));
3831 assert_eq!(observed.request_count, Some(1.0));
3832 assert_eq!(
3833 cache.get(&by_id).await.unwrap(),
3834 cache.get(&by_hash).await.unwrap()
3835 );
3836 assert!(
3837 ctx.database
3838 .list_api_keys_by_reference(&owner)
3839 .await
3840 .unwrap()
3841 .is_empty()
3842 );
3843 }
3844}
3845