Files
tiku-backend.net/Tiku.Infrastructure/Security/AuthorizationStateInvalidator.cs

106 lines
4.9 KiB
C#

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) =>
ExecuteAsync("session", null, null, sessionId, null, null,
token => cache.InvalidateSessionAsync(sessionId, token), cancellationToken);
public Task InvalidateUserAsync(Guid userId, CancellationToken cancellationToken = default) =>
ExecuteAsync("user", null, userId, null, null, null,
token => cache.InvalidateUserAsync(userId, token), cancellationToken);
public Task InvalidateTenantAsync(Guid tenantId, CancellationToken cancellationToken = default) =>
ExecuteAsync("tenant", tenantId, null, null, AuthRealm.Tenant, null,
token => cache.InvalidateTenantAsync(tenantId, token), cancellationToken);
public Task InvalidateMembershipAsync(Guid tenantId, Guid userId, CancellationToken cancellationToken = default) =>
ExecuteAsync("membership", tenantId, userId, null, AuthRealm.Tenant, null,
token => cache.InvalidateMembershipAsync(tenantId, userId, token), cancellationToken);
public async Task<long> 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<CancellationToken, Task> 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) => Task.CompletedTask;
public Task InvalidateUserAsync(Guid userId, CancellationToken cancellationToken = default) => Task.CompletedTask;
public Task InvalidateTenantAsync(Guid tenantId, CancellationToken cancellationToken = default) => Task.CompletedTask;
public Task InvalidateMembershipAsync(Guid tenantId, Guid userId, CancellationToken cancellationToken = default) => Task.CompletedTask;
public Task<long> BumpScopeAsync(AuthRealm realm, Guid? tenantId, CancellationToken cancellationToken = default) => Task.FromResult(1L);
}