322 lines
11 KiB
C#
322 lines
11 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Data.Common;
|
||
using System.Linq;
|
||
using System.Threading.Tasks;
|
||
using Sozsoft.Platform.Entities;
|
||
using Sozsoft.Platform.Extensions;
|
||
using Microsoft.AspNetCore.Authentication;
|
||
using Microsoft.AspNetCore.Http;
|
||
using Microsoft.AspNetCore.Identity;
|
||
using Microsoft.Extensions.Logging;
|
||
using Microsoft.Extensions.Options;
|
||
using Volo.Abp.Domain.Repositories;
|
||
using Volo.Abp.Identity;
|
||
using Volo.Abp.Identity.AspNetCore;
|
||
using Volo.Abp.MultiTenancy;
|
||
using Volo.Abp.Settings;
|
||
using Volo.Abp.TenantManagement;
|
||
using Volo.Abp.Timing;
|
||
using IdentityUser = Volo.Abp.Identity.IdentityUser;
|
||
using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
|
||
|
||
namespace Sozsoft.Platform.Identity;
|
||
|
||
public interface IPlatformSignInManager
|
||
{
|
||
Task<bool> PreSignInCheckAsync(IdentityUser user);
|
||
Task<bool> CheckConcurrentLimitAsync(IdentityUser user);
|
||
}
|
||
|
||
public class PlatformSignInManager : AbpSignInManager, IPlatformSignInManager
|
||
{
|
||
private readonly IClock clock;
|
||
private readonly IRepository<IpRestriction, Guid> repositoryIp;
|
||
private readonly IRepository<WorkHour, Guid> repositoryWorkHour;
|
||
private readonly ITenantRepository tenantRepository;
|
||
private readonly IdentityUserManager userManager;
|
||
private readonly IIdentitySessionRepository identitySessionRepository;
|
||
private readonly ICurrentTenant currentTenant;
|
||
|
||
public PlatformSignInManager(
|
||
IdentityUserManager userManager,
|
||
IHttpContextAccessor contextAccessor,
|
||
IUserClaimsPrincipalFactory<IdentityUser> claimsFactory,
|
||
IOptions<IdentityOptions> optionsAccessor,
|
||
ILogger<SignInManager<IdentityUser>> logger,
|
||
IAuthenticationSchemeProvider schemes,
|
||
IUserConfirmation<IdentityUser> confirmation,
|
||
IOptions<AbpIdentityOptions> options,
|
||
ISettingProvider settingProvider,
|
||
IClock clock,
|
||
IRepository<IpRestriction, Guid> repositoryIp,
|
||
IRepository<WorkHour, Guid> repositoryWorkHour,
|
||
ITenantRepository tenantRepository,
|
||
IIdentitySessionRepository identitySessionRepository,
|
||
ICurrentTenant currentTenant
|
||
) : base(
|
||
userManager,
|
||
contextAccessor,
|
||
claimsFactory,
|
||
optionsAccessor,
|
||
logger,
|
||
schemes,
|
||
confirmation,
|
||
options,
|
||
settingProvider)
|
||
{
|
||
this.clock = clock;
|
||
this.repositoryIp = repositoryIp;
|
||
this.repositoryWorkHour = repositoryWorkHour;
|
||
this.tenantRepository = tenantRepository;
|
||
this.userManager = userManager;
|
||
this.identitySessionRepository = identitySessionRepository;
|
||
this.currentTenant = currentTenant;
|
||
}
|
||
|
||
public async Task<bool> PreSignInCheckAsync(IdentityUser user)
|
||
{
|
||
var result = await PreSignInCheck(user);
|
||
return result?.Succeeded ?? true;
|
||
}
|
||
|
||
protected override async Task<SignInResult> PreSignInCheck(IdentityUser user)
|
||
{
|
||
var result = await base.PreSignInCheck(user);
|
||
|
||
if (result == null)
|
||
{
|
||
if (!await CanSignInVerifiedAsync(user))
|
||
{
|
||
return new PlatformSignInResult() { IsNotAllowed_NotVerified = true };
|
||
}
|
||
if (!CanSignInEndDate(user))
|
||
{
|
||
return new PlatformSignInResult() { IsNotAllowed_LoginEndDateDue = true };
|
||
}
|
||
if (!await CanSignInIpAsync(user))
|
||
{
|
||
return new PlatformSignInResult() { IsNotAllowed_NotAllowedIp = true };
|
||
}
|
||
if (!await CanSignInTenantActiveAsync(user))
|
||
{
|
||
return new PlatformSignInResult() { IsNotAllowed_TenantIsPassive = true };
|
||
}
|
||
if (!await CanSignInWorkHourAsync(user))
|
||
{
|
||
return new PlatformSignInResult() { IsNotAllowed_WorkHour = true };
|
||
}
|
||
if (!await CheckConcurrentLimitAsync(user))
|
||
{
|
||
return new PlatformSignInResult() { IsNotAllowed_ConcurrentUserLimit = true };
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Bu kontrol modules\identity\src\Volo.Abp.Identity.AspNetCore\Volo\Abp\Identity\AspNetCore\AbpSignInManager.cs
|
||
// içinde zaten yapılıyor fakat her senaryoya NotAllowed diyor, bizim ayrıt etmemiz gerektiği için tekrar yapıyoruz.
|
||
if (user.ShouldChangePasswordOnNextLogin)
|
||
{
|
||
return new PlatformSignInResult() { ShouldChangePasswordOnNextLogin = true };
|
||
}
|
||
if (await userManager.ShouldPeriodicallyChangePasswordAsync(user))
|
||
{
|
||
return new PlatformSignInResult() { ShouldChangePasswordPeriodic = true };
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Used to prevent login outside of defined work hours.
|
||
/// </summary>
|
||
private async Task<bool> CanSignInWorkHourAsync(IdentityUser user)
|
||
{
|
||
var workHourName = user.GetWorkHour();
|
||
|
||
if (string.IsNullOrWhiteSpace(workHourName))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
var workHours = await repositoryWorkHour.GetListAsync(a => a.Name == workHourName);
|
||
|
||
if (workHours.IsNullOrEmpty())
|
||
{
|
||
return true;
|
||
}
|
||
|
||
var now = clock.Now;
|
||
var currentTime = now.TimeOfDay;
|
||
var dayOfWeek = now.DayOfWeek;
|
||
|
||
var isAllowed = workHours.Any(wh =>
|
||
{
|
||
var dayMatches = dayOfWeek switch
|
||
{
|
||
DayOfWeek.Monday => wh.Monday == true,
|
||
DayOfWeek.Tuesday => wh.Tuesday == true,
|
||
DayOfWeek.Wednesday => wh.Wednesday == true,
|
||
DayOfWeek.Thursday => wh.Thursday == true,
|
||
DayOfWeek.Friday => wh.Friday == true,
|
||
DayOfWeek.Saturday => wh.Saturday == true,
|
||
DayOfWeek.Sunday => wh.Sunday == true,
|
||
_ => false
|
||
};
|
||
|
||
if (!dayMatches) return false;
|
||
|
||
return currentTime >= wh.StartTime.TimeOfDay && currentTime <= wh.EndTime.TimeOfDay;
|
||
});
|
||
|
||
if (!isAllowed)
|
||
{
|
||
Logger.LogWarning(PlatformEventIds.UserCannotSignInWorkHour, "User cannot sign in outside work hours.");
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Tenant IsActive
|
||
/// </summary>
|
||
private async Task<bool> CanSignInTenantActiveAsync(IdentityUser user)
|
||
{
|
||
if (!user.TenantId.HasValue)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
var tenant = await tenantRepository.FindAsync(user.TenantId.Value);
|
||
if (tenant == null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (!tenant.GetIsActive())
|
||
{
|
||
Logger.LogWarning(PlatformEventIds.UserCannotSignInTenantIsPassive, "Tenant is inactive.");
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Used to prevent login for additional scenarios;
|
||
/// Account Not Verified
|
||
/// </summary>
|
||
private async Task<bool> CanSignInVerifiedAsync(IdentityUser user)
|
||
{
|
||
var rva = await SettingProvider.GetAsync<bool>(PlatformConsts.AbpIdentity.Profile.General.RequireVerifiedAccount);
|
||
if (rva && !user.GetIsVerified())
|
||
{
|
||
Logger.LogWarning(PlatformEventIds.UserCannotSignInWithoutVerifiedAccount, "User cannot sign in without a verified account.");
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Used to prevent login if end date is due
|
||
/// </summary>
|
||
private bool CanSignInEndDate(IdentityUser user)
|
||
{
|
||
var endDate = user.GetLoginEndDate();
|
||
if (endDate != null && clock.Now > endDate)
|
||
{
|
||
//izin yok
|
||
Logger.LogWarning(PlatformEventIds.UserCannotSignInLoginEndDateDue, "User cannot sign in, login end date is due.");
|
||
return false;
|
||
}
|
||
|
||
//izin ver
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Used to prevent login for allowed IPs;
|
||
/// IP Not Allowed
|
||
/// </summary>
|
||
private async Task<bool> CanSignInIpAsync(IdentityUser user)
|
||
{
|
||
// User, Role, Global Restrictions
|
||
var roles = await UserManager.GetRolesAsync(user);
|
||
//var roleIds = roleManager.Roles.Where(a => roles.Contains(a.Name)).Select(a => a.Id).ToArray();
|
||
|
||
var restrictions = await repositoryIp.GetListAsync(a =>
|
||
(a.ResourceType == "User" && a.ResourceId == user.UserName) ||
|
||
(a.ResourceType == "Role" && roles.Contains(a.ResourceId)) ||
|
||
(a.ResourceType == "Global"));
|
||
|
||
if (!restrictions.IsNullOrEmpty())
|
||
{
|
||
var ip = Context.GetRemoteIPAddress();
|
||
var ipString = ip?.ToString();
|
||
if (ip == null || (ip != null && !restrictions.Select(a => a.IP).Any(a => a == ipString)))
|
||
{
|
||
//izin yok
|
||
Logger.LogWarning(PlatformEventIds.UserCannotSignInWithoutAllowedIp, "User cannot sign in without an allowed IP.");
|
||
return false;
|
||
}
|
||
|
||
}
|
||
|
||
//izin ver
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Tenant'a tanımlı MaxConcurrentUsers limitini aşmamak için login'i engeller.
|
||
/// AbpSessions tablosundaki aktif oturumları sayar (farklı UserId'lere göre distinct).
|
||
/// Kullanıcının kendisinin zaten oturumu varsa (refresh senaryosu) yeni concurrent user sayılmaz.
|
||
/// </summary>
|
||
public async Task<bool> CheckConcurrentLimitAsync(IdentityUser user)
|
||
{
|
||
if (!user.TenantId.HasValue)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
var tenant = await tenantRepository.FindAsync(user.TenantId.Value);
|
||
if (tenant == null)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
var maxConcurrentUsers = tenant.GetMaxConcurrentUsers();
|
||
if (!maxConcurrentUsers.HasValue || maxConcurrentUsers.Value == 0)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
// Tenant bağlamını explicit olarak set et — hem password hem refresh token akışlarında
|
||
// doğru tenant izolasyonu için güvenli bir şekilde değiştirilir.
|
||
using (currentTenant.Change(user.TenantId))
|
||
{
|
||
var sessions = await identitySessionRepository.GetListAsync();
|
||
|
||
// Bu kullanıcı hariç diğer distinct aktif kullanıcı sayısını hesapla.
|
||
// Kullanıcının kendi oturumu varsa (refresh) o kişi tekrar sayılmaz.
|
||
var otherActiveUserCount = sessions
|
||
.Where(s => s.UserId != user.Id)
|
||
.Select(s => s.UserId)
|
||
.Distinct()
|
||
.Count();
|
||
|
||
if (otherActiveUserCount >= maxConcurrentUsers.Value)
|
||
{
|
||
Logger.LogWarning(PlatformEventIds.UserCannotSignInConcurrentUserLimit,
|
||
"Tenant {TenantId} concurrent user limit of {Limit} reached. Active users: {ActiveCount}.",
|
||
user.TenantId, maxConcurrentUsers.Value, otherActiveUserCount);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
}
|
||
|