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)
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    /// Read the physical session row by its ID for signed-token liveness.
203    async fn get_session_by_id(&self, _id: &str) -> AuthResult<Option<S::Session>> {
204        Err(AuthError::NotImplemented(
205            "Session ID lookup is unsupported".into(),
206        ))
207    }
208
209    /// Fetch matching sessions once each, including expired rows. The bundled
210    /// SQLite adapter returns token-index order rather than request order.
211    async fn get_sessions_by_tokens(&self, tokens: &[String]) -> AuthResult<Vec<S::Session>> {
212        let mut sessions = Vec::new();
213        for token in BTreeSet::from_iter(tokens) {
214            if let Some(session) = self.get_session(token).await? {
215                sessions.push(session);
216            }
217        }
218        Ok(sessions)
219    }
220
221    async fn get_user_sessions(&self, user_id: &str) -> AuthResult<Vec<S::Session>>;
222
223    async fn update_session_expiry(
224        &self,
225        token: &str,
226        expires_at: chrono::DateTime<chrono::Utc>,
227    ) -> AuthResult<()>;
228
229    /// Refresh expiry and configured fields in one physical update. Adapters
230    /// supporting additional values must override this operation so their actual
231    /// before hooks precede binding and their after hooks see the final row.
232    /// The fallback preserves plain refresh and fails closed on additional writes.
233    async fn refresh_session_with_fields(
234        &self,
235        token: &str,
236        expires_at: chrono::DateTime<chrono::Utc>,
237        mut fields: FieldValues,
238    ) -> AuthResult<Option<S::Session>> {
239        fields.apply_adapter_transforms_async().await?;
240        if !fields.is_empty() {
241            return Err(AuthError::NotImplemented(
242                "The store does not support refresh field updates".into(),
243            ));
244        }
245        self.refresh_session(token, expires_at).await
246    }
247
248    /// Refresh the persisted expiry and return the updated snapshot. A session
249    /// removed before the update returns `None`, never its old credentials.
250    async fn refresh_session(
251        &self,
252        token: &str,
253        expires_at: chrono::DateTime<chrono::Utc>,
254    ) -> AuthResult<Option<S::Session>> {
255        match self.update_session_expiry(token, expires_at).await {
256            Ok(()) => self.get_session(token).await,
257            Err(AuthError::SessionNotFound) => Ok(None),
258            Err(error) => Err(error),
259        }
260    }
261
262    async fn delete_session(&self, token: &str) -> AuthResult<()>;
263
264    async fn delete_user_sessions(&self, user_id: &str) -> AuthResult<()>;
265
266    async fn delete_expired_sessions(&self) -> AuthResult<usize>;
267
268    async fn update_session_active_organization(
269        &self,
270        token: &str,
271        organization_id: Option<&str>,
272    ) -> AuthResult<S::Session>;
273
274    async fn update_session_active_team(
275        &self,
276        _token: &str,
277        _team_id: Option<&str>,
278    ) -> AuthResult<S::Session> {
279        Err(AuthError::internal(
280            "active-team updates are not supported by this store",
281        ))
282    }
283}