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