diff --git a/Distribution/Features/Auth/AuthService.cs b/Distribution/Features/Auth/AuthService.cs index 6aa963d..e9f4928 100644 --- a/Distribution/Features/Auth/AuthService.cs +++ b/Distribution/Features/Auth/AuthService.cs @@ -1,5 +1,5 @@ using Distribution.Features.User; -using Distribution.Infra.Auth; +using Distribution.Infra.Authorization; using Microsoft.AspNetCore.Identity; namespace Distribution.Features.Auth; diff --git a/Distribution/Features/Order/OrderService.cs b/Distribution/Features/Order/OrderService.cs index 16c9999..3ebb0db 100644 --- a/Distribution/Features/Order/OrderService.cs +++ b/Distribution/Features/Order/OrderService.cs @@ -1,4 +1,4 @@ -using Distribution.Infra.Auth; +using Distribution.Infra.Authorization; using Distribution.Infra.UoW; namespace Distribution.Features.Order; diff --git a/Distribution/Features/Store/StoreService.cs b/Distribution/Features/Store/StoreService.cs index d847aa5..2db19eb 100644 --- a/Distribution/Features/Store/StoreService.cs +++ b/Distribution/Features/Store/StoreService.cs @@ -1,4 +1,4 @@ -using Distribution.Infra.Auth; +using Distribution.Infra.Authorization; using Distribution.Infra.UoW; namespace Distribution.Features.Store; diff --git a/Distribution/Infra/Auth/CurrentUserService.cs b/Distribution/Infra/Auth/CurrentUserService.cs deleted file mode 100644 index 34271fe..0000000 --- a/Distribution/Infra/Auth/CurrentUserService.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Microsoft.AspNetCore.Http; - -namespace Distribution.Infra.Auth -{ - public class CurrentUserService : ICurrentUserService - { - private readonly IHttpContextAccessor _httpContextAccessor; - - public CurrentUserService(IHttpContextAccessor httpContextAccessor) - { - _httpContextAccessor = httpContextAccessor; - } - - private System.Security.Claims.ClaimsPrincipal? User => _httpContextAccessor.HttpContext?.User; - - public bool IsAuthenticated => User?.Identity?.IsAuthenticated ?? false; - - public Guid? UserId - { - get - { - var value = User?.FindFirst("sub")?.Value ?? User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; - return Guid.TryParse(value, out var id) ? id : null; - } - } - - public Guid? RoleId - { - get - { - var value = User?.FindFirst("role_id")?.Value; - return Guid.TryParse(value, out var id) ? id : null; - } - } - - public string? Username => User?.FindFirst("username")?.Value; - - public int RoleLevel - { - get - { - var value = User?.FindFirst("role_level")?.Value; - return int.TryParse(value, out var level) ? level : 0; - } - } - } -} diff --git a/Distribution/Infra/Auth/ICurrentUserService.cs b/Distribution/Infra/Auth/ICurrentUserService.cs deleted file mode 100644 index 69224e9..0000000 --- a/Distribution/Infra/Auth/ICurrentUserService.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Distribution.Infra.Auth -{ - public interface ICurrentUserService - { - Guid? UserId { get; } - Guid? RoleId { get; } - string? Username { get; } - int RoleLevel { get; } - bool IsAuthenticated { get; } - } -} diff --git a/Distribution/Infra/Auth/IJwtTokenService.cs b/Distribution/Infra/Auth/IJwtTokenService.cs deleted file mode 100644 index b95986e..0000000 --- a/Distribution/Infra/Auth/IJwtTokenService.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Distribution.Infra.Auth -{ - public interface IJwtTokenService - { - string GenerateToken(Models.User user); - } -} diff --git a/Distribution/Infra/Auth/IPermissionService.cs b/Distribution/Infra/Auth/IPermissionService.cs deleted file mode 100644 index e9b0e13..0000000 --- a/Distribution/Infra/Auth/IPermissionService.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Distribution.Infra.Auth -{ - public interface IPermissionService - { - Task HasPermissionAsync(Guid roleId, string permissionCode); - } -} diff --git a/Distribution/Infra/Auth/IScopeAccessService.cs b/Distribution/Infra/Auth/IScopeAccessService.cs deleted file mode 100644 index c6a4950..0000000 --- a/Distribution/Infra/Auth/IScopeAccessService.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Distribution.Infra.Auth -{ - public interface IScopeAccessService - { - Task HasStoreScopeAsync(Guid userId, Guid storeId); - Task HasRouteScopeAsync(Guid userId, Guid routeId); - Task HasRegionScopeAsync(Guid userId, Guid regionId); - Task> GetAccessibleStoreIdsAsync(Guid userId); - } -} diff --git a/Distribution/Infra/Auth/JwtTokenService.cs b/Distribution/Infra/Auth/JwtTokenService.cs deleted file mode 100644 index c0d4fa5..0000000 --- a/Distribution/Infra/Auth/JwtTokenService.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using System.Text; -using Microsoft.Extensions.Configuration; -using Microsoft.IdentityModel.Tokens; - -namespace Distribution.Infra.Auth -{ - public class JwtTokenService : IJwtTokenService - { - private readonly IConfiguration _configuration; - - public JwtTokenService(IConfiguration configuration) - { - _configuration = configuration; - } - - public string GenerateToken(Models.User user) - { - var jwtSection = _configuration.GetSection("Jwt"); - var key = jwtSection["Key"] ?? throw new InvalidOperationException("Jwt:Key is not configured."); - var issuer = jwtSection["Issuer"]; - var audience = jwtSection["Audience"]; - var expiryMinutes = int.TryParse(jwtSection["ExpiryMinutes"], out var m) ? m : 120; - - var claims = new List - { - new("sub", user.Id.ToString()), - new("username", user.Username), - new("role", user.Role?.Name ?? string.Empty), - new("role_id", user.FK_RoleId.ToString()), - new("role_level", (user.Role?.Level ?? 0).ToString()) - }; - - var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)); - var credentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256); - - var token = new JwtSecurityToken( - issuer: issuer, - audience: audience, - claims: claims, - expires: DateTime.Now.AddMinutes(expiryMinutes), - signingCredentials: credentials); - - return new JwtSecurityTokenHandler().WriteToken(token); - } - } -} diff --git a/Distribution/Infra/Auth/PermissionAuthorizationHandler.cs b/Distribution/Infra/Auth/PermissionAuthorizationHandler.cs deleted file mode 100644 index 0582e85..0000000 --- a/Distribution/Infra/Auth/PermissionAuthorizationHandler.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Microsoft.AspNetCore.Authorization; - -namespace Distribution.Infra.Auth -{ - public class PermissionAuthorizationHandler : AuthorizationHandler - { - private readonly IPermissionService _permissionService; - - public PermissionAuthorizationHandler(IPermissionService permissionService) - { - _permissionService = permissionService; - } - - protected override async Task HandleRequirementAsync( - AuthorizationHandlerContext context, PermissionRequirement requirement) - { - var roleIdClaim = context.User.FindFirst("role_id")?.Value; - if (!Guid.TryParse(roleIdClaim, out var roleId)) - return; - - if (await _permissionService.HasPermissionAsync(roleId, requirement.PermissionCode)) - context.Succeed(requirement); - } - } -} diff --git a/Distribution/Infra/Auth/PermissionPolicyProvider.cs b/Distribution/Infra/Auth/PermissionPolicyProvider.cs deleted file mode 100644 index 994d87a..0000000 --- a/Distribution/Infra/Auth/PermissionPolicyProvider.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.Extensions.Options; - -namespace Distribution.Infra.Auth -{ - public class PermissionPolicyProvider : IAuthorizationPolicyProvider - { - private readonly DefaultAuthorizationPolicyProvider _fallback; - - public PermissionPolicyProvider(IOptions options) - { - _fallback = new DefaultAuthorizationPolicyProvider(options); - } - - public Task GetDefaultPolicyAsync() => _fallback.GetDefaultPolicyAsync(); - - public Task GetFallbackPolicyAsync() => _fallback.GetFallbackPolicyAsync(); - - public Task GetPolicyAsync(string policyName) - { - if (policyName.Contains('.')) - { - var policy = new AuthorizationPolicyBuilder() - .RequireAuthenticatedUser() - .AddRequirements(new PermissionRequirement(policyName)) - .Build(); - return Task.FromResult(policy); - } - - return _fallback.GetPolicyAsync(policyName); - } - } -} diff --git a/Distribution/Infra/Auth/PermissionRequirement.cs b/Distribution/Infra/Auth/PermissionRequirement.cs deleted file mode 100644 index 7ac9421..0000000 --- a/Distribution/Infra/Auth/PermissionRequirement.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.AspNetCore.Authorization; - -namespace Distribution.Infra.Auth -{ - public class PermissionRequirement : IAuthorizationRequirement - { - public string PermissionCode { get; } - - public PermissionRequirement(string permissionCode) - { - PermissionCode = permissionCode; - } - } -} diff --git a/Distribution/Infra/Auth/PermissionService.cs b/Distribution/Infra/Auth/PermissionService.cs deleted file mode 100644 index 234ae04..0000000 --- a/Distribution/Infra/Auth/PermissionService.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Distribution.Data; -using Microsoft.EntityFrameworkCore; - -namespace Distribution.Infra.Auth -{ - public class PermissionService : IPermissionService - { - private readonly AppDBContext _context; - - public PermissionService(AppDBContext context) - { - _context = context; - } - - public async Task HasPermissionAsync(Guid roleId, string permissionCode) => - await _context.RolePermissions - .Where(rp => rp.FK_RoleId == roleId && rp.IsActive) - .Join(_context.Permissions.Where(p => p.IsActive), rp => rp.FK_PermissionId, p => p.Id, (rp, p) => p.Code) - .AnyAsync(code => code == permissionCode); - } -} diff --git a/Distribution/Infra/Auth/ScopeAccessService.cs b/Distribution/Infra/Auth/ScopeAccessService.cs deleted file mode 100644 index bc5887c..0000000 --- a/Distribution/Infra/Auth/ScopeAccessService.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Distribution.Data; -using Microsoft.EntityFrameworkCore; - -namespace Distribution.Infra.Auth -{ - public class ScopeAccessService : IScopeAccessService - { - private readonly AppDBContext _context; - - public ScopeAccessService(AppDBContext context) - { - _context = context; - } - - public async Task HasStoreScopeAsync(Guid userId, Guid storeId) => - await _context.UserStoreScopes.AnyAsync(s => s.FK_UserId == userId && s.FK_StoreId == storeId && s.IsActive); - - public async Task HasRouteScopeAsync(Guid userId, Guid routeId) => - await _context.UserRouteScopes.AnyAsync(s => s.FK_UserId == userId && s.FK_RouteId == routeId && s.IsActive); - - public async Task HasRegionScopeAsync(Guid userId, Guid regionId) => - await _context.UserRegionScopes.AnyAsync(s => s.FK_UserId == userId && s.FK_RegionId == regionId && s.IsActive); - - public async Task> GetAccessibleStoreIdsAsync(Guid userId) - { - var directStoreIds = await _context.UserStoreScopes - .Where(s => s.FK_UserId == userId && s.IsActive) - .Select(s => s.FK_StoreId) - .ToListAsync(); - - var routeIds = await _context.UserRouteScopes - .Where(s => s.FK_UserId == userId && s.IsActive) - .Select(s => s.FK_RouteId) - .ToListAsync(); - - var regionIds = await _context.UserRegionScopes - .Where(s => s.FK_UserId == userId && s.IsActive) - .Select(s => s.FK_RegionId) - .ToListAsync(); - - var storeIdsViaRoute = await _context.Stores - .Where(s => routeIds.Contains(s.FK_RouteId) && s.IsActive) - .Select(s => s.Id) - .ToListAsync(); - - var routeIdsViaRegion = await _context.Routes - .Where(r => regionIds.Contains(r.FK_RegionId) && r.IsActive) - .Select(r => r.Id) - .ToListAsync(); - - var storeIdsViaRegion = await _context.Stores - .Where(s => routeIdsViaRegion.Contains(s.FK_RouteId) && s.IsActive) - .Select(s => s.Id) - .ToListAsync(); - - return directStoreIds - .Concat(storeIdsViaRoute) - .Concat(storeIdsViaRegion) - .Distinct() - .ToList(); - } - } -} diff --git a/Distribution/Infra/Authorization/Controller/AuthorizationController.cs b/Distribution/Infra/Authorization/Controller/AuthorizationController.cs new file mode 100644 index 0000000..df2e4d1 --- /dev/null +++ b/Distribution/Infra/Authorization/Controller/AuthorizationController.cs @@ -0,0 +1,53 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Distribution.Infra.Authorization; + +[ApiController] +[Route("api/authorization")] +[Authorize] +public class AuthorizationController( + IPermissionService permissionService, + IScopeAccessService scopeAccessService, + ICurrentUserService currentUser) : ControllerBase +{ + [HttpGet("permissions/{permissionCode}")] + public async Task CheckPermission(string permissionCode) + { + if (currentUser.RoleId is not { } roleId) return Unauthorized(); + var result = await permissionService.HasPermissionAsync(roleId, permissionCode); + return Ok(new PermissionCheckResponse(result)); + } + + [HttpGet("scopes/stores/{storeId:guid}")] + public async Task CheckStoreScope(Guid storeId) + { + if (currentUser.UserId is not { } userId) return Unauthorized(); + var result = await scopeAccessService.HasStoreScopeAsync(userId, storeId); + return Ok(new StoreScopeCheckResponse(result)); + } + + [HttpGet("scopes/routes/{routeId:guid}")] + public async Task CheckRouteScope(Guid routeId) + { + if (currentUser.UserId is not { } userId) return Unauthorized(); + var result = await scopeAccessService.HasRouteScopeAsync(userId, routeId); + return Ok(new RouteScopeCheckResponse(result)); + } + + [HttpGet("scopes/regions/{regionId:guid}")] + public async Task CheckRegionScope(Guid regionId) + { + if (currentUser.UserId is not { } userId) return Unauthorized(); + var result = await scopeAccessService.HasRegionScopeAsync(userId, regionId); + return Ok(new RegionScopeCheckResponse(result)); + } + + [HttpGet("scopes/stores")] + public async Task GetAccessibleStores() + { + if (currentUser.UserId is not { } userId) return Unauthorized(); + var result = await scopeAccessService.GetAccessibleStoreIdsAsync(userId); + return Ok(new AccessibleStoresResponse(result)); + } +} diff --git a/Distribution/Infra/Authorization/CurrentUser/CurrentUserService.cs b/Distribution/Infra/Authorization/CurrentUser/CurrentUserService.cs new file mode 100644 index 0000000..a599397 --- /dev/null +++ b/Distribution/Infra/Authorization/CurrentUser/CurrentUserService.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Http; + +namespace Distribution.Infra.Authorization; + +public class CurrentUserService : ICurrentUserService +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + public CurrentUserService(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + private System.Security.Claims.ClaimsPrincipal? User => _httpContextAccessor.HttpContext?.User; + + public bool IsAuthenticated => User?.Identity?.IsAuthenticated ?? false; + + public Guid? UserId + { + get + { + var value = User?.FindFirst("sub")?.Value ?? User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + return Guid.TryParse(value, out var id) ? id : null; + } + } + + public Guid? RoleId + { + get + { + var value = User?.FindFirst("role_id")?.Value; + return Guid.TryParse(value, out var id) ? id : null; + } + } + + public string? Username => User?.FindFirst("username")?.Value; + + public int RoleLevel + { + get + { + var value = User?.FindFirst("role_level")?.Value; + return int.TryParse(value, out var level) ? level : 0; + } + } +} diff --git a/Distribution/Infra/Authorization/CurrentUser/ICurrentUserService.cs b/Distribution/Infra/Authorization/CurrentUser/ICurrentUserService.cs new file mode 100644 index 0000000..53febd1 --- /dev/null +++ b/Distribution/Infra/Authorization/CurrentUser/ICurrentUserService.cs @@ -0,0 +1,10 @@ +namespace Distribution.Infra.Authorization; + +public interface ICurrentUserService +{ + Guid? UserId { get; } + Guid? RoleId { get; } + string? Username { get; } + int RoleLevel { get; } + bool IsAuthenticated { get; } +} diff --git a/Distribution/Infra/Authorization/DTOs/AuthorizationDTOs.cs b/Distribution/Infra/Authorization/DTOs/AuthorizationDTOs.cs new file mode 100644 index 0000000..83e3c59 --- /dev/null +++ b/Distribution/Infra/Authorization/DTOs/AuthorizationDTOs.cs @@ -0,0 +1,11 @@ +namespace Distribution.Infra.Authorization; + +public record PermissionCheckResponse(bool HasPermission); + +public record StoreScopeCheckResponse(bool HasScope); + +public record RouteScopeCheckResponse(bool HasScope); + +public record RegionScopeCheckResponse(bool HasScope); + +public record AccessibleStoresResponse(List StoreIds); diff --git a/Distribution/Infra/Authorization/Jwt/IJwtTokenService.cs b/Distribution/Infra/Authorization/Jwt/IJwtTokenService.cs new file mode 100644 index 0000000..d2e0651 --- /dev/null +++ b/Distribution/Infra/Authorization/Jwt/IJwtTokenService.cs @@ -0,0 +1,6 @@ +namespace Distribution.Infra.Authorization; + +public interface IJwtTokenService +{ + string GenerateToken(Models.User user); +} diff --git a/Distribution/Infra/Authorization/Jwt/JwtTokenService.cs b/Distribution/Infra/Authorization/Jwt/JwtTokenService.cs new file mode 100644 index 0000000..9a0eb04 --- /dev/null +++ b/Distribution/Infra/Authorization/Jwt/JwtTokenService.cs @@ -0,0 +1,47 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; + +namespace Distribution.Infra.Authorization; + +public class JwtTokenService : IJwtTokenService +{ + private readonly IConfiguration _configuration; + + public JwtTokenService(IConfiguration configuration) + { + _configuration = configuration; + } + + public string GenerateToken(Models.User user) + { + var jwtSection = _configuration.GetSection("Jwt"); + var key = jwtSection["Key"] ?? throw new InvalidOperationException("Jwt:Key is not configured."); + var issuer = jwtSection["Issuer"]; + var audience = jwtSection["Audience"]; + var expiryMinutes = int.TryParse(jwtSection["ExpiryMinutes"], out var m) ? m : 120; + + var claims = new List + { + new("sub", user.Id.ToString()), + new("username", user.Username), + new("role", user.Role?.Name ?? string.Empty), + new("role_id", user.FK_RoleId.ToString()), + new("role_level", (user.Role?.Level ?? 0).ToString()) + }; + + var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)); + var credentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256); + + var token = new JwtSecurityToken( + issuer: issuer, + audience: audience, + claims: claims, + expires: DateTime.Now.AddMinutes(expiryMinutes), + signingCredentials: credentials); + + return new JwtSecurityTokenHandler().WriteToken(token); + } +} diff --git a/Distribution/Infra/Authorization/Permission/IPermissionRepository.cs b/Distribution/Infra/Authorization/Permission/IPermissionRepository.cs new file mode 100644 index 0000000..7445ba0 --- /dev/null +++ b/Distribution/Infra/Authorization/Permission/IPermissionRepository.cs @@ -0,0 +1,6 @@ +namespace Distribution.Infra.Authorization; + +public interface IPermissionRepository +{ + Task HasPermissionAsync(Guid roleId, string permissionCode); +} diff --git a/Distribution/Infra/Authorization/Permission/IPermissionService.cs b/Distribution/Infra/Authorization/Permission/IPermissionService.cs new file mode 100644 index 0000000..83ebe70 --- /dev/null +++ b/Distribution/Infra/Authorization/Permission/IPermissionService.cs @@ -0,0 +1,6 @@ +namespace Distribution.Infra.Authorization; + +public interface IPermissionService +{ + Task HasPermissionAsync(Guid roleId, string permissionCode); +} diff --git a/Distribution/Infra/Authorization/Permission/PermissionRepository.cs b/Distribution/Infra/Authorization/Permission/PermissionRepository.cs new file mode 100644 index 0000000..7b2c6f3 --- /dev/null +++ b/Distribution/Infra/Authorization/Permission/PermissionRepository.cs @@ -0,0 +1,13 @@ +using Distribution.Data; +using Microsoft.EntityFrameworkCore; + +namespace Distribution.Infra.Authorization; + +public class PermissionRepository(AppDBContext context) : IPermissionRepository +{ + public async Task HasPermissionAsync(Guid roleId, string permissionCode) => + await context.RolePermissions + .Where(rp => rp.FK_RoleId == roleId && rp.IsActive) + .Join(context.Permissions.Where(p => p.IsActive), rp => rp.FK_PermissionId, p => p.Id, (rp, p) => p.Code) + .AnyAsync(code => code == permissionCode); +} diff --git a/Distribution/Infra/Authorization/Permission/PermissionService.cs b/Distribution/Infra/Authorization/Permission/PermissionService.cs new file mode 100644 index 0000000..d705868 --- /dev/null +++ b/Distribution/Infra/Authorization/Permission/PermissionService.cs @@ -0,0 +1,7 @@ +namespace Distribution.Infra.Authorization; + +public class PermissionService(IPermissionRepository repo) : IPermissionService +{ + public Task HasPermissionAsync(Guid roleId, string permissionCode) => + repo.HasPermissionAsync(roleId, permissionCode); +} diff --git a/Distribution/Infra/Authorization/PermissionAuthorizationHandler.cs b/Distribution/Infra/Authorization/PermissionAuthorizationHandler.cs new file mode 100644 index 0000000..edcc4b3 --- /dev/null +++ b/Distribution/Infra/Authorization/PermissionAuthorizationHandler.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Distribution.Infra.Authorization; + +public class PermissionAuthorizationHandler : AuthorizationHandler +{ + private readonly IPermissionService _permissionService; + + public PermissionAuthorizationHandler(IPermissionService permissionService) + { + _permissionService = permissionService; + } + + protected override async Task HandleRequirementAsync( + AuthorizationHandlerContext context, PermissionRequirement requirement) + { + var roleIdClaim = context.User.FindFirst("role_id")?.Value; + if (!Guid.TryParse(roleIdClaim, out var roleId)) + return; + + if (await _permissionService.HasPermissionAsync(roleId, requirement.PermissionCode)) + context.Succeed(requirement); + } +} diff --git a/Distribution/Infra/Authorization/PermissionPolicyProvider.cs b/Distribution/Infra/Authorization/PermissionPolicyProvider.cs new file mode 100644 index 0000000..9ee8ef9 --- /dev/null +++ b/Distribution/Infra/Authorization/PermissionPolicyProvider.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Options; + +namespace Distribution.Infra.Authorization; + +public class PermissionPolicyProvider : IAuthorizationPolicyProvider +{ + private readonly DefaultAuthorizationPolicyProvider _fallback; + + public PermissionPolicyProvider(IOptions options) + { + _fallback = new DefaultAuthorizationPolicyProvider(options); + } + + public Task GetDefaultPolicyAsync() => _fallback.GetDefaultPolicyAsync(); + + public Task GetFallbackPolicyAsync() => _fallback.GetFallbackPolicyAsync(); + + public Task GetPolicyAsync(string policyName) + { + if (policyName.Contains('.')) + { + var policy = new AuthorizationPolicyBuilder() + .RequireAuthenticatedUser() + .AddRequirements(new PermissionRequirement(policyName)) + .Build(); + return Task.FromResult(policy); + } + + return _fallback.GetPolicyAsync(policyName); + } +} diff --git a/Distribution/Infra/Authorization/PermissionRequirement.cs b/Distribution/Infra/Authorization/PermissionRequirement.cs new file mode 100644 index 0000000..a25e8f6 --- /dev/null +++ b/Distribution/Infra/Authorization/PermissionRequirement.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Distribution.Infra.Authorization; + +public class PermissionRequirement : IAuthorizationRequirement +{ + public string PermissionCode { get; } + + public PermissionRequirement(string permissionCode) + { + PermissionCode = permissionCode; + } +} diff --git a/Distribution/Infra/Authorization/Scope/IScopeAccessService.cs b/Distribution/Infra/Authorization/Scope/IScopeAccessService.cs new file mode 100644 index 0000000..c6b117d --- /dev/null +++ b/Distribution/Infra/Authorization/Scope/IScopeAccessService.cs @@ -0,0 +1,9 @@ +namespace Distribution.Infra.Authorization; + +public interface IScopeAccessService +{ + Task HasStoreScopeAsync(Guid userId, Guid storeId); + Task HasRouteScopeAsync(Guid userId, Guid routeId); + Task HasRegionScopeAsync(Guid userId, Guid regionId); + Task> GetAccessibleStoreIdsAsync(Guid userId); +} diff --git a/Distribution/Infra/Authorization/Scope/IScopeRepository.cs b/Distribution/Infra/Authorization/Scope/IScopeRepository.cs new file mode 100644 index 0000000..cbc8321 --- /dev/null +++ b/Distribution/Infra/Authorization/Scope/IScopeRepository.cs @@ -0,0 +1,13 @@ +namespace Distribution.Infra.Authorization; + +public interface IScopeRepository +{ + Task HasStoreScopeAsync(Guid userId, Guid storeId); + Task HasRouteScopeAsync(Guid userId, Guid routeId); + Task HasRegionScopeAsync(Guid userId, Guid regionId); + Task> GetDirectStoreIdsAsync(Guid userId); + Task> GetRouteIdsAsync(Guid userId); + Task> GetRegionIdsAsync(Guid userId); + Task> GetStoreIdsByRouteIdsAsync(List routeIds); + Task> GetRouteIdsByRegionIdsAsync(List regionIds); +} diff --git a/Distribution/Infra/Authorization/Scope/ScopeAccessService.cs b/Distribution/Infra/Authorization/Scope/ScopeAccessService.cs new file mode 100644 index 0000000..e6b0296 --- /dev/null +++ b/Distribution/Infra/Authorization/Scope/ScopeAccessService.cs @@ -0,0 +1,30 @@ +namespace Distribution.Infra.Authorization; + +public class ScopeAccessService(IScopeRepository repo) : IScopeAccessService +{ + public Task HasStoreScopeAsync(Guid userId, Guid storeId) => + repo.HasStoreScopeAsync(userId, storeId); + + public Task HasRouteScopeAsync(Guid userId, Guid routeId) => + repo.HasRouteScopeAsync(userId, routeId); + + public Task HasRegionScopeAsync(Guid userId, Guid regionId) => + repo.HasRegionScopeAsync(userId, regionId); + + public async Task> GetAccessibleStoreIdsAsync(Guid userId) + { + var directStoreIds = await repo.GetDirectStoreIdsAsync(userId); + var routeIds = await repo.GetRouteIdsAsync(userId); + var regionIds = await repo.GetRegionIdsAsync(userId); + + var storeIdsViaRoute = await repo.GetStoreIdsByRouteIdsAsync(routeIds); + var routeIdsViaRegion = await repo.GetRouteIdsByRegionIdsAsync(regionIds); + var storeIdsViaRegion = await repo.GetStoreIdsByRouteIdsAsync(routeIdsViaRegion); + + return directStoreIds + .Concat(storeIdsViaRoute) + .Concat(storeIdsViaRegion) + .Distinct() + .ToList(); + } +} diff --git a/Distribution/Infra/Authorization/Scope/ScopeRepository.cs b/Distribution/Infra/Authorization/Scope/ScopeRepository.cs new file mode 100644 index 0000000..e98ceeb --- /dev/null +++ b/Distribution/Infra/Authorization/Scope/ScopeRepository.cs @@ -0,0 +1,46 @@ +using Distribution.Data; +using Microsoft.EntityFrameworkCore; + +namespace Distribution.Infra.Authorization; + +public class ScopeRepository(AppDBContext context) : IScopeRepository +{ + public async Task HasStoreScopeAsync(Guid userId, Guid storeId) => + await context.UserStoreScopes.AnyAsync(s => s.FK_UserId == userId && s.FK_StoreId == storeId && s.IsActive); + + public async Task HasRouteScopeAsync(Guid userId, Guid routeId) => + await context.UserRouteScopes.AnyAsync(s => s.FK_UserId == userId && s.FK_RouteId == routeId && s.IsActive); + + public async Task HasRegionScopeAsync(Guid userId, Guid regionId) => + await context.UserRegionScopes.AnyAsync(s => s.FK_UserId == userId && s.FK_RegionId == regionId && s.IsActive); + + public async Task> GetDirectStoreIdsAsync(Guid userId) => + await context.UserStoreScopes + .Where(s => s.FK_UserId == userId && s.IsActive) + .Select(s => s.FK_StoreId) + .ToListAsync(); + + public async Task> GetRouteIdsAsync(Guid userId) => + await context.UserRouteScopes + .Where(s => s.FK_UserId == userId && s.IsActive) + .Select(s => s.FK_RouteId) + .ToListAsync(); + + public async Task> GetRegionIdsAsync(Guid userId) => + await context.UserRegionScopes + .Where(s => s.FK_UserId == userId && s.IsActive) + .Select(s => s.FK_RegionId) + .ToListAsync(); + + public async Task> GetStoreIdsByRouteIdsAsync(List routeIds) => + await context.Stores + .Where(s => routeIds.Contains(s.FK_RouteId) && s.IsActive) + .Select(s => s.Id) + .ToListAsync(); + + public async Task> GetRouteIdsByRegionIdsAsync(List regionIds) => + await context.Routes + .Where(r => regionIds.Contains(r.FK_RegionId) && r.IsActive) + .Select(r => r.Id) + .ToListAsync(); +} diff --git a/Distribution/Program.cs b/Distribution/Program.cs index c89a5cc..27466d3 100644 --- a/Distribution/Program.cs +++ b/Distribution/Program.cs @@ -9,7 +9,7 @@ using Distribution.Features.Store; using Distribution.Features.User; using Distribution.Features.Vehicle; using Distribution.Features.Warehouse; -using Distribution.Infra.Auth; +using Distribution.Infra.Authorization; using Distribution.Infra.UoW; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; @@ -49,6 +49,8 @@ builder.Services.AddScoped(); builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped();