1use crate::AdapterRecord;
2use crate::field_policy::FieldValues;
3use crate::types::AddTeamMemberResult;
4use crate::user_validation::{PreparedUserCreation, UserValidationSource};
5use crate::verification::{VerificationCreation, VerificationPublication, VerificationSnapshot};
6use crate::{
7 AuthError, AuthResult, AuthSchema, AuthStore, CreateAccount, CreateJwk, CreateMember,
8 CreatePasskey, CreateSession, CreateUser, CreateVerification, Jwk, Member, Passkey, Team,
9};
10use async_trait::async_trait;
11use std::any::Any;
12use std::pin::Pin;
13pub type BoxedTransactionValue = Box<dyn Any + Send>;
14
15pub type TransactionFuture<'a> =
16 Pin<Box<dyn Future<Output = AuthResult<BoxedTransactionValue>> + Send + 'a>>;
17
18pub type TypedTransactionFuture<'a, T> = Pin<Box<dyn Future<Output = AuthResult<T>> + Send + 'a>>;
19
20pub type TransactionWork<S> =
21 dyn for<'tx> FnOnce(&'tx dyn AuthTransaction<S>) -> TransactionFuture<'tx> + Send;
22
23#[async_trait]
24pub trait AuthTransaction<S: AuthSchema>: Send + Sync {
25 async fn check_scim_user_attributes(
26 &self,
27 _id: &str,
28 _connection: &str,
29 _attributes: &crate::store::ScimUserAttributes,
30 ) -> AuthResult<()> {
31 Err(AuthError::not_implemented(
32 "Transactional SCIM User uniqueness checks are unsupported",
33 ))
34 }
35
36 async fn lock_scim_user(
37 &self,
38 _id: &str,
39 _connection: &str,
40 _domain: &str,
41 _attributes: Option<&crate::store::ScimUserAttributes>,
42 _now: chrono::DateTime<chrono::Utc>,
43 ) -> AuthResult<crate::store::ScimUserMutationContext> {
44 Err(AuthError::not_implemented(
45 "Transactional SCIM User mutation is unsupported",
46 ))
47 }
48 async fn replace_scim_user(
49 &self,
50 _id: &str,
51 _data: crate::store::ReplaceScimUser,
52 _roles: bool,
53 ) -> AuthResult<(
54 crate::store::ScimUserRecord,
55 Vec<crate::store::ScimProjectionPreparedUser>,
56 )> {
57 Err(AuthError::not_implemented(
58 "Transactional SCIM User replacement is unsupported",
59 ))
60 }
61
62 async fn create_scim_user(
63 &self,
64 _data: crate::store::CreateScimUser,
65 _roles: bool,
66 ) -> AuthResult<(
67 crate::store::ScimUserRecord,
68 Vec<crate::store::ScimProjectionPreparedUser>,
69 )> {
70 Err(AuthError::not_implemented(
71 "Transactional SCIM User creation is unsupported",
72 ))
73 }
74
75 async fn delete_scim_user(
76 &self,
77 _id: &str,
78 _connection: &str,
79 _domain: &str,
80 _roles: bool,
81 ) -> AuthResult<(String, Vec<crate::store::ScimProjectionPreparedUser>)> {
82 Err(AuthError::not_implemented(
83 "Transactional SCIM User deletion is unsupported",
84 ))
85 }
86 async fn patch_scim_group(
87 &self,
88 _id: &str,
89 _connection: &str,
90 _domain: &str,
91 _patch: crate::store::ScimGroupPatch,
92 _roles: bool,
93 ) -> AuthResult<(
94 crate::store::ScimGroupRecord,
95 Vec<crate::store::ScimProjectionPreparedUser>,
96 )> {
97 Err(AuthError::not_implemented(
98 "Transactional SCIM Group patching is unsupported",
99 ))
100 }
101 async fn replace_scim_group(
102 &self,
103 _id: &str,
104 _data: crate::store::ReplaceScimGroup,
105 _roles: bool,
106 ) -> AuthResult<(
107 crate::store::ScimGroupRecord,
108 Vec<crate::store::ScimProjectionPreparedUser>,
109 )> {
110 Err(AuthError::not_implemented(
111 "Transactional SCIM Group replacement is unsupported",
112 ))
113 }
114 async fn delete_scim_group(
115 &self,
116 _id: &str,
117 _connection: &str,
118 _domain: &str,
119 _roles: bool,
120 ) -> AuthResult<Vec<crate::store::ScimProjectionPreparedUser>> {
121 Err(AuthError::not_implemented(
122 "Transactional SCIM Group deletion is unsupported",
123 ))
124 }
125 async fn create_scim_group(
126 &self,
127 _data: crate::store::CreateScimGroup,
128 _roles: bool,
129 ) -> AuthResult<(
130 crate::store::ScimGroupRecord,
131 Vec<crate::store::ScimProjectionPreparedUser>,
132 )> {
133 Err(AuthError::not_implemented(
134 "Transactional SCIM Groups are unsupported",
135 ))
136 }
137 async fn fence_scim_connection(&self, _id: &str, _key: &str) -> AuthResult<()> {
138 Err(AuthError::not_implemented(
139 "Transactional SCIM fencing is unsupported",
140 ))
141 }
142 async fn get_scim_connection_binding(
143 &self,
144 _id: &str,
145 ) -> AuthResult<Option<crate::store::ScimConnectionBinding>> {
146 Err(AuthError::not_implemented(
147 "Transactional SCIM bindings are unsupported",
148 ))
149 }
150 async fn scim_projection_candidates(
151 &self,
152 _domain: &str,
153 _cursor: Option<&str>,
154 ) -> AuthResult<Vec<crate::store::ScimProjectionCandidate>> {
155 Err(AuthError::not_implemented(
156 "Transactional SCIM candidates are unsupported",
157 ))
158 }
159 async fn renew_scim_decommission_lease(
160 &self,
161 _binding: &crate::store::ScimConnectionBinding,
162 _lease: &str,
163 ) -> AuthResult<bool> {
164 Err(AuthError::not_implemented(
165 "Transactional SCIM leases are unsupported",
166 ))
167 }
168 async fn advance_scim_decommission(
169 &self,
170 _binding: &crate::store::ScimConnectionBinding,
171 _lease: &str,
172 _cursor: Option<&str>,
173 _count: usize,
174 _more: bool,
175 ) -> AuthResult<Option<crate::store::ScimConnectionBinding>> {
176 Err(AuthError::not_implemented(
177 "Transactional SCIM checkpoints are unsupported",
178 ))
179 }
180 async fn reconcile_scim_identity(
181 &self,
182 _user_id: &str,
183 ) -> AuthResult<crate::store::ScimIdentityState> {
184 Err(AuthError::not_implemented(
185 "Transactional SCIM identity is unsupported",
186 ))
187 }
188 async fn delete_user_sessions(&self, _user_id: &str) -> AuthResult<()> {
189 Err(AuthError::not_implemented(
190 "Transactional session retirement is unsupported",
191 ))
192 }
193
194 async fn prepare_scim_projection(
195 &self,
196 _domain: &str,
197 _ids: &[String],
198 _roles: bool,
199 ) -> AuthResult<Vec<crate::store::ScimProjectionPreparedUser>> {
200 Err(AuthError::not_implemented(
201 "Transactional SCIM projection is unsupported",
202 ))
203 }
204 async fn create_scim_projection_grant(
205 &self,
206 _data: crate::store::CreateScimProjectionGrant,
207 ) -> AuthResult<()> {
208 Err(AuthError::not_implemented(
209 "Transactional SCIM grants are unsupported",
210 ))
211 }
212 async fn remove_scim_projection_grants(
213 &self,
214 _domain: &str,
215 _user_id: &str,
216 _ids: &[String],
217 ) -> AuthResult<()> {
218 Err(AuthError::not_implemented(
219 "Transactional SCIM grants are unsupported",
220 ))
221 }
222
223 async fn get_user_by_email(&self, _email: &str) -> AuthResult<Option<S::User>> {
224 Err(AuthError::NotImplemented(
225 "Transactional user lookup is unsupported".into(),
226 ))
227 }
228 async fn get_account(
229 &self,
230 _provider: &str,
231 _account_id: &str,
232 ) -> AuthResult<Option<S::Account>> {
233 Err(AuthError::NotImplemented(
234 "Transactional account lookup is unsupported".into(),
235 ))
236 }
237 async fn update_user(&self, _id: &str, _update: crate::UpdateUser) -> AuthResult<S::User> {
238 Err(AuthError::NotImplemented(
239 "Transactional user updates are unsupported".into(),
240 ))
241 }
242 async fn update_account(
243 &self,
244 _id: &str,
245 _update: crate::UpdateAccount,
246 ) -> AuthResult<S::Account> {
247 Err(AuthError::NotImplemented(
248 "Transactional account updates are unsupported".into(),
249 ))
250 }
251 async fn get_user_by_email_record(
252 &self,
253 email: &str,
254 ) -> AuthResult<Option<AdapterRecord<S::User>>> {
255 self.get_user_by_email(email)
256 .await?
257 .map(AdapterRecord::physical)
258 .transpose()
259 }
260 async fn get_account_record(
261 &self,
262 provider: &str,
263 account_id: &str,
264 ) -> AuthResult<Option<AdapterRecord<S::Account>>> {
265 self.get_account(provider, account_id)
266 .await?
267 .map(AdapterRecord::physical)
268 .transpose()
269 }
270 async fn update_user_record(
271 &self,
272 id: &str,
273 update: crate::UpdateUser,
274 ) -> AuthResult<AdapterRecord<S::User>> {
275 AdapterRecord::physical(self.update_user(id, update).await?)
276 }
277 async fn update_account_record(
278 &self,
279 id: &str,
280 update: crate::UpdateAccount,
281 ) -> AuthResult<AdapterRecord<S::Account>> {
282 AdapterRecord::physical(self.update_account(id, update).await?)
283 }
284
285 async fn lock_sso_provider(
287 &self,
288 _id: &str,
289 _provider_id: &str,
290 ) -> AuthResult<Option<crate::store::SsoProvider>> {
291 Err(AuthError::NotImplemented(
292 "Transactional SSO provider locking is unsupported".into(),
293 ))
294 }
295 async fn update_sso_provider(
297 &self,
298 _update: crate::store::SsoProviderUpdate,
299 ) -> AuthResult<Option<crate::store::SsoProvider>> {
300 Err(AuthError::NotImplemented(
301 "Transactional SSO provider updates are unsupported".into(),
302 ))
303 }
304 async fn delete_sso_provider_and_accounts(
306 &self,
307 _id: &str,
308 _provider_id: &str,
309 ) -> AuthResult<bool> {
310 Err(AuthError::NotImplemented(
311 "Transactional SSO provider deletion is unsupported".into(),
312 ))
313 }
314
315 async fn provider_verification_output(
319 &self,
320 _id: &str,
321 ) -> AuthResult<Option<serde_json::Value>> {
322 Ok(None)
323 }
324
325 async fn prepare_secondary_session_creation(
327 &self,
328 _input: CreateSession,
329 _persist: bool,
330 ) -> AuthResult<S::Session> {
331 Err(AuthError::NotImplemented(
332 "Transactional secondary session creation is unsupported".into(),
333 ))
334 }
335
336 async fn prepare_secondary_session_update(
338 &self,
339 _session: S::Session,
340 _expires_at: Option<chrono::DateTime<chrono::Utc>>,
341 _fields: FieldValues,
342 ) -> AuthResult<Option<(S::Session, FieldValues)>> {
343 Err(AuthError::NotImplemented(
344 "Transactional secondary session updates are unsupported".into(),
345 ))
346 }
347 async fn complete_secondary_session_update(
349 &self,
350 _session: S::Session,
351 _expires_at: Option<chrono::DateTime<chrono::Utc>>,
352 _fields: FieldValues,
353 _persist: bool,
354 ) -> AuthResult<Option<S::Session>> {
355 Err(AuthError::NotImplemented(
356 "Transactional secondary session updates are unsupported".into(),
357 ))
358 }
359
360 async fn list_jwks(&self) -> AuthResult<Vec<Jwk>> {
362 Err(AuthError::config(
363 "Transactional JWKS reads are unsupported by this store",
364 ))
365 }
366 async fn get_jwk_by_id(&self, _id: &str) -> AuthResult<Option<Jwk>> {
368 Err(AuthError::config(
369 "Transactional JWKS reads are unsupported by this store",
370 ))
371 }
372 async fn create_jwk(&self, _data: CreateJwk) -> AuthResult<Jwk> {
374 Err(AuthError::config(
375 "Transactional JWKS writes are unsupported by this store",
376 ))
377 }
378
379 async fn create_user_record(
382 &self,
383 create_user: CreateUser,
384 ) -> AuthResult<AdapterRecord<S::User>> {
385 AdapterRecord::physical(self.create_user(create_user).await?)
386 }
387
388 async fn create_user_with_source_record(
391 &self,
392 create_user: CreateUser,
393 source: UserValidationSource,
394 ) -> AuthResult<AdapterRecord<S::User>> {
395 AdapterRecord::physical(self.create_user_with_source(create_user, source).await?)
396 }
397
398 async fn create_user_prepared_record(
401 &self,
402 prepared: PreparedUserCreation,
403 ) -> AuthResult<AdapterRecord<S::User>> {
404 AdapterRecord::physical(self.create_user_prepared(prepared).await?)
405 }
406
407 async fn get_user_by_id_record(&self, id: &str) -> AuthResult<Option<AdapterRecord<S::User>>> {
410 self.get_user_by_id(id)
411 .await?
412 .map(AdapterRecord::physical)
413 .transpose()
414 }
415
416 async fn create_account_record(
419 &self,
420 create_account: CreateAccount,
421 ) -> AuthResult<AdapterRecord<S::Account>> {
422 AdapterRecord::physical(self.create_account(create_account).await?)
423 }
424
425 async fn create_session_record(
428 &self,
429 create_session: CreateSession,
430 ) -> AuthResult<AdapterRecord<S::Session>> {
431 AdapterRecord::physical(self.create_session(create_session).await?)
432 }
433
434 async fn update_session_active_organization_record(
437 &self,
438 token: &str,
439 organization_id: Option<&str>,
440 ) -> AuthResult<AdapterRecord<S::Session>> {
441 AdapterRecord::physical(
442 self.update_session_active_organization(token, organization_id)
443 .await?,
444 )
445 }
446
447 async fn update_session_active_team_record(
450 &self,
451 token: &str,
452 team_id: Option<&str>,
453 ) -> AuthResult<AdapterRecord<S::Session>> {
454 AdapterRecord::physical(self.update_session_active_team(token, team_id).await?)
455 }
456
457 async fn create_verification_record(
460 &self,
461 _data: VerificationCreation,
462 _publication: VerificationPublication,
463 ) -> AuthResult<Option<VerificationSnapshot>> {
464 Err(AuthError::NotImplemented(
465 "Verification publication phases are not supported by this store".into(),
466 ))
467 }
468 async fn get_team(&self, _organization_id: &str, _team_id: &str) -> AuthResult<Option<Team>> {
470 Err(AuthError::NotImplemented(
471 "Team lookup in a transaction is not supported by this store".into(),
472 ))
473 }
474 async fn add_team_member(
476 &self,
477 _team_id: &str,
478 _user_id: &str,
479 _maximum: Option<f64>,
480 ) -> AuthResult<AddTeamMemberResult> {
481 Err(AuthError::NotImplemented(
482 "Team admission in a transaction is not supported by this store".into(),
483 ))
484 }
485 async fn create_member(&self, _member: CreateMember) -> AuthResult<Member> {
486 Err(AuthError::NotImplemented(
487 "Member creation in a transaction is not supported by this store".into(),
488 ))
489 }
490 async fn update_session_active_team(
492 &self,
493 _token: &str,
494 _team_id: Option<&str>,
495 ) -> AuthResult<S::Session> {
496 Err(AuthError::NotImplemented(
497 "Active team updates in a transaction are not supported by this store".into(),
498 ))
499 }
500 async fn update_session_active_organization(
501 &self,
502 _token: &str,
503 _organization_id: Option<&str>,
504 ) -> AuthResult<S::Session> {
505 Err(AuthError::NotImplemented(
506 "Active organization updates in a transaction are not supported by this store".into(),
507 ))
508 }
509 async fn get_user_by_id(&self, _id: &str) -> AuthResult<Option<S::User>> {
511 Err(AuthError::NotImplemented(
512 "User lookup in a transaction is not supported by this store".to_owned(),
513 ))
514 }
515 async fn create_passkey(&self, _passkey: CreatePasskey) -> AuthResult<Passkey> {
517 Err(AuthError::NotImplemented(
518 "Passkey creation in a transaction is not supported by this store".to_owned(),
519 ))
520 }
521 async fn create_user(&self, create_user: CreateUser) -> AuthResult<S::User>;
522 async fn create_user_with_source(
525 &self,
526 create_user: CreateUser,
527 _source: UserValidationSource,
528 ) -> AuthResult<S::User> {
529 self.create_user(create_user).await
530 }
531 async fn create_user_prepared(&self, _prepared: PreparedUserCreation) -> AuthResult<S::User> {
534 Err(AuthError::NotImplemented(
535 "Prepared user creation is not supported by this transaction".into(),
536 ))
537 }
538 async fn create_account(&self, create_account: CreateAccount) -> AuthResult<S::Account>;
539 async fn create_session(&self, create_session: CreateSession) -> AuthResult<S::Session>;
540 async fn create_verification(
541 &self,
542 _verification: CreateVerification,
543 ) -> AuthResult<S::Verification> {
544 Err(AuthError::NotImplemented(
545 "Verification creation in a transaction is not supported by this store".to_owned(),
546 ))
547 }
548}
549
550#[derive(Clone, Copy, Debug, PartialEq, Eq)]
552pub enum AfterCommitErrorPolicy {
553 Propagate,
554 Report,
555}
556#[async_trait]
557pub trait TransactionStore<S: AuthSchema>: Send + Sync {
558 async fn transaction_boxed(
559 &self,
560 work: Box<TransactionWork<S>>,
561 ) -> AuthResult<BoxedTransactionValue>;
562 async fn transaction_boxed_with_policy(
563 &self,
564 work: Box<TransactionWork<S>>,
565 policy: AfterCommitErrorPolicy,
566 ) -> AuthResult<BoxedTransactionValue> {
567 if policy == AfterCommitErrorPolicy::Propagate {
568 self.transaction_boxed(work).await
569 } else {
570 Err(AuthError::NotImplemented(
571 "Reporting committed transaction hook failures is unsupported".into(),
572 ))
573 }
574 }
575}
576
577pub async fn transaction<S, T, F>(store: &dyn AuthStore<S>, work: F) -> AuthResult<T>
581where
582 S: AuthSchema,
583 T: Send + 'static,
584 F: for<'tx> FnOnce(&'tx dyn AuthTransaction<S>) -> TypedTransactionFuture<'tx, T>
585 + Send
586 + 'static,
587{
588 transaction_with_policy(store, AfterCommitErrorPolicy::Propagate, work).await
589}
590pub async fn transaction_with_policy<S, T, F>(
595 store: &dyn AuthStore<S>,
596 policy: AfterCommitErrorPolicy,
597 work: F,
598) -> AuthResult<T>
599where
600 S: AuthSchema,
601 T: Send + 'static,
602 F: for<'tx> FnOnce(&'tx dyn AuthTransaction<S>) -> TypedTransactionFuture<'tx, T>
603 + Send
604 + 'static,
605{
606 let value = store
607 .transaction_boxed_with_policy(
608 Box::new(move |tx| {
609 Box::pin(async move {
610 let result: BoxedTransactionValue = Box::new(work(tx).await?);
611 Ok(result)
612 })
613 }),
614 policy,
615 )
616 .await?;
617
618 value
619 .downcast::<T>()
620 .map(|boxed| *boxed)
621 .map_err(|_| AuthError::internal("store returned an invalid transaction payload"))
622}