From d0c309e642a8e596385bcf45b86d2f2347d530f9 Mon Sep 17 00:00:00 2001 From: Mikhail Trubnikov Date: Fri, 1 Dec 2023 16:41:37 +1000 Subject: [PATCH] =?UTF-8?q?feat(api):=20=D0=B0=D1=83=D1=82=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B8=D1=84=D0=B8=D0=BA=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B8?= =?UTF-8?q?=20=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=B8=D0=B7=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8F.=20=D0=9A=D0=B0=D1=81=D1=82=D0=BE=D0=BC=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20ParrAuthenticationHandler=20=D0=B8=20BaseSimpleRolePro?= =?UTF-8?q?vider.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PARR.API/Authentication/Models/UserCache.cs | 8 ++ .../Authentication/Models/UserRoleInCache.cs | 8 ++ .../ParrAuthenticationHandler.cs | 58 +++++++++++++ .../ParrAuthenticationOptions.cs | 10 +++ PARR.API/Controllers/V1/TestController.cs | 8 +- PARR.API/Installers/ApiServicesInstaller.cs | 2 +- PARR.API/Installers/SettingsInstaller.cs | 7 +- PARR.API/Program.cs | 10 +++ .../RoleProvider/BaseSimpleRoleProvider.cs | 48 ++++++++++ PARR.API/RoleProvider/ISimpleRoleProvider.cs | 7 ++ ...uthorizationServiceCollectionExtensions.cs | 14 +++ .../SimpleRoleAuthorizationTransform.cs | 39 +++++++++ .../Services/Implementations/AuthService.cs | 87 +++++++++++++++++++ PARR.API/Services/Interfaces/IAuthService.cs | 22 +++++ PARR.API/Settings/UserCacheSettings.cs | 18 ++++ PARR.API/appsettings.Development.json | 2 +- PARR.API/appsettings.json | 4 + PARR.DAL/CacheServices/RedisCacheService.cs | 2 +- PARR.DAL/ParrDalInstaller.cs | 1 + .../Services/Implementations/HostService.cs | 5 ++ .../Services/Implementations/UserService.cs | 30 +++++++ PARR.DAL/Services/Interfaces/IHostService.cs | 6 +- PARR.DAL/Services/Interfaces/IUserService.cs | 10 +++ 23 files changed, 395 insertions(+), 11 deletions(-) create mode 100644 PARR.API/Authentication/Models/UserCache.cs create mode 100644 PARR.API/Authentication/Models/UserRoleInCache.cs create mode 100644 PARR.API/Authentication/ParrAuthenticationHandler.cs create mode 100644 PARR.API/Authentication/ParrAuthenticationOptions.cs create mode 100644 PARR.API/RoleProvider/BaseSimpleRoleProvider.cs create mode 100644 PARR.API/RoleProvider/ISimpleRoleProvider.cs create mode 100644 PARR.API/RoleProvider/SimpleRoleAuthorizationServiceCollectionExtensions.cs create mode 100644 PARR.API/RoleProvider/SimpleRoleAuthorizationTransform.cs create mode 100644 PARR.API/Services/Implementations/AuthService.cs create mode 100644 PARR.API/Services/Interfaces/IAuthService.cs create mode 100644 PARR.API/Settings/UserCacheSettings.cs create mode 100644 PARR.DAL/Services/Implementations/UserService.cs create mode 100644 PARR.DAL/Services/Interfaces/IUserService.cs diff --git a/PARR.API/Authentication/Models/UserCache.cs b/PARR.API/Authentication/Models/UserCache.cs new file mode 100644 index 00000000..5f4aa2fc --- /dev/null +++ b/PARR.API/Authentication/Models/UserCache.cs @@ -0,0 +1,8 @@ +namespace PARR.API.Authentication.Models +{ + public class UserCache + { + public required string UserIp { get; set; } + public List Roles { get; set; } = new List(); + } +} diff --git a/PARR.API/Authentication/Models/UserRoleInCache.cs b/PARR.API/Authentication/Models/UserRoleInCache.cs new file mode 100644 index 00000000..2d53955c --- /dev/null +++ b/PARR.API/Authentication/Models/UserRoleInCache.cs @@ -0,0 +1,8 @@ +namespace PARR.API.Authentication.Models +{ + public class UserRoleInCache + { + public required string Name { get; set; } + public required string Description { get; set; } + } +} diff --git a/PARR.API/Authentication/ParrAuthenticationHandler.cs b/PARR.API/Authentication/ParrAuthenticationHandler.cs new file mode 100644 index 00000000..cb3b845c --- /dev/null +++ b/PARR.API/Authentication/ParrAuthenticationHandler.cs @@ -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 + { + private readonly IServiceProvider serviceProvider; + + public ParrAuthenticationHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock, + IServiceProvider serviceProvider) : base(options, logger, encoder, clock) + { + this.serviceProvider = serviceProvider; + } + + protected override async Task 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(); + + 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 { + 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)); + } + } + } +} diff --git a/PARR.API/Authentication/ParrAuthenticationOptions.cs b/PARR.API/Authentication/ParrAuthenticationOptions.cs new file mode 100644 index 00000000..38506e62 --- /dev/null +++ b/PARR.API/Authentication/ParrAuthenticationOptions.cs @@ -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"; + } +} diff --git a/PARR.API/Controllers/V1/TestController.cs b/PARR.API/Controllers/V1/TestController.cs index 142d1cd9..3d98381e 100644 --- a/PARR.API/Controllers/V1/TestController.cs +++ b/PARR.API/Controllers/V1/TestController.cs @@ -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.Responses.Base; using PARR.API.Controllers.V1.Base; using PARR.API.Services.Interfaces; +using PARR.Constants; using PARR.DAL.CacheServices; using PARR.DAL.TransformServices; @@ -26,13 +28,13 @@ namespace PARR.API.Controllers.V1 /// Получить мой ip /// /// + //[Authorize(Roles = ParrRoles.Administrator.Role)] + [Authorize] [HttpGet(ApiRoutes.Test.GetMyIp)] public IActionResult GetMyIp() { var ipAddress = clientService.GetClientIp(); - //var ipV4 = ipAddress?.MapToIPv4(); - var ip = ipAddress?.ToString(); return Ok(new Response(new { ip }, true)); diff --git a/PARR.API/Installers/ApiServicesInstaller.cs b/PARR.API/Installers/ApiServicesInstaller.cs index 4ff3bc14..321bbdc6 100644 --- a/PARR.API/Installers/ApiServicesInstaller.cs +++ b/PARR.API/Installers/ApiServicesInstaller.cs @@ -20,7 +20,7 @@ namespace PARR.API.Installers }); services.AddTransient(); - + services.AddTransient(); } diff --git a/PARR.API/Installers/SettingsInstaller.cs b/PARR.API/Installers/SettingsInstaller.cs index ad96399f..933cda6b 100644 --- a/PARR.API/Installers/SettingsInstaller.cs +++ b/PARR.API/Installers/SettingsInstaller.cs @@ -13,10 +13,9 @@ namespace PARR.API.Installers configuration.GetSection(nameof(MqSettings)).Bind(mqSettings); services.AddSingleton(mqSettings); - - //var storageSettings = new StorageSettings(); - //configuration.GetSection(nameof(StorageSettings)).Bind(storageSettings); - //services.AddSingleton(storageSettings); + var userCacheSettings = new UserCacheSettings(); + configuration.GetSection(nameof(UserCacheSettings)).Bind(userCacheSettings); + services.AddSingleton(userCacheSettings); //TODO: add other } diff --git a/PARR.API/Program.cs b/PARR.API/Program.cs index f5427ef5..f2923431 100644 --- a/PARR.API/Program.cs +++ b/PARR.API/Program.cs @@ -1,6 +1,9 @@ using Elastic.CommonSchema.Serilog; using FluentValidation; +using Microsoft.AspNetCore.Server.HttpSys; +using PARR.API.Authentication; using PARR.API.Installers; +using PARR.API.RoleProvider; using PARR.BLL; using PARR.DAL; using Serilog; @@ -29,6 +32,12 @@ builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); builder.Configuration.AddDalConfigurations(builder.Services); builder.Services.AddDallSettings(builder.Configuration); +// Auth +builder.Services.AddAuthentication(ParrAuthenticationOptions.DefaultScheme) + .AddScheme(ParrAuthenticationOptions.DefaultScheme, opt => { }); +builder.Services.AddSimpleRoleAuthorization(); + + builder.Services.AddHttpContextAccessor(); builder.Services.AddControllers(); @@ -44,6 +53,7 @@ var app = builder.Build(); // Configure the HTTP request pipeline. app.InstallSwagger(); +app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); diff --git a/PARR.API/RoleProvider/BaseSimpleRoleProvider.cs b/PARR.API/RoleProvider/BaseSimpleRoleProvider.cs new file mode 100644 index 00000000..be304f49 --- /dev/null +++ b/PARR.API/RoleProvider/BaseSimpleRoleProvider.cs @@ -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> GetUserRolesAsync(string ipClient) + { + // Авторизация - проверка на разрешения + // В данном случае, получаем все разрешения (роли) + + ICollection defaultResult = new string[0]; + + using (var scope = serviceProvider.CreateScope()) + { + //var clientService = scope.ServiceProvider.GetRequiredService(); + + //var ipClient = clientService.GetClientIp()?.ToString(); + //if (string.IsNullOrEmpty(ipClient)) + // return await Task.FromResult(defaultResult); + + var authService = scope.ServiceProvider.GetRequiredService(); + + // проверяем блокировку пользователя + 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); + } + } +} diff --git a/PARR.API/RoleProvider/ISimpleRoleProvider.cs b/PARR.API/RoleProvider/ISimpleRoleProvider.cs new file mode 100644 index 00000000..6607a5ca --- /dev/null +++ b/PARR.API/RoleProvider/ISimpleRoleProvider.cs @@ -0,0 +1,7 @@ +namespace PARR.API.RoleProvider +{ + public interface ISimpleRoleProvider + { + Task> GetUserRolesAsync(string ipClient); + } +} diff --git a/PARR.API/RoleProvider/SimpleRoleAuthorizationServiceCollectionExtensions.cs b/PARR.API/RoleProvider/SimpleRoleAuthorizationServiceCollectionExtensions.cs new file mode 100644 index 00000000..267afb0a --- /dev/null +++ b/PARR.API/RoleProvider/SimpleRoleAuthorizationServiceCollectionExtensions.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Authentication; + +namespace PARR.API.RoleProvider +{ + public static class SimpleRoleAuthorizationServiceCollectionExtensions + { + public static void AddSimpleRoleAuthorization(this IServiceCollection services) + where TRoleProvider : class, ISimpleRoleProvider + { + services.AddSingleton(); + services.AddSingleton(); + } + } +} diff --git a/PARR.API/RoleProvider/SimpleRoleAuthorizationTransform.cs b/PARR.API/RoleProvider/SimpleRoleAuthorizationTransform.cs new file mode 100644 index 00000000..3f8cb977 --- /dev/null +++ b/PARR.API/RoleProvider/SimpleRoleAuthorizationTransform.cs @@ -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 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); + } + } +} diff --git a/PARR.API/Services/Implementations/AuthService.cs b/PARR.API/Services/Implementations/AuthService.cs new file mode 100644 index 00000000..0484e2ec --- /dev/null +++ b/PARR.API/Services/Implementations/AuthService.cs @@ -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 UserIsBlockedAsync(string ipAddress) + { + //пользователь есть в списке блокировки? + var existUserInBlockList = await cacheService.GetCachedDataAsync(GetBlockKey(ipAddress)); + + return existUserInBlockList != null; + } + + + public async Task GetUserAsync(string ipAddress) + { + // смотрим, есть ли в кэше + var userInCache = await cacheService.GetCachedDataAsync(GetAllowKey(ipAddress)); + if (userInCache != null) + return userInCache; + + // нет в кэше + var roles = new List(); + + 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}"; + } + + + } +} diff --git a/PARR.API/Services/Interfaces/IAuthService.cs b/PARR.API/Services/Interfaces/IAuthService.cs new file mode 100644 index 00000000..161133d9 --- /dev/null +++ b/PARR.API/Services/Interfaces/IAuthService.cs @@ -0,0 +1,22 @@ +using PARR.API.Authentication.Models; + +namespace PARR.API.Services.Interfaces +{ + public interface IAuthService + { + /// + /// Получить пользователя с ролями по ip адресу. + /// Смотрим в кэше, затем в БД. При необходимости блокирует или добавлет в кэш. + /// + /// + /// + Task GetUserAsync(string ipAddress); + + /// + /// Проверяет, блокировку пользователя + /// + /// + /// + Task UserIsBlockedAsync(string ipAddress); + } +} diff --git a/PARR.API/Settings/UserCacheSettings.cs b/PARR.API/Settings/UserCacheSettings.cs new file mode 100644 index 00000000..4a5e6a04 --- /dev/null +++ b/PARR.API/Settings/UserCacheSettings.cs @@ -0,0 +1,18 @@ +namespace PARR.API.Settings +{ + /// + /// Настройки кэша пользователй + /// + public class UserCacheSettings + { + /// + /// Сколько хранить в кэше авторизованных пользователей + /// + public TimeSpan UserCacheTtl { get; set; } + + /// + /// Сколько хранить в кэше заблокированных пользователей (как долго блокировать пользователя) + /// + public TimeSpan UserBlockTtl { get; set; } + } +} diff --git a/PARR.API/appsettings.Development.json b/PARR.API/appsettings.Development.json index 41b89417..9c4f419b 100644 --- a/PARR.API/appsettings.Development.json +++ b/PARR.API/appsettings.Development.json @@ -1,6 +1,6 @@ { "ConnectionStrings": { - "RedisConnection": "10.99.253.216:6379" + "RedisConnection": "10.99.253.216:6379,password=ParrP@ssPtk202MMdevDvs" }, "Logging": { "LogLevel": { diff --git a/PARR.API/appsettings.json b/PARR.API/appsettings.json index 03d1942b..18ef1843 100644 --- a/PARR.API/appsettings.json +++ b/PARR.API/appsettings.json @@ -45,5 +45,9 @@ "User": "generate_templates_api", "Password": "DHhjgdsf*&%95" } + }, + "UserCacheSettings": { + "UserCacheTtl": "00:05:00", + "UserBlockTtl": "00:03:00" } } diff --git a/PARR.DAL/CacheServices/RedisCacheService.cs b/PARR.DAL/CacheServices/RedisCacheService.cs index f20f25aa..abf94815 100644 --- a/PARR.DAL/CacheServices/RedisCacheService.cs +++ b/PARR.DAL/CacheServices/RedisCacheService.cs @@ -15,7 +15,7 @@ namespace PARR.DAL.CacheServices public async Task GetCachedDataAsync(string key) { var jsonData = await cache.GetStringAsync(key); - + if (jsonData == null) return default(T); diff --git a/PARR.DAL/ParrDalInstaller.cs b/PARR.DAL/ParrDalInstaller.cs index 85be6c05..4e2e79de 100644 --- a/PARR.DAL/ParrDalInstaller.cs +++ b/PARR.DAL/ParrDalInstaller.cs @@ -62,6 +62,7 @@ namespace PARR.DAL services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); // TransformServices diff --git a/PARR.DAL/Services/Implementations/HostService.cs b/PARR.DAL/Services/Implementations/HostService.cs index 22fb16eb..ca83f0f4 100644 --- a/PARR.DAL/Services/Implementations/HostService.cs +++ b/PARR.DAL/Services/Implementations/HostService.cs @@ -40,6 +40,11 @@ namespace PARR.DAL.Services.Implementations .FirstOrDefaultAsync(h => h.IP == IP); } + public async Task GetByIpAsync(string ip) + { + return await EntitySet.FirstOrDefaultAsync(t => t.IP == ip); + } + //public async Task GetHostByRegionalEKAsync(string RegionalEK)И //{ // try diff --git a/PARR.DAL/Services/Implementations/UserService.cs b/PARR.DAL/Services/Implementations/UserService.cs new file mode 100644 index 00000000..11860ff4 --- /dev/null +++ b/PARR.DAL/Services/Implementations/UserService.cs @@ -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, IUserService + { + private readonly DataContext dataContext; + + public UserService(DataContext dataContext, ILogger logger) : base(logger) + { + this.dataContext = dataContext; + } + + protected override DbSet EntitySet => dataContext.Users; + + protected override DataContext EntitiContext => dataContext; + + public async Task GetByIpWithRolesAsync(string ipAddress) + { + return await EntitySet + .Include(t => t.Roles).ThenInclude(t => t.Role) + .FirstOrDefaultAsync(t => t.Ip == ipAddress); + } + } +} diff --git a/PARR.DAL/Services/Interfaces/IHostService.cs b/PARR.DAL/Services/Interfaces/IHostService.cs index 372ea38d..5188a3be 100644 --- a/PARR.DAL/Services/Interfaces/IHostService.cs +++ b/PARR.DAL/Services/Interfaces/IHostService.cs @@ -5,6 +5,10 @@ namespace PARR.DAL.Services.Interfaces { public interface IHostService : IBaseService { - Task GetHostWithAppsAsync(string IP); + Task GetHostWithAppsAsync(string ip); + + Task GetByIpAsync(string ip); + + } } diff --git a/PARR.DAL/Services/Interfaces/IUserService.cs b/PARR.DAL/Services/Interfaces/IUserService.cs new file mode 100644 index 00000000..abce2a37 --- /dev/null +++ b/PARR.DAL/Services/Interfaces/IUserService.cs @@ -0,0 +1,10 @@ +using PARR.DAL.Models; +using PARR.DAL.Services.Interfaces.Base; + +namespace PARR.DAL.Services.Interfaces +{ + public interface IUserService: IBaseService + { + Task GetByIpWithRolesAsync(string ipAddress); + } +}