fix(auth): Cashier/Storekeeper больше не видят /api/organization/employees + Identity-роль маппится из orgRole
Найдено в e2e-прогоне (отчёт reports/full-cycle-2026-05-07-baseline.md): - GET /api/organization/employees вернул 200 для Cashier (ожидалось 403). - Cashier у созданного через POST /employees вообще не получает Identity-роли — серверная авторизация не работает. Корни: 1. EmployeesController имел class-level [Authorize] без roles, List/Get не имели per-method [Authorize(Roles=...)] — поэтому любой аутентифицированный юзер мог читать список сотрудников. 2. EmployeesController.Create при createAccount=true вызывал _userMgr.CreateAsync, но НЕ вызывал AddToRoleAsync — у созданного юзера не было ни одной Identity-роли. Фиксы: - Class-level `[Authorize(Roles = "SuperAdmin,Admin")]` на EmployeesController. Теперь List/Get/Create/Update/Delete все требуют Admin (или SuperAdmin override). Per-method дубль убран. - Новый helper `Api/Infrastructure/IdentityRoleMapper.cs`: Администратор → Admin, Кладовщик → Storekeeper, Кассир → Cashier. Кастомные orgRole не получают Identity-роли (это by-design — они дают UI-permissions внутри org, без доступа к role-locked endpoint'ам). - EmployeesController.Create вызывает AddToRoleAsync с замапленной Identity-ролью если такая есть. - SuperAdminEmployeesController.Create аналогично — вместо хардкод "Admin" использует mapper с fallback на "Admin" (по запросу юзера при создании учётки SuperAdmin'ом). После фикса в e2e: - Cashier → GET /api/organization/employees → 403 (было 200). - /connect/token → /api/me содержит roles=["Cashier"]. - Cashier → /api/sales/retail-sales → 200 (рабочая авторизация).
This commit is contained in:
parent
7bb941259a
commit
bcf81c57ee
|
|
@ -13,7 +13,7 @@
|
||||||
namespace foodmarket.Api.Controllers.Organizations;
|
namespace foodmarket.Api.Controllers.Organizations;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Authorize]
|
[Authorize(Roles = "SuperAdmin,Admin")]
|
||||||
[Route("api/organization/employees")]
|
[Route("api/organization/employees")]
|
||||||
public class EmployeesController : ControllerBase
|
public class EmployeesController : ControllerBase
|
||||||
{
|
{
|
||||||
|
|
@ -111,12 +111,12 @@ public async Task<ActionResult<EmployeeDto>> Get(Guid id, CancellationToken ct)
|
||||||
return dto is null ? NotFound() : Ok(dto);
|
return dto is null ? NotFound() : Ok(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost, Authorize(Roles = "SuperAdmin,Admin")]
|
[HttpPost]
|
||||||
public async Task<ActionResult<EmployeeCreateResult>> Create([FromBody] EmployeeInput input, CancellationToken ct)
|
public async Task<ActionResult<EmployeeCreateResult>> Create([FromBody] EmployeeInput input, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var orgId = _tenant.OrganizationId ?? throw new InvalidOperationException("No tenant.");
|
var orgId = _tenant.OrganizationId ?? throw new InvalidOperationException("No tenant.");
|
||||||
var roleExists = await _db.EmployeeRoles.AnyAsync(r => r.Id == input.RoleId, ct);
|
var role = await _db.EmployeeRoles.FirstOrDefaultAsync(r => r.Id == input.RoleId, ct);
|
||||||
if (!roleExists) return BadRequest(new { error = "Роль не найдена." });
|
if (role is null) return BadRequest(new { error = "Роль не найдена." });
|
||||||
|
|
||||||
var employee = new Employee
|
var employee = new Employee
|
||||||
{
|
{
|
||||||
|
|
@ -151,6 +151,15 @@ public async Task<ActionResult<EmployeeCreateResult>> Create([FromBody] Employee
|
||||||
if (!result.Succeeded)
|
if (!result.Succeeded)
|
||||||
return BadRequest(new { error = string.Join("; ", result.Errors.Select(e => e.Description)) });
|
return BadRequest(new { error = string.Join("; ", result.Errors.Select(e => e.Description)) });
|
||||||
employee.UserId = user.Id;
|
employee.UserId = user.Id;
|
||||||
|
|
||||||
|
// Identity-роль маппится из orgRole.Name. Кастомные orgRole не
|
||||||
|
// получают Identity-роли — они только дают UI-permissions, без
|
||||||
|
// доступа к role-locked endpoint'ам.
|
||||||
|
var identityRole = foodmarket.Api.Infrastructure.IdentityRoleMapper.FromOrgRoleName(role.Name);
|
||||||
|
if (identityRole is not null)
|
||||||
|
{
|
||||||
|
await _userMgr.AddToRoleAsync(user, identityRole);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var rpId in input.RetailPointIds ?? [])
|
foreach (var rpId in input.RetailPointIds ?? [])
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,9 @@ public async Task<ActionResult<EmployeeDetail>> Create(Guid orgId, [FromBody] Cr
|
||||||
var ur = await _userMgr.CreateAsync(u, tempPassword);
|
var ur = await _userMgr.CreateAsync(u, tempPassword);
|
||||||
if (!ur.Succeeded)
|
if (!ur.Succeeded)
|
||||||
return BadRequest(new { error = string.Join("; ", ur.Errors.Select(x => x.Description)) });
|
return BadRequest(new { error = string.Join("; ", ur.Errors.Select(x => x.Description)) });
|
||||||
await _userMgr.AddToRoleAsync(u, "Admin");
|
// Identity-роль из orgRole (Администратор/Кладовщик/Кассир) — кастомные не получают.
|
||||||
|
var identityRole = foodmarket.Api.Infrastructure.IdentityRoleMapper.FromOrgRoleName(role.Name) ?? "Admin";
|
||||||
|
await _userMgr.AddToRoleAsync(u, identityRole);
|
||||||
newUserId = u.Id;
|
newUserId = u.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
25
src/food-market.api/Infrastructure/IdentityRoleMapper.cs
Normal file
25
src/food-market.api/Infrastructure/IdentityRoleMapper.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
namespace foodmarket.Api.Infrastructure;
|
||||||
|
|
||||||
|
/// <summary>Маппинг доменной orgRole.Name (русский, видимый юзеру в UI)
|
||||||
|
/// на Identity-роль (английский, используется в [Authorize(Roles=...)]).
|
||||||
|
/// Identity-роль определяет ЧТО юзер может вызывать через API; orgRole —
|
||||||
|
/// видимое имя в UI и набор прав для конкретной орги.
|
||||||
|
///
|
||||||
|
/// Системные orgRole сидятся при создании org (см. DevDataSeeder.SeedEmployeeRolesAsync).
|
||||||
|
/// Кастомные orgRole создаются админом орги — для них Identity-роль НЕ
|
||||||
|
/// присваивается (юзер не сможет дёрнуть Admin/Cashier/Storekeeper-only
|
||||||
|
/// endpoints, что и нужно: кастомные роли — только UI-permissions внутри org).</summary>
|
||||||
|
public static class IdentityRoleMapper
|
||||||
|
{
|
||||||
|
public static string? FromOrgRoleName(string? orgRoleName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(orgRoleName)) return null;
|
||||||
|
return orgRoleName.Trim() switch
|
||||||
|
{
|
||||||
|
"Администратор" => "Admin",
|
||||||
|
"Кладовщик" => "Storekeeper",
|
||||||
|
"Кассир" => "Cashier",
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue