Skip to main content

alibi_plugins/api_key/
endpoint.rs

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/// Input to the registered server-only verification operation.
21#[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/// Query for reading one API key through the authenticated server API.
38#[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/// Result of deleting an API key through the authenticated server API.
47#[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
261// Registered schemas validate after accumulated before-hook patches. Preserve
262// Source's schema order and all nested issues before invoking the typed core.
263fn 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    /// Verify and consume usage through the installed logical endpoint pipeline.
372    /// # Errors
373    /// Returns an error if input serialization fails.
374    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    /// Create a key through installed hooks and actual server/client context policy.
386    /// # Errors
387    /// Returns an error if input serialization fails.
388    pub fn create_endpoint(
389        body: &CreateKeyRequest,
390    ) -> AuthResult<ServerEndpoint<CreateKeyResponse>> {
391        ServerEndpoint::new("api-key", "createApiKey").with_body(body)
392    }
393
394    /// Update through installed hooks and owner checks after patched-body validation.
395    /// # Errors
396    /// Returns an error if input serialization fails.
397    pub fn update_endpoint(body: &UpdateKeyRequest) -> AuthResult<ServerEndpoint<ApiKeyView>> {
398        ServerEndpoint::new("api-key", "updateApiKey").with_body(body)
399    }
400
401    /// Read an owned key through installed hooks without exposing its secret.
402    /// # Errors
403    /// Returns an error if query serialization fails.
404    pub fn get_endpoint(query: &GetKeyQuery) -> AuthResult<ServerEndpoint<ApiKeyView>> {
405        ServerEndpoint::new("api-key", "getApiKey").with_query(query)
406    }
407
408    /// Delete an owned key through installed hooks and authenticated policy.
409    /// # Errors
410    /// Returns an error if body serialization fails.
411    pub fn delete_endpoint(
412        body: &DeleteKeyRequest,
413    ) -> AuthResult<ServerEndpoint<DeleteKeyResponse>> {
414        ServerEndpoint::new("api-key", "deleteApiKey").with_body(body)
415    }
416
417    /// List visible keys through installed hooks and authenticated ownership policy.
418    /// # Errors
419    /// Returns an error if query serialization fails.
420    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}