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