1use super::{
2 ApiKeyCallbackContext, ApiKeyErrorCode, ApiKeyErrorMessage, ApiKeyPlugin, ApiKeyReferences,
3 ApiKeyValidationError, ApiKeyVerificationError, CreateKeyRequest, CreateKeyResponse,
4 DeleteExpiredApiKeysResponse, DeleteKeyRequest, ListKeysQuery, ListKeysResponse,
5 UpdateKeyRequest, VerifyApiKey,
6};
7use crate::authentication_helpers::{JsonField, JsonFieldKind, field_issue, json_type};
8use crate::endpoint::{definition, validate_fields};
9use alibi_core::endpoint::{
10 BeforeEndpointAction, EndpointCall, EndpointContextPatch, EndpointDefinition, EndpointHook,
11 EndpointInput, EndpointResponse, ServerEndpoint,
12};
13use alibi_core::error::codes;
14use alibi_core::session::SessionRequest;
15use alibi_core::utils::json::JsValue;
16use alibi_core::wire::ApiKeyView;
17use alibi_core::{AuthContext, AuthError, AuthResult, AuthSchema, AuthUser, HttpMethod};
18use serde::{Deserialize, Serialize};
19
20#[serde_with::skip_serializing_none]
22#[derive(Debug, Serialize, Deserialize)]
23#[serde(rename_all = "camelCase")]
24pub struct ApiKeyVerificationInput {
25 pub key: String,
26 pub config_id: Option<String>,
27 pub permissions: Option<super::ApiKeyPermissions>,
28}
29
30#[derive(Debug, Deserialize)]
31pub struct ApiKeyVerificationOutput {
32 pub valid: bool,
33 pub error: Option<JsValue>,
34 pub key: Option<ApiKeyView>,
35}
36
37#[serde_with::skip_serializing_none]
39#[derive(Debug, Serialize, Deserialize)]
40#[serde(rename_all = "camelCase")]
41pub struct GetKeyQuery {
42 pub config_id: Option<String>,
43 pub id: String,
44}
45
46#[derive(Debug, Serialize, Deserialize)]
48pub struct DeleteKeyResponse {
49 pub success: bool,
50}
51
52pub(super) fn definitions() -> Vec<EndpointDefinition> {
53 vec![
54 definition("verifyApiKey", "verifyApiKey", None, HttpMethod::Post),
55 definition(
56 "deleteAllExpiredApiKeys",
57 "deleteAllExpiredApiKeys",
58 None,
59 HttpMethod::Post,
60 ),
61 definition(
62 "createApiKey",
63 "createApiKey",
64 Some("/api-key/create"),
65 HttpMethod::Post,
66 ),
67 definition(
68 "getApiKey",
69 "getApiKey",
70 Some("/api-key/get"),
71 HttpMethod::Get,
72 ),
73 definition(
74 "deleteApiKey",
75 "deleteApiKey",
76 Some("/api-key/delete"),
77 HttpMethod::Post,
78 ),
79 definition(
80 "listApiKeys",
81 "listApiKeys",
82 Some("/api-key/list"),
83 HttpMethod::Get,
84 ),
85 definition(
86 "updateApiKey",
87 "updateApiKey",
88 Some("/api-key/update"),
89 HttpMethod::Post,
90 ),
91 ]
92}
93
94pub(super) fn validate(call: &EndpointCall) -> AuthResult<EndpointInput> {
95 if call.operation_id() == "listApiKeys" {
96 return Ok(EndpointInput {
97 body: call.body().cloned(),
98 query: call.query().map(validate_list_query).transpose()?,
99 });
100 }
101 if call.operation_id() == "getApiKey" {
102 return Ok(EndpointInput {
103 body: call.body().cloned(),
104 query: Some(validate_fields(
105 call.query(),
106 "query",
107 &[
108 JsonField::string("configId", false),
109 JsonField::string("id", true),
110 ],
111 )?),
112 });
113 }
114 let body = match call.operation_id() {
115 "verifyApiKey" | "createApiKey" | "updateApiKey" | "deleteApiKey" => {
116 Some(validate_key_body(call.body(), call.operation_id())?)
117 }
118 _ => call.body().cloned(),
119 };
120 Ok(EndpointInput {
121 body,
122 query: call.query().cloned(),
123 })
124}
125
126fn validate_list_query(query: &JsValue) -> AuthResult<JsValue> {
127 let object = query.as_object().ok_or_else(|| {
128 crate::endpoint::validation(format!(
129 "[query] Invalid input: expected object, received {}",
130 json_type(Some(query)),
131 ))
132 })?;
133 let mut output = indexmap::IndexMap::new();
134 let mut issues = Vec::new();
135 for name in [
136 "configId",
137 "organizationId",
138 "limit",
139 "offset",
140 "sortBy",
141 "sortDirection",
142 ] {
143 let Some(value) = object.get(name) else {
144 continue;
145 };
146 let path = format!("[query.{name}]");
147 if matches!(name, "limit" | "offset") {
148 let number = match value {
149 JsValue::Null => Some(0.0),
150 JsValue::Bool(value) => Some(f64::from(*value)),
151 JsValue::Number(value) => Some(*value),
152 value => value
153 .coerce_string()
154 .ok()
155 .and_then(|value| alibi_core::utils::javascript::string_to_number(&value)),
156 }
157 .unwrap_or(f64::NAN);
158 if !number.is_finite() {
159 issues.push(format!(
160 "{path} Invalid input: expected number, received {}",
161 if number.is_nan() { "NaN" } else { "number" }
162 ));
163 } else if number.fract() != 0.0 {
164 issues.push(format!(
165 "{path} Invalid input: expected int, received number"
166 ));
167 } else {
168 if number > 9_007_199_254_740_991.0 {
169 issues.push(format!(
170 "{path} Too big: expected int to be <=9007199254740991"
171 ));
172 }
173 if number < -9_007_199_254_740_991.0 {
174 issues.push(format!(
175 "{path} Too small: expected int to be >=-9007199254740991"
176 ));
177 }
178 if number < 0.0 {
179 issues.push(format!("{path} Too small: expected number to be >=0"));
180 }
181 _ = output.insert(name.to_owned(), JsValue::Number(number));
182 }
183 } else {
184 let kind = if name == "sortDirection" {
185 JsonFieldKind::OneOf(&["asc", "desc"])
186 } else {
187 JsonFieldKind::String
188 };
189 if let Some(issue) = field_issue(kind, Some(value), json_type(Some(value))) {
190 issues.push(format!("{path} {issue}"));
191 } else {
192 _ = output.insert(name.to_owned(), value.clone());
193 }
194 }
195 }
196 if issues.is_empty() {
197 Ok(JsValue::Object(output))
198 } else {
199 Err(crate::endpoint::validation(issues.join("; ")))
200 }
201}
202
203#[derive(Clone, Copy)]
204enum KeyFieldKind {
205 String,
206 Prefix,
207 CoercedId,
208 Boolean,
209 Number(Option<f64>),
210 Permissions,
211 Any,
212}
213
214fn key_fields(operation: &str) -> &'static [(&'static str, KeyFieldKind, bool, bool)] {
215 use KeyFieldKind::{Any, Boolean, CoercedId, Number, Permissions, Prefix, String};
216 match operation {
217 "verifyApiKey" => &[
218 ("configId", String, false, false),
219 ("key", String, true, false),
220 ("permissions", Permissions, false, false),
221 ],
222 "deleteApiKey" => &[
223 ("configId", String, false, false),
224 ("keyId", String, true, false),
225 ],
226 "createApiKey" => &[
227 ("configId", String, false, false),
228 ("name", String, false, false),
229 ("expiresIn", Number(Some(1.0)), false, true),
230 ("prefix", Prefix, false, false),
231 ("remaining", Number(Some(0.0)), false, true),
232 ("metadata", Any, false, true),
233 ("refillAmount", Number(Some(1.0)), false, false),
234 ("refillInterval", Number(None), false, false),
235 ("rateLimitTimeWindow", Number(None), false, false),
236 ("rateLimitMax", Number(None), false, false),
237 ("rateLimitEnabled", Boolean, false, false),
238 ("permissions", Permissions, false, false),
239 ("userId", CoercedId, false, true),
240 ("organizationId", CoercedId, false, true),
241 ],
242 _ => &[
243 ("configId", String, false, false),
244 ("keyId", String, true, false),
245 ("userId", CoercedId, false, true),
246 ("name", String, false, false),
247 ("enabled", Boolean, false, false),
248 ("remaining", Number(Some(1.0)), false, false),
249 ("refillAmount", Number(None), false, false),
250 ("refillInterval", Number(None), false, false),
251 ("metadata", Any, false, true),
252 ("expiresIn", Number(Some(1.0)), false, true),
253 ("rateLimitEnabled", Boolean, false, false),
254 ("rateLimitTimeWindow", Number(None), false, false),
255 ("rateLimitMax", Number(None), false, false),
256 ("permissions", Permissions, false, true),
257 ],
258 }
259}
260
261fn validate_key_body(body: Option<&JsValue>, operation: &str) -> AuthResult<JsValue> {
264 use KeyFieldKind::{Any, Boolean, CoercedId, Number, Permissions, Prefix, String};
265 let fields = key_fields(operation);
266 let object = body.and_then(JsValue::as_object).ok_or_else(|| {
267 crate::endpoint::validation(format!(
268 "[body] Invalid input: expected object, received {}",
269 crate::authentication_helpers::json_type(body)
270 ))
271 })?;
272 let mut issues = Vec::new();
273 let mut output = indexmap::IndexMap::new();
274 for &(name, kind, required, nullable) in fields {
275 let value = object.get(name);
276 if value.is_none() && !required {
277 if operation == "createApiKey" && matches!(name, "expiresIn" | "remaining") {
278 _ = output.insert(name.to_owned(), JsValue::Null);
279 }
280 continue;
281 }
282 let path = format!("body.{name}");
283 if nullable && value.is_some_and(JsValue::is_null) && !matches!(kind, CoercedId) {
284 _ = output.insert(name.to_owned(), JsValue::Null);
285 continue;
286 }
287 if matches!(kind, CoercedId) {
288 if let Some(value) = value {
289 match value.coerce_string() {
290 Ok(value) => {
291 _ = output.insert(name.to_owned(), JsValue::String(value));
292 }
293 Err(_) => issues.push(format!(
294 "[{path}] Invalid input: expected string, received {}",
295 crate::authentication_helpers::json_type(Some(value))
296 )),
297 }
298 }
299 continue;
300 }
301 let expected = match kind {
302 String | Prefix | CoercedId => "string",
303 Boolean => "boolean",
304 Number(_) => "number",
305 Permissions => "record",
306 Any => "any",
307 };
308 let valid = value.is_some_and(|value| match kind {
309 String | Prefix => value.is_string(),
310 Boolean => value.is_boolean(),
311 Number(_) => value.as_f64().is_some_and(f64::is_finite),
312 Permissions => value.is_object(),
313 Any => true,
314 CoercedId => false,
315 });
316 if !valid {
317 issues.push(format!(
318 "[{path}] Invalid input: expected {expected}, received {}",
319 key_field_type(value)
320 ));
321 continue;
322 }
323 let value = value.ok_or_else(|| crate::endpoint::validation("Missing validated input"))?;
324 match kind {
325 Number(Some(minimum)) if value.as_f64().is_some_and(|number|number<minimum)=>issues.push(format!("[{path}] Too small: expected number to be >={minimum}")),
326 Prefix if value.as_str().is_some_and(|prefix|prefix.is_empty()||!prefix.bytes().all(|byte|byte.is_ascii_alphanumeric()||matches!(byte,b'_'|b'-')))=>issues.push(format!("[{path}] Invalid prefix format, must be alphanumeric and contain only underscores and hyphens.")),
327 Permissions => validate_permissions(value,&path,&mut issues),
328 _ => {}
329 }
330 _ = output.insert(name.to_owned(), value.clone());
331 }
332 if issues.is_empty() {
333 Ok(JsValue::Object(output))
334 } else {
335 Err(crate::endpoint::validation(issues.join("; ")))
336 }
337}
338
339fn validate_permissions(value: &JsValue, path: &str, issues: &mut Vec<String>) {
340 if let Some(resources) = value.as_object() {
341 for (resource, actions) in resources {
342 let path = format!("{path}.{resource}");
343 if let Some(actions) = actions.as_array() {
344 for (index, action) in actions.iter().enumerate() {
345 if !action.is_string() {
346 issues.push(format!(
347 "[{path}.{index}] Invalid input: expected string, received {}",
348 key_field_type(Some(action))
349 ));
350 }
351 }
352 } else {
353 issues.push(format!(
354 "[{path}] Invalid input: expected array, received {}",
355 key_field_type(Some(actions))
356 ));
357 }
358 }
359 }
360}
361
362fn key_field_type(value: Option<&JsValue>) -> &'static str {
363 if value.and_then(JsValue::as_f64).is_some_and(f64::is_nan) {
364 "NaN"
365 } else {
366 crate::authentication_helpers::json_type(value)
367 }
368}
369
370impl ApiKeyPlugin {
371 pub fn verify_endpoint(
375 input: &ApiKeyVerificationInput,
376 ) -> AuthResult<ServerEndpoint<ApiKeyVerificationOutput>> {
377 ServerEndpoint::new("api-key", "verifyApiKey").with_body(input)
378 }
379
380 #[must_use]
381 pub const fn delete_all_expired_endpoint() -> ServerEndpoint<DeleteExpiredApiKeysResponse> {
382 ServerEndpoint::new("api-key", "deleteAllExpiredApiKeys")
383 }
384
385 pub fn create_endpoint(
389 body: &CreateKeyRequest,
390 ) -> AuthResult<ServerEndpoint<CreateKeyResponse>> {
391 ServerEndpoint::new("api-key", "createApiKey").with_body(body)
392 }
393
394 pub fn update_endpoint(body: &UpdateKeyRequest) -> AuthResult<ServerEndpoint<ApiKeyView>> {
398 ServerEndpoint::new("api-key", "updateApiKey").with_body(body)
399 }
400
401 pub fn get_endpoint(query: &GetKeyQuery) -> AuthResult<ServerEndpoint<ApiKeyView>> {
405 ServerEndpoint::new("api-key", "getApiKey").with_query(query)
406 }
407
408 pub fn delete_endpoint(
412 body: &DeleteKeyRequest,
413 ) -> AuthResult<ServerEndpoint<DeleteKeyResponse>> {
414 ServerEndpoint::new("api-key", "deleteApiKey").with_body(body)
415 }
416
417 pub fn list_endpoint(query: &ListKeysQuery) -> AuthResult<ServerEndpoint<ListKeysResponse>> {
421 ServerEndpoint::new("api-key", "listApiKeys").with_query(query)
422 }
423
424 pub(super) async fn call_endpoint<S: AuthSchema>(
425 &self,
426 call: &EndpointCall,
427 ctx: &AuthContext<S>,
428 ) -> AuthResult<EndpointResponse> {
429 match call.operation_id() {
430 "listApiKeys" => {
431 let query: ListKeysQuery = if call.query().is_some() {
432 call.query_as()?
433 } else {
434 ListKeysQuery::default()
435 };
436 let (user, _) = ctx.require_cached_session(call).await.map_err(|error| {
437 if matches!(
438 error,
439 AuthError::Unauthenticated | AuthError::SessionNotFound
440 ) {
441 AuthError::Api {
442 status: 401,
443 code: Some(codes::UNAUTHORIZED.into()),
444 message: "Unauthorized".into(),
445 }
446 } else {
447 error
448 }
449 })?;
450 EndpointResponse::json(
451 &super::handlers::list_keys_core(user.id(), &query, self, ctx).await?,
452 )
453 }
454 "deleteApiKey" => {
455 let body: DeleteKeyRequest = call.body_as()?;
456 let (user, _) = ctx.require_cached_session(call).await.map_err(|error| {
457 if matches!(
458 error,
459 AuthError::Unauthenticated | AuthError::SessionNotFound
460 ) {
461 AuthError::Api {
462 status: 401,
463 code: Some(codes::UNAUTHORIZED.into()),
464 message: "Unauthorized".into(),
465 }
466 } else {
467 error
468 }
469 })?;
470 if user.banned() {
471 return Err(AuthError::authentication_failed("User is banned"));
472 }
473 EndpointResponse::json(
474 &super::handlers::delete_key_core(&body, user.id(), self, ctx).await?,
475 )
476 }
477 "getApiKey" => {
478 let query: GetKeyQuery = call.query_as()?;
479 let (user, _) = ctx.require_cached_session(call).await.map_err(|error| {
480 if matches!(
481 error,
482 AuthError::Unauthenticated | AuthError::SessionNotFound
483 ) {
484 AuthError::Api {
485 status: 401,
486 code: Some(codes::UNAUTHORIZED.into()),
487 message: "Unauthorized".into(),
488 }
489 } else {
490 error
491 }
492 })?;
493 EndpointResponse::json(
494 &super::handlers::get_key_core(
495 &query.id,
496 query.config_id.as_deref(),
497 user.id(),
498 self,
499 ctx,
500 )
501 .await?,
502 )
503 }
504 "verifyApiKey" => {
505 let body: ApiKeyVerificationInput = call.body_as()?;
506 let permissions = body
507 .permissions
508 .as_ref()
509 .map(serde_json::to_value)
510 .transpose()?;
511 let input = VerifyApiKey {
512 key: &body.key,
513 config_id: body.config_id.as_deref(),
514 permissions: permissions.as_ref(),
515 };
516 let value = match self
517 .verify_api_key_with_registration(&input, call.request(), ctx)
518 .await
519 {
520 Ok(key) => serde_json::json!({"valid":true,"error":null,"key":key}),
521 Err(ApiKeyVerificationError::Validation(error)) => {
522 serde_json::json!({"valid":false,"error":error,"key":null})
523 }
524 Err(ApiKeyVerificationError::ExplicitValidator(error)) => return Err(error),
525 Err(ApiKeyVerificationError::Internal(error)) => {
526 tracing::error!(%error,"Failed to validate API key");
527 if alibi_core::endpoint::is_endpoint_api_error(&error) {
528 let (_, code, message) = error.error_payload();
529 serde_json::json!({"valid":false,"error":{"code":code,"message":message},"key":null})
530 } else {
531 serde_json::json!({"valid":false,"error":{"code":codes::INVALID_API_KEY,"message":{"code":codes::INVALID_API_KEY,"message":super::ApiKeyErrorCode::InvalidApiKey.message()}},"key":null})
532 }
533 }
534 };
535 EndpointResponse::json(&value)
536 }
537 "deleteAllExpiredApiKeys" => {
538 EndpointResponse::json(&self.delete_all_expired_api_keys(ctx).await)
539 }
540 "createApiKey" => {
541 let body: CreateKeyRequest = call.body_as()?;
542 let config = self.resolve_configuration(body.config_id.as_deref())?;
543 let mut resolution = call.clone();
544 alibi_core::endpoint::EndpointContextPatch {
545 query: Some(JsValue::Object(
546 [("disableCookieCache".into(), JsValue::Bool(true))]
547 .into_iter()
548 .collect(),
549 )),
550 ..alibi_core::endpoint::EndpointContextPatch::default()
551 }
552 .apply(&mut resolution);
553 let session = ctx.require_cached_session(&resolution).await.ok();
554 if let Some((user, session)) = &session {
555 call.record_authenticated_session(
556 match user {
557 alibi_core::AuthenticatedUser::Stored(user) => ctx.user_view(user),
558 alibi_core::AuthenticatedUser::Cached(user) => (**user).clone(),
559 },
560 session.clone(),
561 );
562 }
563 let is_client = call.request().is_some() || call.headers().is_some();
564 if is_client
565 && (body.refill_amount.is_some()
566 || body.refill_interval.is_some()
567 || body.rate_limit_max.is_some()
568 || body.rate_limit_time_window.is_some()
569 || body.rate_limit_enabled.is_some()
570 || body.permissions.is_some()
571 || body.remaining.is_some())
572 {
573 return Err(super::api_key_error(ApiKeyErrorCode::ServerOnlyProperty));
574 }
575 if call.request().is_some() && body.user_id.is_some() {
576 return Err(super::api_key_error(ApiKeyErrorCode::UnauthorizedSession));
577 }
578 let user_id = if config.references == ApiKeyReferences::Organization {
579 session
580 .as_ref()
581 .map(|(user, _)| user.id().into_owned())
582 .or_else(|| body.user_id.clone())
583 } else if is_client {
584 session.as_ref().map(|(user, _)| user.id().into_owned())
585 } else {
586 if let Some((user, _)) = &session
587 && body
588 .user_id
589 .as_ref()
590 .is_some_and(|id| !id.is_empty() && user.id() != *id)
591 {
592 return Err(super::api_key_error(ApiKeyErrorCode::UnauthorizedSession));
593 }
594 session
595 .as_ref()
596 .map(|(user, _)| user.id().into_owned())
597 .or_else(|| body.user_id.clone())
598 }
599 .filter(|id| !id.is_empty())
600 .ok_or_else(|| super::api_key_error(ApiKeyErrorCode::UnauthorizedSession))?;
601 EndpointResponse::json(
602 &super::handlers::create_key_for_user(
603 &body,
604 &user_id,
605 self,
606 ctx,
607 call.request(),
608 )
609 .await?,
610 )
611 }
612 "updateApiKey" => {
613 let body: UpdateKeyRequest = call.body_as()?;
614 let mut resolution = call.clone();
615 alibi_core::endpoint::EndpointContextPatch {
616 query: Some(JsValue::Object(
617 [("disableCookieCache".into(), JsValue::Bool(true))]
618 .into_iter()
619 .collect(),
620 )),
621 ..alibi_core::endpoint::EndpointContextPatch::default()
622 }
623 .apply(&mut resolution);
624 let session = ctx.require_cached_session(&resolution).await.ok();
625 if let Some((user, session)) = &session {
626 call.record_authenticated_session(
627 match user {
628 alibi_core::AuthenticatedUser::Stored(user) => ctx.user_view(user),
629 alibi_core::AuthenticatedUser::Cached(user) => (**user).clone(),
630 },
631 session.clone(),
632 );
633 }
634 let is_client = call.request().is_some() || call.headers().is_some();
635 let user_id = session
636 .as_ref()
637 .map(|(user, _)| user.id().into_owned())
638 .or_else(|| (!is_client).then(|| body.user_id.clone()).flatten())
639 .filter(|id| !id.is_empty())
640 .ok_or_else(|| super::api_key_error(ApiKeyErrorCode::UnauthorizedSession))?;
641 if body
642 .user_id
643 .as_ref()
644 .is_some_and(|id| !id.is_empty() && *id != user_id)
645 {
646 return Err(super::api_key_error(ApiKeyErrorCode::UnauthorizedSession));
647 }
648 if is_client {
649 EndpointResponse::json(
650 &super::handlers::update_key_core(&body, &user_id, self, ctx).await?,
651 )
652 } else {
653 EndpointResponse::json(
654 &super::handlers::update_key_for_user(&body, &user_id, self, ctx).await?,
655 )
656 }
657 }
658 _ => Err(AuthError::not_found("Unregistered API key operation")),
659 }
660 }
661}
662
663#[async_trait::async_trait]
664impl<S: AuthSchema> EndpointHook<S> for ApiKeyPlugin {
665 fn matches_before(&self, call: &EndpointCall, ctx: &AuthContext<S>) -> AuthResult<bool> {
666 Ok(self
667 .find_session_key_for_input(call.session_headers(), call.request(), Some(call), ctx)?
668 .is_some())
669 }
670
671 async fn before(
672 &self,
673 call: &EndpointCall,
674 ctx: &AuthContext<S>,
675 ) -> AuthResult<Option<BeforeEndpointAction>> {
676 let (config, key) = self
677 .find_session_key_for_input(call.session_headers(), call.request(), Some(call), ctx)?
678 .ok_or_else(|| {
679 AuthError::internal("API key getter did not return a key after matching")
680 })?;
681 if f64::from(
682 u32::try_from(key.encode_utf16().count())
683 .map_err(|error| AuthError::internal(error.to_string()))?,
684 ) < config.key_length
685 {
686 return Ok(Some(BeforeEndpointAction::Reject(rejection(
687 &ApiKeyValidationError::new(ApiKeyErrorCode::InvalidApiKey),
688 Some(403),
689 )?)));
690 }
691 if let Some(validator) = &config.custom_api_key_validator
692 && !validator
693 .validate(
694 &ApiKeyCallbackContext::new(call.request(), ctx, &config.config_id)
695 .with_endpoint(call),
696 &key,
697 )
698 .await?
699 {
700 return Ok(Some(BeforeEndpointAction::Reject(rejection(
701 &ApiKeyValidationError::new(ApiKeyErrorCode::InvalidApiKey),
702 Some(403),
703 )?)));
704 }
705 let input = VerifyApiKey {
706 key: &key,
707 config_id: Some(&config.config_id),
708 permissions: None,
709 };
710 let view = match self
711 .verify_api_key_checked(&input, call.request(), ctx, false)
712 .await
713 {
714 Ok(view) => view,
715 Err(ApiKeyVerificationError::Validation(error)) => {
716 return Ok(Some(BeforeEndpointAction::Reject(rejection(&error, None)?)));
717 }
718 Err(
719 ApiKeyVerificationError::Internal(error)
720 | ApiKeyVerificationError::ExplicitValidator(error),
721 ) => return Err(error),
722 };
723 if config.defer_updates {
724 self.register_expired_cleanup(ctx).await?;
725 } else if let Err(error) = self.maybe_delete_expired(ctx).await {
726 tracing::error!(%error,"Failed to delete expired API keys");
727 }
728 if config.references != ApiKeyReferences::User {
729 return Ok(Some(BeforeEndpointAction::Reject(rejection(
730 &ApiKeyValidationError::new(ApiKeyErrorCode::InvalidReferenceIdFromApiKey),
731 None,
732 )?)));
733 }
734 let Some(user) = ctx.database.get_user_by_id(&view.reference_id).await? else {
735 return Ok(Some(BeforeEndpointAction::Reject(rejection(
736 &ApiKeyValidationError::new(ApiKeyErrorCode::InvalidReferenceIdFromApiKey),
737 None,
738 )?)));
739 };
740 let session = Self::virtual_session_from_key(&view, &key, &user, call.request(), ctx)?;
741 let user_view = ctx.user_view(&user);
742 call.establish_session::<S>(user, user_view.clone(), session.clone(), ctx);
743 if call.path() == Some("/get-session") {
744 return Ok(Some(BeforeEndpointAction::Respond(EndpointResponse::json(
745 &serde_json::json!({"user":user_view,"session":session}),
746 )?)));
747 }
748 Ok(Some(BeforeEndpointAction::Patch(Box::new(
749 EndpointContextPatch {
750 path: call.path().map(str::to_owned),
751 ..EndpointContextPatch::default()
752 },
753 ))))
754 }
755}
756
757fn rejection(error: &ApiKeyValidationError, status: Option<u16>) -> AuthResult<EndpointResponse> {
758 let status = status.unwrap_or_else(|| error.status());
759 let message = match &error.message {
760 ApiKeyErrorMessage::Text(message) | ApiKeyErrorMessage::CodeMessage { message, .. } => {
761 message.clone()
762 }
763 };
764 let body = alibi_core::utils::json::parse_value(&alibi_core::utils::json::to_string(error)?)?;
765 Ok(EndpointResponse::error(AuthError::Api {
766 status,
767 code: Some(error.code.as_str().into()),
768 message,
769 })
770 .with_error_body(body))
771}