Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,66 +21,73 @@ public EfCoreRefreshTokenStore(TDbContext db, TenantExecutionContext tenant)
private DbSet<RefreshTokenProjection> DbSet => _db.Set<RefreshTokenProjection>();

// TODO: Review execution strategy retries and commit-unknown scenarios.
// 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.
public async Task ExecuteAsync(Func<CancellationToken, Task> action, CancellationToken ct = default)
// 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.
public Task ExecuteAsync(Func<CancellationToken, Task> action, CancellationToken ct = default)
{
var strategy = _db.Database.CreateExecutionStrategy();

await strategy.ExecuteAsync(async () =>
{
await using var tx = await _db.Database.BeginTransactionAsync(ct);
ArgumentNullException.ThrowIfNull(action);

_inTransaction = true;

try
return ExecuteAsync<object?>(
async innerCt =>
{
await action(ct);
await _db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
}
catch
{
await tx.RollbackAsync(ct);
throw;
}
finally
{
_inTransaction = false;
}
});
await action(innerCt);
return null;
},
ct);
}

public async Task<TResult> ExecuteAsync<TResult>(Func<CancellationToken, Task<TResult>> action, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(action);
ct.ThrowIfCancellationRequested();

// Participate in the transaction owned by the atomic executor.
if (_db.Database.CurrentTransaction is not null)
return await ExecuteCoreAsync(action, ct);

var strategy = _db.Database.CreateExecutionStrategy();

return await strategy.ExecuteAsync(async () =>
{
await using var tx = await _db.Database.BeginTransactionAsync(ct);

_inTransaction = true;
await using var transaction =
await _db.Database.BeginTransactionAsync(ct);

try
{
var result = await action(ct);
await _db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
var result = await ExecuteCoreAsync(action, ct);

await transaction.CommitAsync(ct);

return result;
}
catch
{
await tx.RollbackAsync(ct);
await transaction.RollbackAsync(CancellationToken.None);
throw;
}
finally
{
_inTransaction = false;
}
});
}

private async Task<TResult> ExecuteCoreAsync<TResult>(Func<CancellationToken, Task<TResult>> action, CancellationToken ct)
{
if (_inTransaction)
throw new InvalidOperationException("A refresh token store execution is already active.");

_inTransaction = true;

try
{
var result = await action(ct);

await _db.SaveChangesAsync(ct);

return result;
}
finally
{
_inTransaction = false;
}
}

private void EnsureTransaction()
{
if (!_inTransaction)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,91 +222,156 @@ public async Task<UserStatusChangeResult> ChangeUserStatusAsync(AccessContext co
return await _accessOrchestrator.ExecuteAsync(context, command, ct);
}


public async Task DeleteMeAsync(AccessContext context, CancellationToken ct = default)
{
var command = new AccessCommand(async innerCt =>
{
var userKey = context.GetTargetUserKey();
var now = _clock.UtcNow;
await _atomicExecutor.ExecuteAsync(async atomicCt =>
{
var userKey = context.GetTargetUserKey();
var now = _clock.UtcNow;

var lifecycleKey = new UserLifecycleKey(context.ResourceTenant, userKey);
var lifecycleStore = _lifecycleStoreFactory.Create(context.ResourceTenant);
var lifecycle = await lifecycleStore.GetAsync(lifecycleKey, innerCt);
var lifecycleKey = new UserLifecycleKey(context.ResourceTenant, userKey);

if (lifecycle is null)
throw new UAuthNotFoundException();
var lifecycleStore = _lifecycleStoreFactory.Create(context.ResourceTenant);

var profileStore = _profileStoreFactory.Create(context.ResourceTenant);
var identifierStore = _identifierStoreFactory.Create(context.ResourceTenant);
var lifecycle = await lifecycleStore.GetAsync(lifecycleKey, atomicCt);

await lifecycleStore.DeleteAsync(lifecycleKey, lifecycle.Version, DeleteMode.Soft, now, innerCt);
await identifierStore.DeleteByUserAsync(userKey, DeleteMode.Soft, now, innerCt);
if (lifecycle is null)
throw new UAuthNotFoundException();

var profiles = await profileStore.GetAllProfilesByUserAsync(userKey, innerCt);
var profileStore = _profileStoreFactory.Create(context.ResourceTenant);

foreach (var profile in profiles)
{
var key = new UserProfileKey(context.ResourceTenant, userKey, profile.ProfileKey);
await profileStore.DeleteAsync(key, profile.Version, DeleteMode.Soft, now, innerCt);
}
var identifierStore = _identifierStoreFactory.Create(context.ResourceTenant);

foreach (var integration in _integrations)
{
await integration.OnUserDeletedAsync(context.ResourceTenant, userKey, DeleteMode.Soft, innerCt);
}
await lifecycleStore.DeleteAsync(lifecycleKey, lifecycle.Version, DeleteMode.Soft, now, atomicCt);

await identifierStore.DeleteByUserAsync(userKey, DeleteMode.Soft, now, atomicCt);

var sessionStore = _sessionStoreFactory.Create(context.ResourceTenant);
await sessionStore.RevokeAllChainsAsync(userKey, now, innerCt);
var profiles = await profileStore.GetAllProfilesByUserAsync(userKey, atomicCt);

foreach (var profile in profiles)
{
var key = new UserProfileKey(
context.ResourceTenant,
userKey,
profile.ProfileKey);

await profileStore.DeleteAsync(
key,
profile.Version,
DeleteMode.Soft,
now,
atomicCt);
}

foreach (var integration in _integrations)
{
await integration.OnUserDeletedAsync(
context.ResourceTenant,
userKey,
DeleteMode.Soft,
atomicCt);
}

var sessionStore = _sessionStoreFactory.Create(
context.ResourceTenant);

await sessionStore.ExecuteAsync(
sessionCt => sessionStore.RevokeAllChainsAsync(
userKey,
now,
sessionCt),
atomicCt);
}, innerCt);
});

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


public async Task<UserDeleteResult> DeleteUserAsync(AccessContext context, DeleteUserRequest request, CancellationToken ct = default)
{
var command = new AccessCommand<UserDeleteResult>(async innerCt =>
{
var targetUserKey = context.GetTargetUserKey();
var now = _clock.UtcNow;
var userLifecycleKey = new UserLifecycleKey(context.ResourceTenant, targetUserKey);
var lifecycleStore = _lifecycleStoreFactory.Create(context.ResourceTenant);
var lifecycle = await lifecycleStore.GetAsync(userLifecycleKey, innerCt);
return await _atomicExecutor.ExecuteAsync(async atomicCt =>
{
var targetUserKey = context.GetTargetUserKey();
var now = _clock.UtcNow;

if (lifecycle is null)
throw new UAuthNotFoundException();
var userLifecycleKey = new UserLifecycleKey(context.ResourceTenant, targetUserKey);

var profileStore = _profileStoreFactory.Create(context.ResourceTenant);
var identifierStore = _identifierStoreFactory.Create(context.ResourceTenant);
var lifecycleStore = _lifecycleStoreFactory.Create(context.ResourceTenant);

await lifecycleStore.DeleteAsync(userLifecycleKey, lifecycle.Version, request.Mode, now, innerCt);
await identifierStore.DeleteByUserAsync(targetUserKey, request.Mode, now, innerCt);
var profiles = await profileStore.GetAllProfilesByUserAsync(targetUserKey, innerCt);
var lifecycle = await lifecycleStore.GetAsync(userLifecycleKey, atomicCt);

foreach (var profile in profiles)
{
var key = new UserProfileKey(context.ResourceTenant, profile.UserKey, profile.ProfileKey);
if (lifecycle is null)
throw new UAuthNotFoundException();

await profileStore.DeleteAsync(key, profile.Version, request.Mode, now, innerCt);
}
var profileStore = _profileStoreFactory.Create(context.ResourceTenant);

foreach (var integration in _integrations)
{
await integration.OnUserDeletedAsync(context.ResourceTenant, targetUserKey, request.Mode, innerCt);
}
var identifierStore = _identifierStoreFactory.Create( context.ResourceTenant);

await lifecycleStore.DeleteAsync(
userLifecycleKey,
lifecycle.Version,
request.Mode,
now,
atomicCt);

await identifierStore.DeleteByUserAsync(
targetUserKey,
request.Mode,
now,
atomicCt);

var sessionStore = _sessionStoreFactory.Create(context.ResourceTenant);
var profiles = await profileStore.GetAllProfilesByUserAsync(
targetUserKey,
atomicCt);

await sessionStore.RevokeAllChainsAsync(targetUserKey, now, innerCt);
foreach (var profile in profiles)
{
var key = new UserProfileKey(
context.ResourceTenant,
profile.UserKey,
profile.ProfileKey);

await profileStore.DeleteAsync(
key,
profile.Version,
request.Mode,
now,
atomicCt);
}

return UserDeleteResult.Success(request.Mode);
foreach (var integration in _integrations)
{
await integration.OnUserDeletedAsync(
context.ResourceTenant,
targetUserKey,
request.Mode,
atomicCt);
}

var sessionStore = _sessionStoreFactory.Create(
context.ResourceTenant);

await sessionStore.ExecuteAsync(
sessionCt => sessionStore.RevokeAllChainsAsync(
targetUserKey,
now,
sessionCt),
atomicCt);

return UserDeleteResult.Success(request.Mode);
}, innerCt);
});

return await _accessOrchestrator.ExecuteAsync(context, command, ct);
return await _accessOrchestrator.ExecuteAsync(
context,
command,
ct);
}


#endregion


Expand Down Expand Up @@ -819,6 +884,7 @@ public async Task<UserIdentifierAvailabilityResult> CheckIdentifierAvailabilityA

#endregion


public async Task<PagedResult<UserSummary>> QueryUsersAsync(AccessContext context, UserQuery query, CancellationToken ct = default)
{
var command = new AccessCommand<PagedResult<UserSummary>>(
Expand Down
Loading
Loading