Skip to main content

alibi_plugins/helpers/
mod.rs

1//! Shared helpers for plugin implementations.
2mod api_key_authorization;
3mod credentials;
4mod sessions;
5
6use alibi_core::error::codes;
7use alibi_core::{AuthContext, AuthError, CreateUser};
8pub use api_key_authorization::{get_owned_api_key, require_org_api_key_permission};
9pub(crate) use credentials::{ResetProof, reset_credential_password};
10pub use credentials::{get_credential_account, get_credential_password_hash, user_has_password};
11pub use sessions::{
12    admin_banned_user_message, admin_plugin_enabled, delete_session_cookie_headers,
13    expires_in_to_at, get_cookie, issue_user_session, issue_user_session_record,
14    issue_user_session_with_fields, issue_user_session_with_fields_record,
15    issue_user_session_with_overrides, issue_user_session_with_overrides_record, response_session,
16};
17pub(crate) use sessions::{
18    completed_response_session, create_user_session_record,
19    issue_selected_user_session_in_transaction, issue_selected_user_session_record,
20    ordinary_session, publish_committed_session, record_completed_session,
21    record_completed_session_record, record_completed_session_user_view,
22    response_has_session_cookie,
23};
24
25/// Join the configured auth origin and mount path for links sent to users.
26pub(crate) fn auth_base_url(config: &alibi_core::AuthConfig) -> String {
27    let origin = config.base_url.trim_end_matches('/');
28    let path = config.base_path.trim_matches('/');
29    if path.is_empty() {
30        origin.to_owned()
31    } else {
32        format!("{origin}/{path}")
33    }
34}
35
36/// Wrap an ordinary application callback failure as an empty HTTP 500; explicit
37/// API errors keep their public status and body.
38pub(crate) fn callback_failure(error: AuthError) -> AuthError {
39    if matches!(
40        error,
41        AuthError::Api { .. } | AuthError::Upstream { .. } | AuthError::CallbackFailure(_)
42    ) {
43        error
44    } else {
45        AuthError::CallbackFailure(Box::new(error))
46    }
47}
48
49/// Run detached work that keeps the caller's endpoint and request-hook contexts,
50/// so it can outlive an aggregate that rejects early.
51pub(crate) fn spawn_in_request_context(work: impl Future<Output = ()> + Send + 'static) {
52    let endpoint = alibi_core::endpoint::current_endpoint_call_context();
53    let hook = alibi_core::hooks::current_request_hook_context();
54    _ = tokio::spawn(async move {
55        let work = alibi_core::hooks::with_optional_request_hook_context(hook, work);
56        if let Some(endpoint) = endpoint {
57            alibi_core::endpoint::with_endpoint_call_context(endpoint, work).await;
58        } else {
59            work.await;
60        }
61    });
62}
63
64/// The documented 401 for a request without an authenticated session.
65pub(crate) const fn unauthorized() -> AuthError {
66    AuthError::Upstream {
67        status: 401,
68        code: codes::UNAUTHORIZED,
69        message: "Unauthorized",
70    }
71}
72
73/// Map a missing session to the documented 401, passing other errors through.
74pub(crate) fn unauthorized_if_unauthenticated(error: AuthError) -> AuthError {
75    if matches!(error, AuthError::Unauthenticated) {
76        unauthorized()
77    } else {
78        error
79    }
80}
81
82/// Like [`unauthorized_if_unauthenticated`], also for a session or user that
83/// no longer exists.
84pub(crate) fn unauthorized_if_session_missing(error: AuthError) -> AuthError {
85    if matches!(
86        error,
87        AuthError::Unauthenticated | AuthError::SessionNotFound | AuthError::UserNotFound
88    ) {
89        unauthorized()
90    } else {
91        error
92    }
93}
94
95/// Result of issuing a real session for a user.
96pub struct IssuedSession<S: alibi_core::AuthSchema> {
97    pub user: S::User,
98    pub session: S::Session,
99}
100
101/// Actual session issuance with retained adapter output for framework callbacks
102/// and separately filtered public responses.
103pub struct IssuedSessionRecord<S: alibi_core::AuthSchema> {
104    pub user: alibi_core::AdapterRecord<S::User>,
105    pub session: alibi_core::AdapterRecord<S::Session>,
106}
107impl<S: alibi_core::AuthSchema> IssuedSessionRecord<S> {
108    fn into_stored(self) -> IssuedSession<S> {
109        IssuedSession {
110            user: self.user.into_stored(),
111            session: self.session.into_stored(),
112        }
113    }
114}
115
116/// Original rows used by the handler that issued the completed session.
117/// This is a callback observation; authorization still uses a current session read.
118pub(crate) struct CompletedSession<S: alibi_core::AuthSchema> {
119    pub(crate) user: S::User,
120    pub(crate) session: S::Session,
121    pub(crate) user_view: Option<alibi_core::wire::UserView>,
122    user_record: Option<alibi_core::AdapterRecord<S::User>>,
123    session_record: Option<alibi_core::AdapterRecord<S::Session>>,
124}
125impl<S: alibi_core::AuthSchema> CompletedSession<S> {
126    pub(crate) fn callback_user(&self, ctx: &AuthContext<S>) -> alibi_core::UserView {
127        self.user_view.clone().unwrap_or_else(|| {
128            self.user_record.as_ref().map_or_else(
129                || ctx.trusted_user_view(&self.user),
130                |record| ctx.trusted_user_view(record),
131            )
132        })
133    }
134    pub(crate) fn callback_session(&self, ctx: &AuthContext<S>) -> alibi_core::SessionView {
135        self.session_record.as_ref().map_or_else(
136            || ctx.trusted_session_view(&self.session),
137            |record| ctx.trusted_session_view(record),
138        )
139    }
140}
141
142/// Session issuance failures that callers may need to surface differently from
143/// a generic auth error (for example OAuth callback redirects).
144#[derive(Debug)]
145pub enum SessionIssueError {
146    Auth(AuthError),
147    Banned { message: String },
148}
149
150impl SessionIssueError {
151    #[must_use]
152    pub fn into_auth_error(self) -> AuthError {
153        match self {
154            Self::Auth(error) => error,
155            Self::Banned { message } => AuthError::banned_user(message),
156        }
157    }
158
159    #[must_use]
160    pub const fn banned_message(&self) -> Option<&str> {
161        match self {
162            Self::Banned { message } => Some(message.as_str()),
163            Self::Auth(_) => None,
164        }
165    }
166}
167
168impl From<AuthError> for SessionIssueError {
169    fn from(value: AuthError) -> Self {
170        Self::Auth(value)
171    }
172}
173
174/// Trusted fields for the initial session insert in a custom sign-in flow.
175///
176/// These values pass through configured field policies and session creation
177/// hooks. Use only after authenticating the user and authorizing the requested
178/// organization, team, or impersonation; issuance does not check membership.
179/// The issuer owns the session token, user ID, and expiry.
180#[derive(Clone, Debug, Default)]
181pub struct SessionOverrides {
182    /// Application-defined session fields declared by the schema and configuration.
183    pub additional_fields: alibi_core::field_policy::FieldValues,
184    /// The trusted administrator responsible for impersonation, if any.
185    pub impersonated_by: Option<String>,
186    /// The organization selected by the application for this session.
187    pub active_organization_id: Option<String>,
188    /// The team selected by the application for this session.
189    pub active_team_id: Option<String>,
190}
191
192impl<S: alibi_core::AuthSchema> std::fmt::Debug for IssuedSession<S> {
193    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194        f.debug_struct("IssuedSession").finish_non_exhaustive()
195    }
196}
197
198/// Apply the configured default admin role to a new user when the caller
199/// didn't set an explicit role.
200pub fn apply_default_role(
201    ctx: &AuthContext<impl alibi_core::AuthSchema>,
202    create_user: &mut CreateUser,
203) {
204    // The registered admin create transform runs after identity validation.
205    // Preserve that ordering for policy-enabled instances.
206    if ctx.config.user_validation.is_some() || create_user.role.is_some() {
207        return;
208    }
209
210    if let Some(default_role) = ctx
211        .get_metadata("admin.default_role")
212        .and_then(|value| value.as_str())
213    {
214        create_user.role = Some(default_role.to_owned());
215    }
216}