Skip to main content

alibi_api/plugins/admin/
callbacks.rs

1//! Application messages for banned users, resolved from the stored user entity.
2use alibi_core::{AuthError, AuthResult, entity::AuthUser};
3use async_trait::async_trait;
4use std::{
5    any::{Any, TypeId},
6    fmt,
7    sync::Arc,
8};
9
10/// Resolves the message returned when a nonexpired banned user requests a session.
11///
12/// The user is the stored application entity, including fields hidden from HTTP
13/// responses. This callback is awaited before any new session is created. Errors
14/// retain their status. Returning [`AuthError::Internal`] represents an ordinary
15/// application failure: HTTP callers receive an empty 500 response, matching
16/// the upstream callback exception contract. Explicit API errors retain their
17/// public body, including when their status is 500.
18#[async_trait]
19pub trait AdminBannedUserMessage<U: AuthUser>: Send + Sync + 'static {
20    async fn message(&self, user: &U) -> AuthResult<String>;
21}
22
23#[async_trait]
24trait ErasedMessage: Send + Sync {
25    fn user_type(&self) -> TypeId;
26    async fn message(&self, user: &(dyn Any + Send + Sync)) -> AuthResult<String>;
27}
28struct TypedMessage<U, H> {
29    handler: H,
30    marker: std::marker::PhantomData<fn() -> U>,
31}
32#[async_trait]
33impl<U: AuthUser, H: AdminBannedUserMessage<U>> ErasedMessage for TypedMessage<U, H> {
34    fn user_type(&self) -> TypeId {
35        TypeId::of::<U>()
36    }
37    async fn message(&self, user: &(dyn Any + Send + Sync)) -> AuthResult<String> {
38        let user = user.downcast_ref::<U>().or_else(|| user.downcast_ref::<alibi_core::AdapterRecord<U>>().map(alibi_core::AdapterRecord::stored)).ok_or_else(|| AuthError::config(
39            "Admin banned-user message callback user type does not match the authentication schema"
40        ))?;
41        self.handler
42            .message(user)
43            .await
44            .map_err(|error| match error {
45                AuthError::Internal(_) => AuthError::CallbackFailure(Box::new(error)),
46                other => other,
47            })
48    }
49}
50
51/// Cloneable, type-checked application callback configuration.
52#[derive(Clone)]
53pub struct AdminBannedUserMessageHandler(Arc<dyn ErasedMessage>);
54impl fmt::Debug for AdminBannedUserMessageHandler {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        f.write_str("AdminBannedUserMessageHandler(..)")
57    }
58}
59impl AdminBannedUserMessageHandler {
60    /// Stores a callback for the exact user entity of the application's schema.
61    #[must_use]
62    pub fn new<U: AuthUser, H: AdminBannedUserMessage<U>>(handler: H) -> Self {
63        Self(Arc::new(TypedMessage::<U, H> {
64            handler,
65            marker: std::marker::PhantomData,
66        }))
67    }
68    ///
69    /// # Errors
70    /// Returns an error when validation, storage, or an application callback fails.
71    pub(super) fn validate<U: AuthUser>(&self) -> AuthResult<()> {
72        if self.0.user_type() != TypeId::of::<U>()
73            && self.0.user_type() != TypeId::of::<alibi_core::AdapterRecord<U>>()
74        {
75            return Err(AuthError::config(
76                "Admin banned-user message callback user type does not match the authentication schema",
77            ));
78        }
79        Ok(())
80    }
81    ///
82    /// # Errors
83    /// Returns an error when validation, storage, or an application callback fails.
84    pub(super) async fn message<U: AuthUser>(&self, user: &U) -> AuthResult<String> {
85        self.0.message(user).await
86    }
87}
88
89#[derive(Clone)]
90pub(in crate::plugins) struct BannedUserMessagePolicy(pub(super) AdminBannedUserMessageHandler);
91impl BannedUserMessagePolicy {
92    ///
93    /// # Errors
94    /// Returns an error when validation, storage, or an application callback fails.
95    pub(in crate::plugins) async fn message<U: AuthUser>(&self, user: &U) -> AuthResult<String> {
96        self.0.message(user).await
97    }
98}