feat(auth): add Redis authorization caching
This commit is contained in:
105
Tiku.Infrastructure/Security/AuthorizationStateInvalidator.cs
Normal file
105
Tiku.Infrastructure/Security/AuthorizationStateInvalidator.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user