Skip to main content

alibi_api/plugins/helpers/
mod.rs

1//! Shared helpers for plugin implementations.
2//!
3//! Extracted to avoid duplicating common patterns across plugins (DRY).
4mod api_key_authorization;
5mod credentials;
6mod sessions;
7
8use alibi_core::entity::{AuthAccount, AuthUser};
9use alibi_core::{AuthContext, AuthError, AuthRequest, AuthResult, CreateUser, UpdateUser};
10pub use api_key_authorization::get_owned_api_key;
11pub use api_key_authorization::require_org_api_key_permission;
12pub use credentials::get_credential_account;
13pub use credentials::get_credential_password_hash;
14pub use credentials::user_has_password;
15pub use sessions::admin_banned_user_message;
16pub use sessions::admin_plugin_enabled;
17pub(in crate::plugins) use sessions::completed_response_session;
18pub(in crate::plugins) use sessions::create_user_session_record;
19pub use sessions::delete_session_cookie_headers;
20pub use sessions::expires_in_to_at;
21pub use sessions::get_cookie;
22pub(in crate::plugins) use sessions::issue_selected_user_session_record;
23pub use sessions::issue_user_session;
24pub use sessions::issue_user_session_record;
25pub use sessions::issue_user_session_with_fields;
26pub use sessions::issue_user_session_with_fields_record;
27pub use sessions::issue_user_session_with_overrides;
28pub use sessions::issue_user_session_with_overrides_record;
29pub(in crate::plugins) use sessions::ordinary_session;
30pub(in crate::plugins) use sessions::record_completed_session;
31pub(in crate::plugins) use sessions::record_completed_session_record;
32pub(in crate::plugins) use sessions::record_completed_session_user_view;
33pub(in crate::plugins) use sessions::response_has_session_cookie;
34pub use sessions::response_session;
35
36/// Join the configured auth origin and mount path for links sent to users.
37pub(in crate::plugins) fn auth_base_url(config: &alibi_core::AuthConfig) -> String {
38    let origin = config.base_url.trim_end_matches('/');
39    let path = config.base_path.trim_matches('/');
40    if path.is_empty() {
41        origin.to_owned()
42    } else {
43        format!("{origin}/{path}")
44    }
45}
46use chrono::Utc;
47
48/// Result of issuing a real session for a user.
49pub struct IssuedSession<S: alibi_core::AuthSchema> {
50    pub user: S::User,
51    pub session: S::Session,
52}
53
54/// Actual session issuance with retained adapter output for framework callbacks
55/// and separately filtered public responses.
56pub struct IssuedSessionRecord<S: alibi_core::AuthSchema> {
57    pub user: alibi_core::AdapterRecord<S::User>,
58    pub session: alibi_core::AdapterRecord<S::Session>,
59}
60impl<S: alibi_core::AuthSchema> IssuedSessionRecord<S> {
61    fn into_stored(self) -> IssuedSession<S> {
62        IssuedSession {
63            user: self.user.into_stored(),
64            session: self.session.into_stored(),
65        }
66    }
67}
68
69/// Original rows used by the handler that issued the completed session.
70/// This is a callback observation; authorization still uses a current session read.
71pub(in crate::plugins) struct CompletedSession<S: alibi_core::AuthSchema> {
72    pub(in crate::plugins) user: S::User,
73    pub(in crate::plugins) session: S::Session,
74    pub(in crate::plugins) user_view: Option<alibi_core::wire::UserView>,
75    user_record: Option<alibi_core::AdapterRecord<S::User>>,
76    session_record: Option<alibi_core::AdapterRecord<S::Session>>,
77}
78impl<S: alibi_core::AuthSchema> CompletedSession<S> {
79    pub(in crate::plugins) fn callback_user(&self, ctx: &AuthContext<S>) -> alibi_core::UserView {
80        self.user_view.clone().unwrap_or_else(|| {
81            self.user_record.as_ref().map_or_else(
82                || ctx.trusted_user_view(&self.user),
83                |record| ctx.trusted_user_view(record),
84            )
85        })
86    }
87    pub(in crate::plugins) fn callback_session(
88        &self,
89        ctx: &AuthContext<S>,
90    ) -> alibi_core::SessionView {
91        self.session_record.as_ref().map_or_else(
92            || ctx.trusted_session_view(&self.session),
93            |record| ctx.trusted_session_view(record),
94        )
95    }
96}
97
98/// Session issuance failures that callers may need to surface differently from
99/// a generic auth error (for example OAuth callback redirects).
100#[derive(Debug)]
101pub enum SessionIssueError {
102    Auth(AuthError),
103    Banned { message: String },
104}
105
106impl SessionIssueError {
107    #[must_use]
108    pub fn into_auth_error(self) -> AuthError {
109        match self {
110            Self::Auth(error) => error,
111            Self::Banned { message } => AuthError::banned_user(message),
112        }
113    }
114
115    #[must_use]
116    pub const fn banned_message(&self) -> Option<&str> {
117        match self {
118            Self::Banned { message } => Some(message.as_str()),
119            Self::Auth(_) => None,
120        }
121    }
122}
123
124impl From<AuthError> for SessionIssueError {
125    fn from(value: AuthError) -> Self {
126        Self::Auth(value)
127    }
128}
129
130/// Trusted fields for the initial session insert in a custom sign-in flow.
131///
132/// These values pass through configured field policies and session creation
133/// hooks. Use only after authenticating the user and authorizing the requested
134/// organization, team, or impersonation; issuance does not check membership.
135/// The issuer owns the session token, user ID, and expiry.
136#[derive(Clone, Debug, Default)]
137pub struct SessionOverrides {
138    /// Application-defined session fields declared by the schema and configuration.
139    pub additional_fields: alibi_core::field_policy::FieldValues,
140    /// The trusted administrator responsible for impersonation, if any.
141    pub impersonated_by: Option<String>,
142    /// The organization selected by the application for this session.
143    pub active_organization_id: Option<String>,
144    /// The team selected by the application for this session.
145    pub active_team_id: Option<String>,
146}
147
148impl<S: alibi_core::AuthSchema> std::fmt::Debug for IssuedSession<S> {
149    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150        f.debug_struct("IssuedSession").finish_non_exhaustive()
151    }
152}
153
154/// Apply the configured default admin role to a new user when the caller
155/// didn't set an explicit role.
156pub fn apply_default_role(
157    ctx: &AuthContext<impl alibi_core::AuthSchema>,
158    create_user: &mut CreateUser,
159) {
160    // The registered admin create transform runs after identity validation.
161    // Preserve that ordering for policy-enabled instances.
162    if ctx.config.user_validation.is_some() || create_user.role.is_some() {
163        return;
164    }
165
166    if let Some(default_role) = ctx
167        .get_metadata("admin.default_role")
168        .and_then(|value| value.as_str())
169    {
170        create_user.role = Some(default_role.to_owned());
171    }
172}