Skip to main content

alibi_core/store/contracts/
session.rs

1use crate::field_policy::FieldValues;
2use crate::{AdapterRecord, AuthSession};
3use crate::{AuthError, AuthResult, AuthSchema, CreateSession};
4use async_trait::async_trait;
5use std::collections::BTreeSet;
6#[async_trait]
7pub trait SessionStore<S: AuthSchema>: Send + Sync {
8    /// Stage a secondary session through creation hooks and optional physical persistence.
9    /// The caller publishes the cache before completing its after hooks.
10    async fn prepare_secondary_session_creation(
11        &self,
12        _input: CreateSession,
13        _persist: bool,
14    ) -> AuthResult<S::Session> {
15        Err(AuthError::NotImplemented(
16            "Secondary session creation is unsupported".into(),
17        ))
18    }
19    /// Complete creation hooks after the actual secondary writes succeed.
20    async fn complete_secondary_session_creation(&self, _session: &S::Session) -> AuthResult<()> {
21        Err(AuthError::NotImplemented(
22            "Secondary session creation is unsupported".into(),
23        ))
24    }
25    /// Bind a trusted secondary update once, retaining hook-mutated fields for physical publication.
26    async fn prepare_secondary_session_update(
27        &self,
28        _session: S::Session,
29        _expires_at: Option<chrono::DateTime<chrono::Utc>>,
30        _fields: FieldValues,
31    ) -> AuthResult<Option<(S::Session, FieldValues)>> {
32        Err(AuthError::NotImplemented(
33            "Secondary session updates are unsupported".into(),
34        ))
35    }
36    /// Publish the staged physical update and run after hooks exactly once.
37    async fn complete_secondary_session_update(
38        &self,
39        _session: S::Session,
40        _expires_at: Option<chrono::DateTime<chrono::Utc>>,
41        _fields: FieldValues,
42        _persist: bool,
43    ) -> AuthResult<Option<S::Session>> {
44        Err(AuthError::NotImplemented(
45            "Secondary session updates are unsupported".into(),
46        ))
47    }
48    /// End a still-live physical session without deleting its audit row.
49    /// Returns false when no live row matched or a delete hook vetoed the end.
50    async fn end_session_preserving(&self, _token: &str) -> AuthResult<bool> {
51        Err(AuthError::NotImplemented(
52            "Preserving ended session rows is unsupported".into(),
53        ))
54    }
55    /// End all live session rows for one owner while retaining audit history.
56    /// Returns false when a delete hook vetoed the bulk mutation.
57    async fn end_user_sessions_preserving(&self, _user_id: &str) -> AuthResult<bool> {
58        Err(AuthError::NotImplemented(
59            "Preserving ended session rows is unsupported".into(),
60        ))
61    }
62    /// A typed user snapshot belonging to an authenticated session. The default
63    /// signals that the caller must use its physical user store.
64    async fn get_session_user(&self, _token: &str) -> AuthResult<Option<S::User>> {
65        Ok(None)
66    }
67
68    /// Retain declared adapter output for a secondary-backed session owner.
69    async fn get_session_user_record(
70        &self,
71        token: &str,
72    ) -> AuthResult<Option<AdapterRecord<S::User>>> {
73        self.get_session_user(token)
74            .await?
75            .map(AdapterRecord::physical)
76            .transpose()
77    }
78
79    /// Return a retained adapter record. The default is the physical model's
80    /// serialized snapshot; initialized stores apply their declared output policy.
81    async fn create_session_record(
82        &self,
83        create_session: CreateSession,
84    ) -> AuthResult<AdapterRecord<S::Session>> {
85        AdapterRecord::physical(self.create_session(create_session).await?)
86    }
87
88    /// Return a retained adapter record. The default is the physical model's
89    /// serialized snapshot; initialized stores apply their declared output policy.
90    async fn get_session_record(
91        &self,
92        token: &str,
93    ) -> AuthResult<Option<AdapterRecord<S::Session>>> {
94        self.get_session(token)
95            .await?
96            .map(AdapterRecord::physical)
97            .transpose()
98    }
99
100    /// Return a retained adapter record. The default is the physical model's
101    /// serialized snapshot; initialized stores apply their declared output policy.
102    async fn get_sessions_by_tokens_record(
103        &self,
104        tokens: &[String],
105    ) -> AuthResult<Vec<AdapterRecord<S::Session>>> {
106        self.get_sessions_by_tokens(tokens)
107            .await?
108            .into_iter()
109            .map(AdapterRecord::physical)
110            .collect()
111    }
112
113    /// Return a retained adapter record. The default is the physical model's
114    /// serialized snapshot; initialized stores apply their declared output policy.
115    async fn get_user_sessions_record(
116        &self,
117        user_id: &str,
118    ) -> AuthResult<Vec<AdapterRecord<S::Session>>> {
119        self.get_user_sessions(user_id)
120            .await?
121            .into_iter()
122            .map(AdapterRecord::physical)
123            .collect()
124    }
125
126    /// Return active physical sessions after declared output transforms. Expiry
127    /// selection runs before application callbacks, without trusting output fields.
128    async fn get_active_user_sessions_record(
129        &self,
130        user_id: &str,
131    ) -> AuthResult<Vec<AdapterRecord<S::Session>>> {
132        let now = chrono::Utc::now();
133        self.get_user_sessions(user_id)
134            .await?
135            .into_iter()
136            .filter(|session| session.expires_at() > now)
137            .map(AdapterRecord::physical)
138            .collect()
139    }
140
141    /// Return a retained adapter record. The default is the physical model's
142    /// serialized snapshot; initialized stores apply their declared output policy.
143    async fn refresh_session_record(
144        &self,
145        token: &str,
146        expires_at: chrono::DateTime<chrono::Utc>,
147    ) -> AuthResult<Option<AdapterRecord<S::Session>>> {
148        self.refresh_session(token, expires_at)
149            .await?
150            .map(AdapterRecord::physical)
151            .transpose()
152    }
153
154    /// Return a retained adapter record. The default is the physical model's
155    /// serialized snapshot; initialized stores apply their declared output policy.
156    async fn update_session_fields_record(
157        &self,
158        token: &str,
159        fields: FieldValues,
160    ) -> AuthResult<Option<AdapterRecord<S::Session>>> {
161        self.update_session_fields(token, fields)
162            .await?
163            .map(AdapterRecord::physical)
164            .transpose()
165    }
166
167    /// Return a retained adapter record. The default is the physical model's
168    /// serialized snapshot; initialized stores apply their declared output policy.
169    async fn update_session_active_organization_record(
170        &self,
171        token: &str,
172        organization_id: Option<&str>,
173    ) -> AuthResult<AdapterRecord<S::Session>> {
174        AdapterRecord::physical(
175            self.update_session_active_organization(token, organization_id)
176                .await?,
177        )
178    }
179
180    /// Return a retained adapter record. The default is the physical model's
181    /// serialized snapshot; initialized stores apply their declared output policy.
182    async fn update_session_active_team_record(
183        &self,
184        token: &str,
185        team_id: Option<&str>,
186    ) -> AuthResult<AdapterRecord<S::Session>> {
187        AdapterRecord::physical(self.update_session_active_team(token, team_id).await?)
188    }
189
190    /// Persist already authorized fields for the currently authenticated token.
191    async fn update_session_fields(
192        &self,
193        _token: &str,
194        _fields: FieldValues,
195    ) -> AuthResult<Option<S::Session>> {
196        Err(AuthError::internal(
197            "the store does not support session field updates",
198        ))
199    }
200
201    async fn create_session(&self, create_session: CreateSession) -> AuthResult<S::Session>;
202
203    async fn get_session(&self, token: &str) -> AuthResult<Option<S::Session>>;
204    /// Read the physical session row by its ID for signed-token liveness.
205    async fn get_session_by_id(&self, _id: &str) -> AuthResult<Option<S::Session>> {
206        Err(AuthError::NotImplemented(
207            "Session ID lookup is unsupported".into(),
208        ))
209    }
210
211    /// Fetch matching sessions once each, including expired rows. The bundled
212    /// SQLite adapter returns token-index order rather than request order.
213    async fn get_sessions_by_tokens(&self, tokens: &[String]) -> AuthResult<Vec<S::Session>> {
214        let mut sessions = Vec::new();
215        for token in BTreeSet::from_iter(tokens) {
216            if let Some(session) = self.get_session(token).await? {
217                sessions.push(session);
218            }
219        }
220        Ok(sessions)
221    }
222
223    async fn get_user_sessions(&self, user_id: &str) -> AuthResult<Vec<S::Session>>;
224
225    async fn update_session_expiry(
226        &self,
227        token: &str,
228        expires_at: chrono::DateTime<chrono::Utc>,
229    ) -> AuthResult<()>;
230
231    /// Refresh expiry and configured fields in one physical update. Adapters
232    /// supporting additional values must override this operation so their actual
233    /// before hooks precede binding and their after hooks see the final row.
234    /// The fallback preserves plain refresh and fails closed on additional writes.
235    async fn refresh_session_with_fields(
236        &self,
237        token: &str,
238        expires_at: chrono::DateTime<chrono::Utc>,
239        mut fields: FieldValues,
240    ) -> AuthResult<Option<S::Session>> {
241        fields.apply_adapter_transforms_async().await?;
242        if !fields.is_empty() {
243            return Err(AuthError::NotImplemented(
244                "The store does not support refresh field updates".into(),
245            ));
246        }
247        self.refresh_session(token, expires_at).await
248    }
249
250    /// Refresh the persisted expiry and return the updated snapshot. A session
251    /// removed before the update returns `None`, never its old credentials.
252    async fn refresh_session(
253        &self,
254        token: &str,
255        expires_at: chrono::DateTime<chrono::Utc>,
256    ) -> AuthResult<Option<S::Session>> {
257        match self.update_session_expiry(token, expires_at).await {
258            Ok(()) => self.get_session(token).await,
259            Err(AuthError::SessionNotFound) => Ok(None),
260            Err(error) => Err(error),
261        }
262    }
263
264    async fn delete_session(&self, token: &str) -> AuthResult<()>;
265
266    async fn delete_user_sessions(&self, user_id: &str) -> AuthResult<()>;
267
268    async fn delete_expired_sessions(&self) -> AuthResult<usize>;
269
270    async fn update_session_active_organization(
271        &self,
272        token: &str,
273        organization_id: Option<&str>,
274    ) -> AuthResult<S::Session>;
275
276    async fn update_session_active_team(
277        &self,
278        _token: &str,
279        _team_id: Option<&str>,
280    ) -> AuthResult<S::Session> {
281        Err(AuthError::internal(
282            "active-team updates are not supported by this store",
283        ))
284    }
285}