using Microsoft.EntityFrameworkCore; using Tiku.Application.Security; using Tiku.Domain.Operations; using Tiku.Domain.Tenancy; using Tiku.Infrastructure.Persistence; namespace Tiku.Infrastructure.Security; internal sealed class AuthorizationStateInvalidator( TikuDbContext dbContext, IAccessSecurityCache cache) : IAuthorizationStateInvalidator { public Task InvalidateSessionAsync(Guid sessionId, CancellationToken cancellationToken = default) { return ExecuteAsync("session", null, null, sessionId, null, null, token => cache.InvalidateSessionAsync(sessionId, token), cancellationToken); } public Task InvalidateUserAsync(Guid userId, CancellationToken cancellationToken = default) { return ExecuteAsync("user", null, userId, null, null, null, token => cache.InvalidateUserAsync(userId, token), cancellationToken); } public Task InvalidateTenantAsync(Guid tenantId, CancellationToken cancellationToken = default) { return ExecuteAsync("tenant", tenantId, null, null, AuthRealm.Tenant, null, token => cache.InvalidateTenantAsync(tenantId, token), cancellationToken); } public Task InvalidateMembershipAsync(Guid tenantId, Guid userId, CancellationToken cancellationToken = default) { return ExecuteAsync("membership", tenantId, userId, null, AuthRealm.Tenant, null, token => cache.InvalidateMembershipAsync(tenantId, userId, token), cancellationToken); } public async Task BumpScopeAsync(AuthRealm realm, Guid? tenantId, CancellationToken cancellationToken = default) { var updated = await dbContext.AuthorizationScopeVersions .Where(item => item.Realm == realm && item.TenantId == tenantId) .ExecuteUpdateAsync(setters => setters .SetProperty(item => item.Version, item => item.Version + 1) .SetProperty(item => item.UpdatedAt, DateTimeOffset.UtcNow), cancellationToken); if (updated == 0) { dbContext.AuthorizationScopeVersions.Add(new AuthorizationScopeVersion { Realm = realm, TenantId = tenantId, Version = 2 }); await dbContext.SaveChangesAsync(cancellationToken); } var version = await dbContext.AuthorizationScopeVersions.AsNoTracking() .Where(item => item.Realm == realm && item.TenantId == tenantId) .Select(item => item.Version) .SingleAsync(cancellationToken); await ExecuteAsync("scope", tenantId, null, null, realm, version, token => cache.SetAuthorizationVersionAsync(realm, tenantId, version, token), cancellationToken); return version; } private async Task ExecuteAsync( string targetType, Guid? tenantId, Guid? userId, Guid? sessionId, AuthRealm? realm, long? version, Func operation, CancellationToken cancellationToken) { if (!cache.IsConfigured) return; var invalidation = new AuthorizationCacheInvalidation { TargetType = targetType, TenantId = tenantId, UserId = userId, SessionId = sessionId, Realm = realm, Version = version }; dbContext.AuthorizationCacheInvalidations.Add(invalidation); await dbContext.SaveChangesAsync(cancellationToken); try { await operation(cancellationToken); invalidation.ProcessedAt = DateTimeOffset.UtcNow; invalidation.AttemptCount++; await dbContext.SaveChangesAsync(cancellationToken); AuthorizationCacheTelemetry.Invalidated(targetType, true); } catch (Exception exception) when (exception is not OperationCanceledException) { invalidation.AttemptCount++; invalidation.LastError = exception.Message; await dbContext.SaveChangesAsync(CancellationToken.None); AuthorizationCacheTelemetry.Invalidated(targetType, false); if (dbContext.Database.CurrentTransaction is not null) return; throw new AuthorizationSecurityUnavailableException(exception); } } } internal sealed class NullAuthorizationStateInvalidator : IAuthorizationStateInvalidator { public Task InvalidateSessionAsync(Guid sessionId, CancellationToken cancellationToken = default) { return Task.CompletedTask; } public Task InvalidateUserAsync(Guid userId, CancellationToken cancellationToken = default) { return Task.CompletedTask; } public Task InvalidateTenantAsync(Guid tenantId, CancellationToken cancellationToken = default) { return Task.CompletedTask; } public Task InvalidateMembershipAsync(Guid tenantId, Guid userId, CancellationToken cancellationToken = default) { return Task.CompletedTask; } public Task BumpScopeAsync(AuthRealm realm, Guid? tenantId, CancellationToken cancellationToken = default) { return Task.FromResult(1L); } }