diff --git a/services/merchant-service-net/src/MerchantService.API/Application/Behaviors/IRequirePermission.cs b/services/merchant-service-net/src/MerchantService.API/Application/Behaviors/IRequirePermission.cs new file mode 100644 index 00000000..f0a09878 --- /dev/null +++ b/services/merchant-service-net/src/MerchantService.API/Application/Behaviors/IRequirePermission.cs @@ -0,0 +1,18 @@ +using MerchantService.Domain.AggregatesModel.MerchantStaffAggregate; + +namespace MerchantService.API.Application.Behaviors; + +/// +/// EN: Marker interface for commands that require a specific StaffPermission. +/// Commands implementing this will be checked by PermissionAuthorizationBehavior. +/// VI: Marker interface cho commands yêu cầu StaffPermission cụ thể. +/// Commands implement interface này sẽ được kiểm tra bởi PermissionAuthorizationBehavior. +/// +public interface IRequirePermission +{ + /// + /// EN: The permission required to execute this command. + /// VI: Permission cần thiết để thực thi command này. + /// + StaffPermissions RequiredPermission { get; } +} diff --git a/services/merchant-service-net/src/MerchantService.API/Application/Behaviors/PermissionAuthorizationBehavior.cs b/services/merchant-service-net/src/MerchantService.API/Application/Behaviors/PermissionAuthorizationBehavior.cs new file mode 100644 index 00000000..ea911b87 --- /dev/null +++ b/services/merchant-service-net/src/MerchantService.API/Application/Behaviors/PermissionAuthorizationBehavior.cs @@ -0,0 +1,72 @@ +using MediatR; +using MerchantService.Domain.AggregatesModel.MerchantStaffAggregate; + +namespace MerchantService.API.Application.Behaviors; + +/// +/// EN: MediatR pipeline behavior that checks permission claims from JWT token. +/// Only applies to commands implementing IRequirePermission. +/// Reads "permission" claims from HttpContext.User and converts to StaffPermissions bitmask. +/// VI: MediatR pipeline behavior kiểm tra permission claims từ JWT token. +/// Chỉ áp dụng cho commands implement IRequirePermission. +/// Đọc "permission" claims từ HttpContext.User và chuyển sang StaffPermissions bitmask. +/// +public class PermissionAuthorizationBehavior : IPipelineBehavior + where TRequest : IRequest +{ + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly ILogger> _logger; + + public PermissionAuthorizationBehavior( + IHttpContextAccessor httpContextAccessor, + ILogger> logger) + { + _httpContextAccessor = httpContextAccessor; + _logger = logger; + } + + public async Task Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken) + { + // EN: Skip if command doesn't require permission check + // VI: Bỏ qua nếu command không yêu cầu kiểm tra permission + if (request is not IRequirePermission permissionRequest) + return await next(); + + var required = permissionRequest.RequiredPermission; + if (required == StaffPermissions.None) + return await next(); + + var user = _httpContextAccessor.HttpContext?.User; + if (user?.Identity?.IsAuthenticated != true) + { + _logger.LogWarning("EN: Permission check failed — user not authenticated for {Command}", + typeof(TRequest).Name); + throw new UnauthorizedAccessException("User not authenticated / Người dùng chưa xác thực"); + } + + // EN: Extract permission claims from JWT and convert to bitmask + // VI: Trích xuất permission claims từ JWT và chuyển sang bitmask + var permClaims = user.Claims + .Where(c => c.Type == PermissionConstants.ClaimType) + .Select(c => c.Value) + .ToList(); + + var granted = PermissionConstants.FromClaims(permClaims); + + // EN: Check if user has "All" permission or the specific required permission + // VI: Kiểm tra user có "All" permission hoặc permission cụ thể cần thiết + if (granted == StaffPermissions.All || (granted & required) == required) + return await next(); + + _logger.LogWarning( + "EN: Permission denied — {Command} requires {Required}, user has {Granted}. " + + "VI: Từ chối quyền — {Command} yêu cầu {Required}, user có {Granted}.", + typeof(TRequest).Name, required, granted); + + throw new UnauthorizedAccessException( + $"Permission denied: requires {required} / Từ chối quyền: yêu cầu {required}"); + } +} diff --git a/services/merchant-service-net/src/MerchantService.API/Application/Commands/Staff/StaffCommands.cs b/services/merchant-service-net/src/MerchantService.API/Application/Commands/Staff/StaffCommands.cs index b8de8b08..b490c8e0 100644 --- a/services/merchant-service-net/src/MerchantService.API/Application/Commands/Staff/StaffCommands.cs +++ b/services/merchant-service-net/src/MerchantService.API/Application/Commands/Staff/StaffCommands.cs @@ -2,6 +2,7 @@ // VI: Commands quản lý nhân viên. using MediatR; +using MerchantService.API.Application.Behaviors; using MerchantService.Domain.AggregatesModel.MerchantAggregate; using MerchantService.Domain.AggregatesModel.MerchantStaffAggregate; using MerchantService.Domain.Exceptions; @@ -11,11 +12,12 @@ namespace MerchantService.API.Application.Commands.Staff; #region Invite Staff Command /// -/// EN: Command to invite a new staff member. -/// VI: Command để mời nhân viên mới. +/// EN: Command to invite a new staff member. Requires ManageStaff permission. +/// VI: Command để mời nhân viên mới. Yêu cầu quyền ManageStaff. /// -public record InviteStaffCommand : IRequest +public record InviteStaffCommand : IRequest, IRequirePermission { + public StaffPermissions RequiredPermission => StaffPermissions.ManageStaff; /// /// EN: Owner's user ID extracted from JWT claims by the controller. /// VI: User ID của chủ merchant được lấy từ JWT claims bởi controller. @@ -96,8 +98,9 @@ public class InviteStaffCommandHandler : IRequestHandler -public record CreateActiveStaffCommand : IRequest +public record CreateActiveStaffCommand : IRequest, IRequirePermission { + public StaffPermissions RequiredPermission => StaffPermissions.ManageStaff; /// /// EN: Owner's user ID extracted from JWT claims by the controller. /// VI: User ID của chủ merchant được lấy từ JWT claims bởi controller. @@ -190,8 +193,9 @@ public class CreateActiveStaffCommandHandler : IRequestHandler -public record UpdateStaffCommand : IRequest +public record UpdateStaffCommand : IRequest, IRequirePermission { + public StaffPermissions RequiredPermission => StaffPermissions.ManageStaff; /// /// EN: Owner's user ID extracted from JWT claims by the controller. /// VI: User ID của chủ merchant được lấy từ JWT claims bởi controller. diff --git a/services/merchant-service-net/src/MerchantService.API/Program.cs b/services/merchant-service-net/src/MerchantService.API/Program.cs index 8e15e61a..0bcd0b59 100644 --- a/services/merchant-service-net/src/MerchantService.API/Program.cs +++ b/services/merchant-service-net/src/MerchantService.API/Program.cs @@ -32,12 +32,17 @@ try cfg.RegisterServicesFromAssemblyContaining(); cfg.AddOpenBehavior(typeof(LoggingBehavior<,>)); cfg.AddOpenBehavior(typeof(ValidatorBehavior<,>)); + cfg.AddOpenBehavior(typeof(PermissionAuthorizationBehavior<,>)); cfg.AddOpenBehavior(typeof(TransactionBehavior<,>)); }); // EN: Add FluentValidation / VI: Thêm FluentValidation builder.Services.AddValidatorsFromAssemblyContaining(); + // EN: Required for PermissionAuthorizationBehavior to read JWT claims + // VI: Cần thiết cho PermissionAuthorizationBehavior đọc JWT claims + builder.Services.AddHttpContextAccessor(); + // EN: Add controllers / VI: Thêm controllers builder.Services.AddControllers(); diff --git a/services/merchant-service-net/src/MerchantService.Domain/AggregatesModel/MerchantStaffAggregate/PermissionConstants.cs b/services/merchant-service-net/src/MerchantService.Domain/AggregatesModel/MerchantStaffAggregate/PermissionConstants.cs new file mode 100644 index 00000000..d9946e0a --- /dev/null +++ b/services/merchant-service-net/src/MerchantService.Domain/AggregatesModel/MerchantStaffAggregate/PermissionConstants.cs @@ -0,0 +1,37 @@ +namespace MerchantService.Domain.AggregatesModel.MerchantStaffAggregate; + +/// +/// EN: Maps permission claim strings (from IAM role_claims) to StaffPermissions bitmask. +/// VI: Map chuỗi permission claim (từ IAM role_claims) sang StaffPermissions bitmask. +/// +public static class PermissionConstants +{ + public const string ClaimType = "permission"; + + private static readonly Dictionary Map = new(StringComparer.OrdinalIgnoreCase) + { + ["ViewSales"] = StaffPermissions.ViewSales, + ["ProcessPayment"] = StaffPermissions.ProcessPayment, + ["RefundOrder"] = StaffPermissions.RefundOrder, + ["ManageInventory"] = StaffPermissions.ManageInventory, + ["ViewReports"] = StaffPermissions.ViewReports, + ["ManageStaff"] = StaffPermissions.ManageStaff, + ["ManageSettings"] = StaffPermissions.ManageSettings, + ["All"] = StaffPermissions.All, + }; + + /// + /// EN: Convert JWT permission claim values to StaffPermissions bitmask. + /// VI: Chuyển đổi giá trị permission claim từ JWT sang StaffPermissions bitmask. + /// + public static StaffPermissions FromClaims(IEnumerable claimValues) + { + var result = StaffPermissions.None; + foreach (var val in claimValues) + { + if (Map.TryGetValue(val, out var perm)) + result |= perm; + } + return result; + } +}