Skip to content

Commit d0d8566

Browse files
committed
Fix EF Core transaction execution for user deletion and refresh tokens
1 parent d507a94 commit d0d8566

4 files changed

Lines changed: 556 additions & 89 deletions

File tree

‎src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/Stores/EfCoreRefreshTokenStore.cs‎

Lines changed: 44 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -21,66 +21,73 @@ public EfCoreRefreshTokenStore(TDbContext db, TenantExecutionContext tenant)
2121
private DbSet<RefreshTokenProjection> DbSet => _db.Set<RefreshTokenProjection>();
2222

2323
// TODO: Review execution strategy retries and commit-unknown scenarios.
24-
// Ensure failed transaction attempts do not retain stale tracked entities,
25-
// and evaluate recovery when the database commit succeeds but the client
26-
// does not receive the replacement refresh token.
27-
public async Task ExecuteAsync(Func<CancellationToken, Task> action, CancellationToken ct = default)
24+
// Ensure failed transaction attempts do not retain stale tracked entities and evaluate recovery when the database commit succeeds but the client does not receive the replacement refresh token.
25+
public Task ExecuteAsync(Func<CancellationToken, Task> action, CancellationToken ct = default)
2826
{
29-
var strategy = _db.Database.CreateExecutionStrategy();
30-
31-
await strategy.ExecuteAsync(async () =>
32-
{
33-
await using var tx = await _db.Database.BeginTransactionAsync(ct);
27+
ArgumentNullException.ThrowIfNull(action);
3428

35-
_inTransaction = true;
36-
37-
try
29+
return ExecuteAsync<object?>(
30+
async innerCt =>
3831
{
39-
await action(ct);
40-
await _db.SaveChangesAsync(ct);
41-
await tx.CommitAsync(ct);
42-
}
43-
catch
44-
{
45-
await tx.RollbackAsync(ct);
46-
throw;
47-
}
48-
finally
49-
{
50-
_inTransaction = false;
51-
}
52-
});
32+
await action(innerCt);
33+
return null;
34+
},
35+
ct);
5336
}
5437

5538
public async Task<TResult> ExecuteAsync<TResult>(Func<CancellationToken, Task<TResult>> action, CancellationToken ct = default)
5639
{
40+
ArgumentNullException.ThrowIfNull(action);
41+
ct.ThrowIfCancellationRequested();
42+
43+
// Participate in the transaction owned by the atomic executor.
44+
if (_db.Database.CurrentTransaction is not null)
45+
return await ExecuteCoreAsync(action, ct);
46+
5747
var strategy = _db.Database.CreateExecutionStrategy();
5848

5949
return await strategy.ExecuteAsync(async () =>
6050
{
61-
await using var tx = await _db.Database.BeginTransactionAsync(ct);
62-
63-
_inTransaction = true;
51+
await using var transaction =
52+
await _db.Database.BeginTransactionAsync(ct);
6453

6554
try
6655
{
67-
var result = await action(ct);
68-
await _db.SaveChangesAsync(ct);
69-
await tx.CommitAsync(ct);
56+
var result = await ExecuteCoreAsync(action, ct);
57+
58+
await transaction.CommitAsync(ct);
59+
7060
return result;
7161
}
7262
catch
7363
{
74-
await tx.RollbackAsync(ct);
64+
await transaction.RollbackAsync(CancellationToken.None);
7565
throw;
7666
}
77-
finally
78-
{
79-
_inTransaction = false;
80-
}
8167
});
8268
}
8369

70+
private async Task<TResult> ExecuteCoreAsync<TResult>(Func<CancellationToken, Task<TResult>> action, CancellationToken ct)
71+
{
72+
if (_inTransaction)
73+
throw new InvalidOperationException("A refresh token store execution is already active.");
74+
75+
_inTransaction = true;
76+
77+
try
78+
{
79+
var result = await action(ct);
80+
81+
await _db.SaveChangesAsync(ct);
82+
83+
return result;
84+
}
85+
finally
86+
{
87+
_inTransaction = false;
88+
}
89+
}
90+
8491
private void EnsureTransaction()
8592
{
8693
if (!_inTransaction)

‎src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs‎

Lines changed: 117 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -222,91 +222,156 @@ public async Task<UserStatusChangeResult> ChangeUserStatusAsync(AccessContext co
222222
return await _accessOrchestrator.ExecuteAsync(context, command, ct);
223223
}
224224

225-
226225
public async Task DeleteMeAsync(AccessContext context, CancellationToken ct = default)
227226
{
228227
var command = new AccessCommand(async innerCt =>
229228
{
230-
var userKey = context.GetTargetUserKey();
231-
var now = _clock.UtcNow;
229+
await _atomicExecutor.ExecuteAsync(async atomicCt =>
230+
{
231+
var userKey = context.GetTargetUserKey();
232+
var now = _clock.UtcNow;
232233

233-
var lifecycleKey = new UserLifecycleKey(context.ResourceTenant, userKey);
234-
var lifecycleStore = _lifecycleStoreFactory.Create(context.ResourceTenant);
235-
var lifecycle = await lifecycleStore.GetAsync(lifecycleKey, innerCt);
234+
var lifecycleKey = new UserLifecycleKey(context.ResourceTenant, userKey);
236235

237-
if (lifecycle is null)
238-
throw new UAuthNotFoundException();
236+
var lifecycleStore = _lifecycleStoreFactory.Create(context.ResourceTenant);
239237

240-
var profileStore = _profileStoreFactory.Create(context.ResourceTenant);
241-
var identifierStore = _identifierStoreFactory.Create(context.ResourceTenant);
238+
var lifecycle = await lifecycleStore.GetAsync(lifecycleKey, atomicCt);
242239

243-
await lifecycleStore.DeleteAsync(lifecycleKey, lifecycle.Version, DeleteMode.Soft, now, innerCt);
244-
await identifierStore.DeleteByUserAsync(userKey, DeleteMode.Soft, now, innerCt);
240+
if (lifecycle is null)
241+
throw new UAuthNotFoundException();
245242

246-
var profiles = await profileStore.GetAllProfilesByUserAsync(userKey, innerCt);
243+
var profileStore = _profileStoreFactory.Create(context.ResourceTenant);
247244

248-
foreach (var profile in profiles)
249-
{
250-
var key = new UserProfileKey(context.ResourceTenant, userKey, profile.ProfileKey);
251-
await profileStore.DeleteAsync(key, profile.Version, DeleteMode.Soft, now, innerCt);
252-
}
245+
var identifierStore = _identifierStoreFactory.Create(context.ResourceTenant);
253246

254-
foreach (var integration in _integrations)
255-
{
256-
await integration.OnUserDeletedAsync(context.ResourceTenant, userKey, DeleteMode.Soft, innerCt);
257-
}
247+
await lifecycleStore.DeleteAsync(lifecycleKey, lifecycle.Version, DeleteMode.Soft, now, atomicCt);
248+
249+
await identifierStore.DeleteByUserAsync(userKey, DeleteMode.Soft, now, atomicCt);
258250

259-
var sessionStore = _sessionStoreFactory.Create(context.ResourceTenant);
260-
await sessionStore.RevokeAllChainsAsync(userKey, now, innerCt);
251+
var profiles = await profileStore.GetAllProfilesByUserAsync(userKey, atomicCt);
252+
253+
foreach (var profile in profiles)
254+
{
255+
var key = new UserProfileKey(
256+
context.ResourceTenant,
257+
userKey,
258+
profile.ProfileKey);
259+
260+
await profileStore.DeleteAsync(
261+
key,
262+
profile.Version,
263+
DeleteMode.Soft,
264+
now,
265+
atomicCt);
266+
}
267+
268+
foreach (var integration in _integrations)
269+
{
270+
await integration.OnUserDeletedAsync(
271+
context.ResourceTenant,
272+
userKey,
273+
DeleteMode.Soft,
274+
atomicCt);
275+
}
276+
277+
var sessionStore = _sessionStoreFactory.Create(
278+
context.ResourceTenant);
279+
280+
await sessionStore.ExecuteAsync(
281+
sessionCt => sessionStore.RevokeAllChainsAsync(
282+
userKey,
283+
now,
284+
sessionCt),
285+
atomicCt);
286+
}, innerCt);
261287
});
262288

263289
await _accessOrchestrator.ExecuteAsync(context, command, ct);
264290
}
265291

266-
267292
public async Task<UserDeleteResult> DeleteUserAsync(AccessContext context, DeleteUserRequest request, CancellationToken ct = default)
268293
{
269294
var command = new AccessCommand<UserDeleteResult>(async innerCt =>
270295
{
271-
var targetUserKey = context.GetTargetUserKey();
272-
var now = _clock.UtcNow;
273-
var userLifecycleKey = new UserLifecycleKey(context.ResourceTenant, targetUserKey);
274-
var lifecycleStore = _lifecycleStoreFactory.Create(context.ResourceTenant);
275-
var lifecycle = await lifecycleStore.GetAsync(userLifecycleKey, innerCt);
296+
return await _atomicExecutor.ExecuteAsync(async atomicCt =>
297+
{
298+
var targetUserKey = context.GetTargetUserKey();
299+
var now = _clock.UtcNow;
276300

277-
if (lifecycle is null)
278-
throw new UAuthNotFoundException();
301+
var userLifecycleKey = new UserLifecycleKey(context.ResourceTenant, targetUserKey);
279302

280-
var profileStore = _profileStoreFactory.Create(context.ResourceTenant);
281-
var identifierStore = _identifierStoreFactory.Create(context.ResourceTenant);
303+
var lifecycleStore = _lifecycleStoreFactory.Create(context.ResourceTenant);
282304

283-
await lifecycleStore.DeleteAsync(userLifecycleKey, lifecycle.Version, request.Mode, now, innerCt);
284-
await identifierStore.DeleteByUserAsync(targetUserKey, request.Mode, now, innerCt);
285-
var profiles = await profileStore.GetAllProfilesByUserAsync(targetUserKey, innerCt);
305+
var lifecycle = await lifecycleStore.GetAsync(userLifecycleKey, atomicCt);
286306

287-
foreach (var profile in profiles)
288-
{
289-
var key = new UserProfileKey(context.ResourceTenant, profile.UserKey, profile.ProfileKey);
307+
if (lifecycle is null)
308+
throw new UAuthNotFoundException();
290309

291-
await profileStore.DeleteAsync(key, profile.Version, request.Mode, now, innerCt);
292-
}
310+
var profileStore = _profileStoreFactory.Create(context.ResourceTenant);
293311

294-
foreach (var integration in _integrations)
295-
{
296-
await integration.OnUserDeletedAsync(context.ResourceTenant, targetUserKey, request.Mode, innerCt);
297-
}
312+
var identifierStore = _identifierStoreFactory.Create( context.ResourceTenant);
313+
314+
await lifecycleStore.DeleteAsync(
315+
userLifecycleKey,
316+
lifecycle.Version,
317+
request.Mode,
318+
now,
319+
atomicCt);
320+
321+
await identifierStore.DeleteByUserAsync(
322+
targetUserKey,
323+
request.Mode,
324+
now,
325+
atomicCt);
298326

299-
var sessionStore = _sessionStoreFactory.Create(context.ResourceTenant);
327+
var profiles = await profileStore.GetAllProfilesByUserAsync(
328+
targetUserKey,
329+
atomicCt);
300330

301-
await sessionStore.RevokeAllChainsAsync(targetUserKey, now, innerCt);
331+
foreach (var profile in profiles)
332+
{
333+
var key = new UserProfileKey(
334+
context.ResourceTenant,
335+
profile.UserKey,
336+
profile.ProfileKey);
337+
338+
await profileStore.DeleteAsync(
339+
key,
340+
profile.Version,
341+
request.Mode,
342+
now,
343+
atomicCt);
344+
}
302345

303-
return UserDeleteResult.Success(request.Mode);
346+
foreach (var integration in _integrations)
347+
{
348+
await integration.OnUserDeletedAsync(
349+
context.ResourceTenant,
350+
targetUserKey,
351+
request.Mode,
352+
atomicCt);
353+
}
354+
355+
var sessionStore = _sessionStoreFactory.Create(
356+
context.ResourceTenant);
357+
358+
await sessionStore.ExecuteAsync(
359+
sessionCt => sessionStore.RevokeAllChainsAsync(
360+
targetUserKey,
361+
now,
362+
sessionCt),
363+
atomicCt);
364+
365+
return UserDeleteResult.Success(request.Mode);
366+
}, innerCt);
304367
});
305368

306-
return await _accessOrchestrator.ExecuteAsync(context, command, ct);
369+
return await _accessOrchestrator.ExecuteAsync(
370+
context,
371+
command,
372+
ct);
307373
}
308374

309-
310375
#endregion
311376

312377

@@ -819,6 +884,7 @@ public async Task<UserIdentifierAvailabilityResult> CheckIdentifierAvailabilityA
819884

820885
#endregion
821886

887+
822888
public async Task<PagedResult<UserSummary>> QueryUsersAsync(AccessContext context, UserQuery query, CancellationToken ct = default)
823889
{
824890
var command = new AccessCommand<PagedResult<UserSummary>>(

0 commit comments

Comments
 (0)