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    async fn end_session_preserving(&self, _token: &str) -> AuthResult<()> {
50        Err(AuthError::NotImplemented(
51            "Preserving ended session rows is unsupported".into(),
52        ))
53    }
54    /// End all live session rows for one owner while retaining audit history.
55    async fn end_user_sessions_preserving(&self, _user_id: &str) -> AuthResult<()> {
56        Err(AuthError::NotImplemented(
57            "Preserving ended session rows is unsupported".into(),
58        ))
59    }
60    /// A typed user snapshot belonging to an authenticated session. The default
61    /// signals that the caller must use its physical user store.
62    async fn get_session_user(&self, _token: &str) -> AuthResult<Option<S::User>> {
63        Ok(None)
64    }
65
66    /// Retain declared adapter output for a secondary-backed session owner.
67    async fn get_session_user_record(
68        &self,
69        token: &str,
70    ) -> AuthResult<Option<AdapterRecord<S::User>>> {
71        self.get_session_user(token)
72            .await?
73            .map(AdapterRecord::physical)
74            .transpose()
75    }
76
77    /// Return a retained adapter record. The default is the physical model's
78    /// serialized snapshot; initialized stores apply their declared output policy.
79    async fn create_session_record(
80        &self,
81        create_session: CreateSession,
82    ) -> AuthResult<AdapterRecord<S::Session>> {
83        AdapterRecord::physical(self.create_session(create_session).await?)
84    }
85
86    /// Return a retained adapter record. The default is the physical model's
87    /// serialized snapshot; initialized stores apply their declared output policy.
88    async fn get_session_record(
89        &self,
90        token: &str,
91    ) -> AuthResult<Option<AdapterRecord<S::Session>>> {
92        self.get_session(token)
93            .await?
94            .map(AdapterRecord::physical)
95            .transpose()
96    }
97
98    /// Return a retained adapter record. The default is the physical model's
99    /// serialized snapshot; initialized stores apply their declared output policy.
100    async fn get_sessions_by_tokens_record(
101        &self,
102        tokens: &[String],
103    ) -> AuthResult<Vec<AdapterRecord<S::Session>>> {
104        self.get_sessions_by_tokens(tokens)
105            .await?
106            .into_iter()
107            .map(AdapterRecord::physical)
108            .collect()
109    }
110
111    /// Return a retained adapter record. The default is the physical model's
112    /// serialized snapshot; initialized stores apply their declared output policy.
113    async fn get_user_sessions_record(
114        &self,
115        user_id: &str,
116    ) -> AuthResult<Vec<AdapterRecord<S::Session>>> {
117        self.get_user_sessions(user_id)
118            .await?
119            .into_iter()
120            .map(AdapterRecord::physical)
121            .collect()
122    }
123
124    /// Return active physical sessions after declared output transforms. Expiry
125    /// selection runs before application callbacks, without trusting output fields.
126    async fn get_active_user_sessions_record(
127        &self,
128        user_id: &str,
129    ) -> AuthResult<Vec<AdapterRecord<S::Session>>> {
130        let now = chrono::Utc::now();
131        self.get_user_sessions(user_id)
132            .await?
133            .into_iter()
134            .filter(|session| session.expires_at() > now && session.active())
135            .map(AdapterRecord::physical)
136            .collect()
137    }
138
139    /// Return a retained adapter record. The default is the physical model's
140    /// serialized snapshot; initialized stores apply their declared output policy.
141    async fn refresh_session_record(
142        &self,
143        token: &str,
144        expires_at: chrono::DateTime<chrono::Utc>,
145    ) -> AuthResult<Option<AdapterRecord<S::Session>>> {
146        self.refresh_session(token, expires_at)
147            .await?
148            .map(AdapterRecord::physical)
149            .transpose()
150    }
151
152    /// Return a retained adapter record. The default is the physical model's
153    /// serialized snapshot; initialized stores apply their declared output policy.
154    async fn update_session_fields_record(
155        &self,
156        token: &str,
157        fields: FieldValues,
158    ) -> AuthResult<Option<AdapterRecord<S::Session>>> {
159        self.update_session_fields(token, fields)
160            .await?
161            .map(AdapterRecord::physical)
162            .transpose()
163    }
164
165    /// Return a retained adapter record. The default is the physical model's
166    /// serialized snapshot; initialized stores apply their declared output policy.
167    async fn update_session_active_organization_record(
168        &self,
169        token: &str,
170        organization_id: Option<&str>,
171    ) -> AuthResult<AdapterRecord<S::Session>> {
172        AdapterRecord::physical(
173            self.update_session_active_organization(token, organization_id)
174                .await?,
175        )
176    }
177
178    /// Return a retained adapter record. The default is the physical model's
179    /// serialized snapshot; initialized stores apply their declared output policy.
180    async fn update_session_active_team_record(
181        &self,
182        token: &str,
183        team_id: Option<&str>,
184    ) -> AuthResult<AdapterRecord<S::Session>> {
185        AdapterRecord::physical(self.update_session_active_team(token, team_id).await?)
186    }
187
188    /// Persist already authorized fields for the currently authenticated token.
189    async fn update_session_fields(
190        &self,
191        _token: &str,
192        _fields: FieldValues,
193    ) -> AuthResult<Option<S::Session>> {
194        Err(AuthError::internal(
195            "the store does not support session field updates",
196        ))
197    }
198
199    async fn create_session(&self, create_session: CreateSession) -> AuthResult<S::Session>;
200
201    async fn get_session(&self, token: &str) -> AuthResult<Option<S::Session>>;
202
203    /// Fetch matching sessions once each, including expired rows. The bundled
204    /// SQLite adapter returns token-index order rather than request order.
205    async fn get_sessions_by_tokens(&self, tokens: &[String]) -> AuthResult<Vec<S::Session>> {
206        let mut sessions = Vec::new();
207        for token in BTreeSet::from_iter(tokens) {
208            if let Some(session) = self.get_session(token).await? {
209                sessions.push(session);
210            }
211        }
212        Ok(sessions)
213    }
214
215    async fn get_user_sessions(&self, user_id: &str) -> AuthResult<Vec<S::Session>>;
216
217    async fn update_session_expiry(
218        &self,
219        token: &str,
220        expires_at: chrono::DateTime<chrono::Utc>,
221    ) -> AuthResult<()>;
222
223    /// Refresh expiry and configured fields in one physical update. Adapters
224    /// supporting additional values must override this operation so their actual
225    /// before hooks precede binding and their after hooks see the final row.
226    /// The fallback preserves plain refresh and fails closed on additional writes.
227    async fn refresh_session_with_fields(
228        &self,
229        token: &str,
230        expires_at: chrono::DateTime<chrono::Utc>,
231        mut fields: FieldValues,
232    ) -> AuthResult<Option<S::Session>> {
233        fields.apply_adapter_transforms_async().await?;
234        if !fields.is_empty() {
235            return Err(AuthError::NotImplemented(
236                "The store does not support refresh field updates".into(),
237            ));
238        }
239        self.refresh_session(token, expires_at).await
240    }
241
242    /// Refresh the persisted expiry and return the updated snapshot. A session
243    /// removed before the update returns `None`, never its old credentials.
244    async fn refresh_session(
245        &self,
246        token: &str,
247        expires_at: chrono::DateTime<chrono::Utc>,
248    ) -> AuthResult<Option<S::Session>> {
249        match self.update_session_expiry(token, expires_at).await {
250            Ok(()) => self.get_session(token).await,
251            Err(AuthError::SessionNotFound) => Ok(None),
252            Err(error) => Err(error),
253        }
254    }
255
256    async fn delete_session(&self, token: &str) -> AuthResult<()>;
257
258    async fn delete_user_sessions(&self, user_id: &str) -> AuthResult<()>;
259
260    async fn delete_expired_sessions(&self) -> AuthResult<usize>;
261
262    async fn update_session_active_organization(
263        &self,
264        token: &str,
265        organization_id: Option<&str>,
266    ) -> AuthResult<S::Session>;
267
268    async fn update_session_active_team(
269        &self,
270        _token: &str,
271        _team_id: Option<&str>,
272    ) -> AuthResult<S::Session> {
273        Err(AuthError::internal(
274            "active-team updates are not supported by this store",
275        ))
276    }
277}