106 lines
4.9 KiB
C#
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);
|
|
}
|