fix(auth): кассир без модуля «Деньги», кладовщик со складскими правами, ИЛИ-семантика прав
Роль «Кассир» больше не имеет MoneyView (видит только свою смену/Z через CashShiftOperate); роль «Кладовщик» получает InventoryEdit/EnterEdit/LossEdit (веб уже показывал эти пункты — API теперь совпадает). PermissionRequirement поддерживает список прав через запятую (ИЛИ); CashShifts List/Get/Report → MoneyView,CashShiftOperate. DevDataSeeder: дефолты + backfill существующих орг. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
659a749d7f
commit
24646e0649
|
|
@ -64,7 +64,7 @@ public record DrawerOpInput(
|
|||
[Range(0.0001, 1e12)] decimal Amount,
|
||||
[Required, MaxLength(500)] string Reason);
|
||||
|
||||
[HttpGet, RequiresPermission("MoneyView")]
|
||||
[HttpGet, RequiresPermission("MoneyView,CashShiftOperate")]
|
||||
public async Task<ActionResult<PagedResult<CashShiftRow>>> List(
|
||||
[FromQuery] PagedRequest req,
|
||||
[FromQuery] CashShiftStatus? status,
|
||||
|
|
@ -115,7 +115,7 @@ public record DrawerOpInput(
|
|||
return new PagedResult<CashShiftRow> { Items = items, Total = total, Page = req.Page, PageSize = req.Take };
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}"), RequiresPermission("MoneyView")]
|
||||
[HttpGet("{id:guid}"), RequiresPermission("MoneyView,CashShiftOperate")]
|
||||
public async Task<ActionResult<object>> Get(Guid id, CancellationToken ct)
|
||||
{
|
||||
var s = await _db.CashShifts.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
|
|
@ -172,7 +172,7 @@ public async Task<ActionResult<object>> Drawer(Guid id, [FromBody] DrawerOpInput
|
|||
}
|
||||
|
||||
/// <summary>Z-отчёт: все цифры смены + внесения/выплаты для печати.</summary>
|
||||
[HttpGet("{id:guid}/report"), RequiresPermission("MoneyView")]
|
||||
[HttpGet("{id:guid}/report"), RequiresPermission("MoneyView,CashShiftOperate")]
|
||||
public async Task<ActionResult<object>> Report(Guid id, CancellationToken ct)
|
||||
{
|
||||
var s = await _db.CashShifts.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||
|
|
|
|||
|
|
@ -57,7 +57,8 @@ public sealed class PermissionAuthorizationHandler : AuthorizationHandler<Permis
|
|||
.Select(e => e.Role)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (role is not null && HasPermission(role.Permissions, requirement.Permission))
|
||||
// ИЛИ-семантика: достаточно любого из перечисленных прав.
|
||||
if (role is not null && requirement.Permissions.Any(p => HasPermission(role.Permissions, p)))
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,18 @@
|
|||
|
||||
namespace foodmarket.Api.Infrastructure.Authorization;
|
||||
|
||||
/// <summary>Требование «у текущего пользователя включён флаг права
|
||||
/// <see cref="Permission"/> в его роли (RolePermissions)». Имя права = имя
|
||||
/// boolean-свойства <c>RolePermissions</c> (например, "ProductsEdit").</summary>
|
||||
/// <summary>Требование «у текущего пользователя включён ХОТЯ БЫ ОДИН из флагов
|
||||
/// прав <see cref="Permissions"/> в его роли (RolePermissions)». Имя права = имя
|
||||
/// boolean-свойства <c>RolePermissions</c> (например, "ProductsEdit"). Несколько
|
||||
/// прав через запятую = ИЛИ-семантика (например "MoneyView,CashShiftOperate" —
|
||||
/// эндпоинт доступен и финансисту, и кассиру для своей смены).</summary>
|
||||
public sealed class PermissionRequirement : IAuthorizationRequirement
|
||||
{
|
||||
public string Permission { get; }
|
||||
public IReadOnlyList<string> Permissions { get; }
|
||||
|
||||
public PermissionRequirement(string permission) => Permission = permission;
|
||||
/// <summary>Первое право — для обратной совместимости/логов.</summary>
|
||||
public string Permission => Permissions[0];
|
||||
|
||||
public PermissionRequirement(string permission)
|
||||
=> Permissions = permission.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,10 +144,10 @@ public async Task StartAsync(CancellationToken ct)
|
|||
}
|
||||
|
||||
/// <summary>Идемпотентный back-fill модуля «Деньги» по всем организациям:
|
||||
/// системной роли «Кассир» проставляет MoneyView + CashShiftOperate (если
|
||||
/// ещё не стоят), и гарантирует 5 системных статей расходов в каждой орге.
|
||||
/// Нужно только для орг, созданных ДО Sprint 33 — у новых всё ставит
|
||||
/// <see cref="SeedEmployeeRolesAsync"/>.</summary>
|
||||
/// системной роли «Кассир» гарантирует CashShiftOperate=true (своя смена) и
|
||||
/// MoneyView=FALSE (модуль «Деньги» кассиру недоступен — требование владельца),
|
||||
/// и гарантирует 5 системных статей расходов в каждой орге. Нужно для орг,
|
||||
/// созданных ДО этого изменения (у них кассиру раньше ставился MoneyView=true).</summary>
|
||||
private static async Task EnsureMoneyDefaultsAsync(AppDbContext db, CancellationToken ct)
|
||||
{
|
||||
var cashierRoles = await db.EmployeeRoles.IgnoreQueryFilters()
|
||||
|
|
@ -156,19 +156,38 @@ private static async Task EnsureMoneyDefaultsAsync(AppDbContext db, Cancellation
|
|||
var changed = false;
|
||||
foreach (var role in cashierRoles)
|
||||
{
|
||||
if (!role.Permissions.MoneyView || !role.Permissions.CashShiftOperate)
|
||||
if (role.Permissions.MoneyView || !role.Permissions.CashShiftOperate)
|
||||
{
|
||||
// Permissions — owned-тип; мутация скалярных флагов на
|
||||
// отслеживаемой сущности ловится DetectChanges при SaveChanges.
|
||||
// Остальные права кассира (ProductsView/RetailSalesOperate/…)
|
||||
// сохраняются — мы их не трогаем.
|
||||
role.Permissions.MoneyView = true;
|
||||
role.Permissions.MoneyView = false;
|
||||
role.Permissions.CashShiftOperate = true;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) await db.SaveChangesAsync(ct);
|
||||
|
||||
// Back-fill прав кладовщика: системной роли «Кладовщик» нужны права на
|
||||
// инвентаризацию/оприходование/списание (веб их показывает, API должен
|
||||
// совпадать). Старые орги их не имели → ловили 403.
|
||||
var keeperRoles = await db.EmployeeRoles.IgnoreQueryFilters()
|
||||
.Where(r => r.IsSystem && r.Name == "Кладовщик")
|
||||
.ToListAsync(ct);
|
||||
var keeperChanged = false;
|
||||
foreach (var role in keeperRoles)
|
||||
{
|
||||
if (!role.Permissions.InventoryEdit || !role.Permissions.EnterEdit || !role.Permissions.LossEdit)
|
||||
{
|
||||
role.Permissions.InventoryEdit = true;
|
||||
role.Permissions.EnterEdit = true;
|
||||
role.Permissions.LossEdit = true;
|
||||
keeperChanged = true;
|
||||
}
|
||||
}
|
||||
if (keeperChanged) await db.SaveChangesAsync(ct);
|
||||
|
||||
var orgIds = await db.Organizations.IgnoreQueryFilters().Select(o => o.Id).ToListAsync(ct);
|
||||
foreach (var orgId in orgIds)
|
||||
await SeedExpenseItemsAsync(db, orgId, ct);
|
||||
|
|
@ -303,10 +322,11 @@ private static async Task SeedEmployeeRolesAsync(AppDbContext db, Guid orgId, Ca
|
|||
ProductsView = true,
|
||||
SuppliesView = true, SuppliesEdit = true, SuppliesPost = true,
|
||||
StocksView = true,
|
||||
// Складская работа кладовщика: инвентаризация, оприходование,
|
||||
// списание (веб уже показывает эти пункты — API должен совпадать).
|
||||
InventoryEdit = true, EnterEdit = true, LossEdit = true,
|
||||
// Кладовщику деньги не положены — только видимость оплаченности
|
||||
// приёмки через SuppliesView (payedSum в DTO). MoneyView=false
|
||||
// здесь, этим флаг «кладовщик без доступа к деньгам» (простой
|
||||
// режим, п.7 ТЗ модуля).
|
||||
// приёмки через SuppliesView (payedSum в DTO). MoneyView=false.
|
||||
},
|
||||
};
|
||||
var cashier = new EmployeeRole
|
||||
|
|
@ -325,7 +345,11 @@ private static async Task SeedEmployeeRolesAsync(AppDbContext db, Guid orgId, Ca
|
|||
// вносить/выплачивать деньги в ней. Чужие смены и движения
|
||||
// по банковским счетам ему недоступны (контроллер фильтрует).
|
||||
CashShiftOperate = true,
|
||||
MoneyView = true,
|
||||
// Кассир НЕ видит модуль «Деньги» (счета/платежи/долги/чужие
|
||||
// смены) — требование владельца «кассир не видит денег». Свою
|
||||
// смену и Z-отчёт он видит через CashShiftOperate (эндпоинты
|
||||
// shifts List/Get/Report принимают его право по ИЛИ-семантике).
|
||||
MoneyView = false,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue