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