feat(api): аутентификация и авторизация. Кастомные ParrAuthenticationHandler и BaseSimpleRoleProvider.

This commit is contained in:
Mikhail Trubnikov
2023-12-01 16:41:37 +10:00
parent 78f2310199
commit d0c309e642
23 changed files with 395 additions and 11 deletions

View File

@@ -0,0 +1,8 @@
namespace PARR.API.Authentication.Models
{
public class UserCache
{
public required string UserIp { get; set; }
public List<UserRoleInCache> Roles { get; set; } = new List<UserRoleInCache>();
}
}

View File

@@ -0,0 +1,8 @@
namespace PARR.API.Authentication.Models
{
public class UserRoleInCache
{
public required string Name { get; set; }
public required string Description { get; set; }
}
}

View File

@@ -0,0 +1,58 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using PARR.API.Services.Interfaces;
using System.Security.Claims;
using System.Text.Encodings.Web;
namespace PARR.API.Authentication
{
public class ParrAuthenticationHandler : AuthenticationHandler<ParrAuthenticationOptions>
{
private readonly IServiceProvider serviceProvider;
public ParrAuthenticationHandler(
IOptionsMonitor<ParrAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
IServiceProvider serviceProvider) : base(options, logger, encoder, clock)
{
this.serviceProvider = serviceProvider;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var ipClient = Request.HttpContext.Connection.RemoteIpAddress?.MapToIPv4().ToString();
if (ipClient == null)
return AuthenticateResult.Fail($"IP address not defined.");
// Аутентификация - просто проверка, есть ли у нас такой пользователь
using (var scope = serviceProvider.CreateScope())
{
var authService = scope.ServiceProvider.GetRequiredService<IAuthService>();
var userIsBlocked = await authService.UserIsBlockedAsync(ipClient);
if (userIsBlocked)
return AuthenticateResult.Fail($"IP address is on the blocking list.");
var user = await authService.GetUserAsync(ipClient);
if (user == null)
return AuthenticateResult.Fail($"IP address not defined.");
// пользователь найден, аутентификация пройдена
var claims = new List<Claim> {
new Claim(ClaimTypes.Name, user.UserIp)
};
var claimsIdentity = new ClaimsIdentity(claims, Scheme.Name);
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
return AuthenticateResult.Success(new AuthenticationTicket(claimsPrincipal, Scheme.Name));
}
}
}
}

View File

@@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Authentication;
namespace PARR.API.Authentication
{
public class ParrAuthenticationOptions : AuthenticationSchemeOptions
{
public const string DefaultScheme = "ParrAuthenticationScheme";
//public string TokenHeaderName { get; set; } = "MyToken";
}
}

View File

@@ -1,8 +1,10 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using PARR.API.Contracts.V1; using PARR.API.Contracts.V1;
using PARR.API.Contracts.V1.Responses.Base; using PARR.API.Contracts.V1.Responses.Base;
using PARR.API.Controllers.V1.Base; using PARR.API.Controllers.V1.Base;
using PARR.API.Services.Interfaces; using PARR.API.Services.Interfaces;
using PARR.Constants;
using PARR.DAL.CacheServices; using PARR.DAL.CacheServices;
using PARR.DAL.TransformServices; using PARR.DAL.TransformServices;
@@ -26,13 +28,13 @@ namespace PARR.API.Controllers.V1
/// Получить мой ip /// Получить мой ip
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
//[Authorize(Roles = ParrRoles.Administrator.Role)]
[Authorize]
[HttpGet(ApiRoutes.Test.GetMyIp)] [HttpGet(ApiRoutes.Test.GetMyIp)]
public IActionResult GetMyIp() public IActionResult GetMyIp()
{ {
var ipAddress = clientService.GetClientIp(); var ipAddress = clientService.GetClientIp();
//var ipV4 = ipAddress?.MapToIPv4();
var ip = ipAddress?.ToString(); var ip = ipAddress?.ToString();
return Ok(new Response<object>(new { ip }, true)); return Ok(new Response<object>(new { ip }, true));

View File

@@ -20,7 +20,7 @@ namespace PARR.API.Installers
}); });
services.AddTransient<IClientService, ClientService>(); services.AddTransient<IClientService, ClientService>();
services.AddTransient<IAuthService, AuthService>();
} }

View File

@@ -13,10 +13,9 @@ namespace PARR.API.Installers
configuration.GetSection(nameof(MqSettings)).Bind(mqSettings); configuration.GetSection(nameof(MqSettings)).Bind(mqSettings);
services.AddSingleton(mqSettings); services.AddSingleton(mqSettings);
var userCacheSettings = new UserCacheSettings();
//var storageSettings = new StorageSettings(); configuration.GetSection(nameof(UserCacheSettings)).Bind(userCacheSettings);
//configuration.GetSection(nameof(StorageSettings)).Bind(storageSettings); services.AddSingleton(userCacheSettings);
//services.AddSingleton(storageSettings);
//TODO: add other //TODO: add other
} }

View File

@@ -1,6 +1,9 @@
using Elastic.CommonSchema.Serilog; using Elastic.CommonSchema.Serilog;
using FluentValidation; using FluentValidation;
using Microsoft.AspNetCore.Server.HttpSys;
using PARR.API.Authentication;
using PARR.API.Installers; using PARR.API.Installers;
using PARR.API.RoleProvider;
using PARR.BLL; using PARR.BLL;
using PARR.DAL; using PARR.DAL;
using Serilog; using Serilog;
@@ -29,6 +32,12 @@ builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
builder.Configuration.AddDalConfigurations(builder.Services); builder.Configuration.AddDalConfigurations(builder.Services);
builder.Services.AddDallSettings(builder.Configuration); builder.Services.AddDallSettings(builder.Configuration);
// Auth
builder.Services.AddAuthentication(ParrAuthenticationOptions.DefaultScheme)
.AddScheme<ParrAuthenticationOptions, ParrAuthenticationHandler>(ParrAuthenticationOptions.DefaultScheme, opt => { });
builder.Services.AddSimpleRoleAuthorization<BaseSimpleRoleProvider>();
builder.Services.AddHttpContextAccessor(); builder.Services.AddHttpContextAccessor();
builder.Services.AddControllers(); builder.Services.AddControllers();
@@ -44,6 +53,7 @@ var app = builder.Build();
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
app.InstallSwagger(); app.InstallSwagger();
app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.MapControllers(); app.MapControllers();

View File

@@ -0,0 +1,48 @@
using PARR.API.Services.Interfaces;
namespace PARR.API.RoleProvider
{
public class BaseSimpleRoleProvider : ISimpleRoleProvider
{
private readonly IServiceProvider serviceProvider;
public BaseSimpleRoleProvider(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider;
}
public async Task<ICollection<string>> GetUserRolesAsync(string ipClient)
{
// Авторизация - проверка на разрешения
// В данном случае, получаем все разрешения (роли)
ICollection<string> defaultResult = new string[0];
using (var scope = serviceProvider.CreateScope())
{
//var clientService = scope.ServiceProvider.GetRequiredService<IClientService>();
//var ipClient = clientService.GetClientIp()?.ToString();
//if (string.IsNullOrEmpty(ipClient))
// return await Task.FromResult(defaultResult);
var authService = scope.ServiceProvider.GetRequiredService<IAuthService>();
// проверяем блокировку пользователя
var userIsBlocked = await authService.UserIsBlockedAsync(ipClient);
if (userIsBlocked)
return await Task.FromResult(defaultResult);
// получаем пользователя из кэша или бд
var user = await authService.GetUserAsync(ipClient);
if (user == null)
return await Task.FromResult(defaultResult);
return user.Roles.Select(t => t.Name).ToList();
}
// return await Task.FromResult(defaultResult);
}
}
}

View File

@@ -0,0 +1,7 @@
namespace PARR.API.RoleProvider
{
public interface ISimpleRoleProvider
{
Task<ICollection<string>> GetUserRolesAsync(string ipClient);
}
}

View File

@@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Authentication;
namespace PARR.API.RoleProvider
{
public static class SimpleRoleAuthorizationServiceCollectionExtensions
{
public static void AddSimpleRoleAuthorization<TRoleProvider>(this IServiceCollection services)
where TRoleProvider : class, ISimpleRoleProvider
{
services.AddSingleton<ISimpleRoleProvider, TRoleProvider>();
services.AddSingleton<IClaimsTransformation, SimpleRoleAuthorizationTransform>();
}
}
}

View File

@@ -0,0 +1,39 @@
using Microsoft.AspNetCore.Authentication;
using System.Security.Claims;
namespace PARR.API.RoleProvider
{
public class SimpleRoleAuthorizationTransform : IClaimsTransformation
{
//private static readonly string RoleClaimType = $"http://{typeof(SimpleRoleAuthorizationTransform).FullName.Replace('.', '/')}/role";
private static readonly string RoleClaimType = ClaimTypes.Role;
private readonly ISimpleRoleProvider roleProvider;
public SimpleRoleAuthorizationTransform(ISimpleRoleProvider roleProvider)
{
this.roleProvider = roleProvider ?? throw new ArgumentNullException(nameof(roleProvider));
}
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
// Cast the principal identity to a Claims identity to access claims etc...
var oldIdentity = (ClaimsIdentity)principal.Identity!;
// "Clone" the old identity to avoid nasty side effects.
// NB: We take a chance to replace the claim type used to define the roles with our own.
var newIdentity = new ClaimsIdentity(
oldIdentity.Claims,
oldIdentity.AuthenticationType,
oldIdentity.NameClaimType,
RoleClaimType);
// Fetch the roles for the user and add the claims of the correct type so that roles can be recognized.
var roles = await roleProvider.GetUserRolesAsync(newIdentity.Name!);
newIdentity.AddClaims(roles.Select(r => new Claim(RoleClaimType, r)));
// Create and return a new claims principal
return new ClaimsPrincipal(newIdentity);
}
}
}

View File

@@ -0,0 +1,87 @@
using PARR.API.Authentication.Models;
using PARR.API.Services.Interfaces;
using PARR.API.Settings;
using PARR.Constants;
using PARR.DAL.CacheServices;
using PARR.DAL.Services.Interfaces;
namespace PARR.API.Services.Implementations
{
public class AuthService : IAuthService
{
private readonly IRedisCacheService cacheService;
private readonly IUserService userService;
private readonly IHostService hostService;
private readonly UserCacheSettings userCacheSettings;
public AuthService(IRedisCacheService cacheService, IUserService userService, IHostService hostService, UserCacheSettings userCacheSettings)
{
this.cacheService = cacheService;
this.userService = userService;
this.hostService = hostService;
this.userCacheSettings = userCacheSettings;
}
public async Task<bool> UserIsBlockedAsync(string ipAddress)
{
//пользователь есть в списке блокировки?
var existUserInBlockList = await cacheService.GetCachedDataAsync<DateTimeOffset?>(GetBlockKey(ipAddress));
return existUserInBlockList != null;
}
public async Task<UserCache?> GetUserAsync(string ipAddress)
{
// смотрим, есть ли в кэше
var userInCache = await cacheService.GetCachedDataAsync<UserCache>(GetAllowKey(ipAddress));
if (userInCache != null)
return userInCache;
// нет в кэше
var roles = new List<UserRoleInCache>();
var user = await userService.GetByIpWithRolesAsync(ipAddress);
if (user != null)
user.Roles.ToList().ForEach(t =>
{
if (t.Role != null)
roles.Add(new UserRoleInCache { Name = t.Role.Name, Description = t.Role.Description });
});
var host = await hostService.GetByIpAsync(ipAddress);
if (host != null)
roles.Add(new UserRoleInCache { Name = ParrRoles.Agent.Role, Description = ParrRoles.Agent.Description });
if (user == null && host == null)
{
// если это не пользователь и не хост, добавляем в кэш на блокировку
await cacheService.SetCachedDataAsync(GetBlockKey(ipAddress), DateTimeOffset.UtcNow, userCacheSettings.UserBlockTtl);
return null;
}
// пользователь есть, добавляем в КЭШ и возвращаем роли
var cacheData = new UserCache { UserIp = ipAddress, Roles = roles };
await cacheService.SetCachedDataAsync(GetAllowKey(ipAddress), cacheData, userCacheSettings.UserCacheTtl);
return cacheData;
}
private string GetBlockKey(string ipAddress)
{
// d - deny
return $"d_{ipAddress}";
}
private string GetAllowKey(string ipAddress)
{
// a - allow
return $"a_{ipAddress}";
}
}
}

View File

@@ -0,0 +1,22 @@
using PARR.API.Authentication.Models;
namespace PARR.API.Services.Interfaces
{
public interface IAuthService
{
/// <summary>
/// Получить пользователя с ролями по ip адресу.
/// Смотрим в кэше, затем в БД. При необходимости блокирует или добавлет в кэш.
/// </summary>
/// <param name="ipAddress"></param>
/// <returns></returns>
Task<UserCache?> GetUserAsync(string ipAddress);
/// <summary>
/// Проверяет, блокировку пользователя
/// </summary>
/// <param name="ipAddress"></param>
/// <returns></returns>
Task<bool> UserIsBlockedAsync(string ipAddress);
}
}

View File

@@ -0,0 +1,18 @@
namespace PARR.API.Settings
{
/// <summary>
/// Настройки кэша пользователй
/// </summary>
public class UserCacheSettings
{
/// <summary>
/// Сколько хранить в кэше авторизованных пользователей
/// </summary>
public TimeSpan UserCacheTtl { get; set; }
/// <summary>
/// Сколько хранить в кэше заблокированных пользователей (как долго блокировать пользователя)
/// </summary>
public TimeSpan UserBlockTtl { get; set; }
}
}

View File

@@ -1,6 +1,6 @@
{ {
"ConnectionStrings": { "ConnectionStrings": {
"RedisConnection": "10.99.253.216:6379" "RedisConnection": "10.99.253.216:6379,password=ParrP@ssPtk202MMdevDvs"
}, },
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {

View File

@@ -45,5 +45,9 @@
"User": "generate_templates_api", "User": "generate_templates_api",
"Password": "DHhjgdsf*&%95" "Password": "DHhjgdsf*&%95"
} }
},
"UserCacheSettings": {
"UserCacheTtl": "00:05:00",
"UserBlockTtl": "00:03:00"
} }
} }

View File

@@ -62,6 +62,7 @@ namespace PARR.DAL
services.AddTransient<IAgentHistoryService, AgentHistoryService>(); services.AddTransient<IAgentHistoryService, AgentHistoryService>();
services.AddTransient<IOrderService, OrderService>(); services.AddTransient<IOrderService, OrderService>();
services.AddTransient<IOrderStatusService, OrderStatusService>(); services.AddTransient<IOrderStatusService, OrderStatusService>();
services.AddTransient<IUserService, UserService>();
// TransformServices // TransformServices

View File

@@ -40,6 +40,11 @@ namespace PARR.DAL.Services.Implementations
.FirstOrDefaultAsync(h => h.IP == IP); .FirstOrDefaultAsync(h => h.IP == IP);
} }
public async Task<Host?> GetByIpAsync(string ip)
{
return await EntitySet.FirstOrDefaultAsync(t => t.IP == ip);
}
//public async Task<Host?> GetHostByRegionalEKAsync(string RegionalEK)И //public async Task<Host?> GetHostByRegionalEKAsync(string RegionalEK)И
//{ //{
// try // try

View File

@@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.DAL.Context;
using PARR.DAL.Models;
using PARR.DAL.Services.Abstracts;
using PARR.DAL.Services.Interfaces;
namespace PARR.DAL.Services.Implementations
{
internal class UserService : BaseService<User>, IUserService
{
private readonly DataContext dataContext;
public UserService(DataContext dataContext, ILogger<UserService> logger) : base(logger)
{
this.dataContext = dataContext;
}
protected override DbSet<User> EntitySet => dataContext.Users;
protected override DataContext EntitiContext => dataContext;
public async Task<User?> GetByIpWithRolesAsync(string ipAddress)
{
return await EntitySet
.Include(t => t.Roles).ThenInclude(t => t.Role)
.FirstOrDefaultAsync(t => t.Ip == ipAddress);
}
}
}

View File

@@ -5,6 +5,10 @@ namespace PARR.DAL.Services.Interfaces
{ {
public interface IHostService : IBaseService<Host> public interface IHostService : IBaseService<Host>
{ {
Task<Host?> GetHostWithAppsAsync(string IP); Task<Host?> GetHostWithAppsAsync(string ip);
Task<Host?> GetByIpAsync(string ip);
} }
} }

View File

@@ -0,0 +1,10 @@
using PARR.DAL.Models;
using PARR.DAL.Services.Interfaces.Base;
namespace PARR.DAL.Services.Interfaces
{
public interface IUserService: IBaseService<User>
{
Task<User?> GetByIpWithRolesAsync(string ipAddress);
}
}