feat(enforcement): add MediatR permission authorization behavior
Phase 2 of permission management — enforcement in MerchantService: - PermissionConstants: maps JWT "permission" claim strings to StaffPermissions bitmask via FromClaims() method - IRequirePermission: marker interface for commands needing permission check (StaffPermissions RequiredPermission property) - PermissionAuthorizationBehavior: MediatR pipeline behavior that reads permission claims from HttpContext.User, converts to bitmask, validates against IRequirePermission.RequiredPermission. Skips non-annotated commands. - Registered in MediatR pipeline after Validator, before Transaction - Annotated 3 staff commands with ManageStaff permission: InviteStaffCommand, CreateActiveStaffCommand, UpdateStaffCommand - Added HttpContextAccessor DI registration Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
using MerchantService.Domain.AggregatesModel.MerchantStaffAggregate;
|
||||
|
||||
namespace MerchantService.API.Application.Behaviors;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public interface IRequirePermission
|
||||
{
|
||||
/// <summary>
|
||||
/// EN: The permission required to execute this command.
|
||||
/// VI: Permission cần thiết để thực thi command này.
|
||||
/// </summary>
|
||||
StaffPermissions RequiredPermission { get; }
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using MediatR;
|
||||
using MerchantService.Domain.AggregatesModel.MerchantStaffAggregate;
|
||||
|
||||
namespace MerchantService.API.Application.Behaviors;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class PermissionAuthorizationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : IRequest<TResponse>
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly ILogger<PermissionAuthorizationBehavior<TRequest, TResponse>> _logger;
|
||||
|
||||
public PermissionAuthorizationBehavior(
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<PermissionAuthorizationBehavior<TRequest, TResponse>> logger)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<TResponse> Handle(
|
||||
TRequest request,
|
||||
RequestHandlerDelegate<TResponse> 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}");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public record InviteStaffCommand : IRequest<InviteStaffResult>
|
||||
public record InviteStaffCommand : IRequest<InviteStaffResult>, IRequirePermission
|
||||
{
|
||||
public StaffPermissions RequiredPermission => StaffPermissions.ManageStaff;
|
||||
/// <summary>
|
||||
/// 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<InviteStaffCommand, Inv
|
||||
/// EN: Command to create a staff member directly as Active (when owner creates IAM account for them).
|
||||
/// VI: Command để tạo nhân viên trực tiếp Active (khi chủ DN tạo tài khoản IAM cho họ).
|
||||
/// </summary>
|
||||
public record CreateActiveStaffCommand : IRequest<InviteStaffResult>
|
||||
public record CreateActiveStaffCommand : IRequest<InviteStaffResult>, IRequirePermission
|
||||
{
|
||||
public StaffPermissions RequiredPermission => StaffPermissions.ManageStaff;
|
||||
/// <summary>
|
||||
/// 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<CreateActiveStaff
|
||||
/// EN: Command to update a staff member.
|
||||
/// VI: Command để cập nhật nhân viên.
|
||||
/// </summary>
|
||||
public record UpdateStaffCommand : IRequest<bool>
|
||||
public record UpdateStaffCommand : IRequest<bool>, IRequirePermission
|
||||
{
|
||||
public StaffPermissions RequiredPermission => StaffPermissions.ManageStaff;
|
||||
/// <summary>
|
||||
/// 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.
|
||||
|
||||
@@ -32,12 +32,17 @@ try
|
||||
cfg.RegisterServicesFromAssemblyContaining<Program>();
|
||||
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<Program>();
|
||||
|
||||
// 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();
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
namespace MerchantService.Domain.AggregatesModel.MerchantStaffAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static class PermissionConstants
|
||||
{
|
||||
public const string ClaimType = "permission";
|
||||
|
||||
private static readonly Dictionary<string, StaffPermissions> 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,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// EN: Convert JWT permission claim values to StaffPermissions bitmask.
|
||||
/// VI: Chuyển đổi giá trị permission claim từ JWT sang StaffPermissions bitmask.
|
||||
/// </summary>
|
||||
public static StaffPermissions FromClaims(IEnumerable<string> claimValues)
|
||||
{
|
||||
var result = StaffPermissions.None;
|
||||
foreach (var val in claimValues)
|
||||
{
|
||||
if (Map.TryGetValue(val, out var perm))
|
||||
result |= perm;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user