sozsoft-platform/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformSessionValidationMiddleware.cs

128 lines
4.6 KiB
C#
Raw Normal View History

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Data.SqlClient;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Sozsoft.Platform.DbStartup;
using Volo.Abp;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Identity;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Security.Claims;
using Volo.Abp.Uow;
namespace Sozsoft.Platform.Identity;
public class PlatformSessionValidationMiddleware : IMiddleware, ITransientDependency
{
private const string SetupPathPrefix = "/api/setup";
private const string SetupStatusPath = "/api/setup/application-status";
private readonly IIdentitySessionRepository sessionRepository;
private readonly ICurrentTenant currentTenant;
private readonly IUnitOfWorkManager unitOfWorkManager;
private readonly IConfiguration configuration;
public PlatformSessionValidationMiddleware(
IIdentitySessionRepository sessionRepository,
ICurrentTenant currentTenant,
IUnitOfWorkManager unitOfWorkManager,
IConfiguration configuration)
{
this.sessionRepository = sessionRepository;
this.currentTenant = currentTenant;
this.unitOfWorkManager = unitOfWorkManager;
this.configuration = configuration;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (ShouldSkipSessionValidation(context))
{
await next(context);
return;
}
if (context.User.Identity?.IsAuthenticated != true)
{
await next(context);
return;
}
var userIdValue = context.User.FindFirst(AbpClaimTypes.UserId)?.Value;
var sessionId = context.User.FindFirst(PlatformClaimTypes.SessionId.Key)?.Value;
if (!Guid.TryParse(userIdValue, out var userId) || sessionId.IsNullOrWhiteSpace())
{
await next(context);
return;
}
var tenantId = ParseTenantId(context.User.FindFirst(AbpClaimTypes.TenantId)?.Value);
using (currentTenant.Change(tenantId))
{
try
{
using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false);
var sessions = await sessionRepository.GetListAsync(userId: userId);
var isCurrentSession = sessions.Any(session => session.SessionId == sessionId);
await uow.CompleteAsync();
if (!isCurrentSession)
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return;
}
}
catch (Exception ex) when (IsDatabaseUnavailableException(ex))
{
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
return;
}
}
await next(context);
}
/// <summary>
/// Kurulum endpointleri için oturum doğrulaması ne zaman atlanır.
///
/// - Durum sorgusu: anonim erişime açık, oturum bilgisine hiç ihtiyaç duymaz.
/// - Diğer /api/setup istekleri: yalnızca veritabanı erişilemezken atlanır. O durumda
/// oturum zaten doğrulanamaz (kayıtlar veritabanında) ve doğrulamaya çalışmak, tam da
/// veritabanını onaracak endpoint'i 503 ile bloke eder. Güvenlik kaybı yoktur: veritabanı
/// düşükken migration yalnızca kurulum parolasıyla çalışır, oturum yetki vermez
/// (bkz. SetupController.AuthorizeMigrateRequestAsync).
///
/// Veritabanı ayaktayken kurulum endpointleri de normal oturum doğrulamasından geçer;
/// böylece iptal edilmiş bir oturum migration tetikleyemez.
/// </summary>
private bool ShouldSkipSessionValidation(HttpContext context)
{
var path = context.Request.Path;
if (!path.StartsWithSegments(SetupPathPrefix))
{
return false;
}
return path.StartsWithSegments(SetupStatusPath) ||
!DatabaseReadinessChecker.GetStatus(configuration).IsReady;
}
private static Guid? ParseTenantId(string value)
{
return !string.IsNullOrWhiteSpace(value) && Guid.TryParse(value, out var id) ? id : null;
}
private static bool IsDatabaseUnavailableException(Exception exception)
{
return exception is SqlException ||
exception is TimeoutException ||
exception is TaskCanceledException ||
exception is InvalidOperationException && exception.InnerException != null &&
IsDatabaseUnavailableException(exception.InnerException);
}
}