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