[CLAUDE] Auth: golive Văn phòng số — public Read+Create module Office cho mọi role (allow-list 16 key) + 6 test
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m33s

Anh chốt "public văn phòng số cho all user eoffice". Mở quyền Xem + Tạo (self-service) module Office cho mọi role trên cả 2 app.

- NEW SeedAllRolesOfficeModulePermissionsAsync (DbInitializer): grant CanRead+CanCreate=true cho allow-list 16 key Office, chạy SAU RevokeTemporarilyHiddenModulesAsync để THẮNG revoke (mirror đúng pattern S65 HRM public). Upgrade-only: nâng false→true trên row prod đã có, KHÔNG hạ, KHÔNG đụng CanUpdate/CanDelete. No migration (seed-logic, idempotent).
- ALLOW-LIST 16: Off + Off_Dashboard + Off_DanhBa + Off_PhongHop(View/Book) + Off_DeXuat(List/Create/Inbox) + Off_DonTu(Leave/Ot/Travel) + Off_DatXe + Off_ItTicket.
- GIỮ ẨN (ngoài allow-list → revoke vẫn che non-Admin): Off_PhongHop_Manage (admin CRUD phòng), Off_AttendanceReport (báo cáo chấm công — riêng tư), Off_ChamCong (Cá nhân — golive riêng). HRM (trừ Hồ sơ NS S65) + Personal VẪN ẩn (anh chỉ mở Office).
- reviewer PASS 0 blocker (security): cascade-safe (Off KHÔNG phải inherit-root trong GetMyMenuTree → excluded-3 giữ false, không lan); KHÔNG mở write-path thật (Office controller dùng [Authorize] self-service + [Authorize(Roles=Admin)] cho admin-write — CanCreate chỉ mở menu + nút tạo FE, API authz độc lập menu key; quản lý phòng double-protected).
- +6 test OfficeModulePermissionSeedTests (286→292) lock: allow-list read+create=true · excluded-3 stay hidden (load-bearing) · admin not demoted · no-leak HRM/Personal · upgrade-only preserves admin-raised Update/Delete.
- Build slnx 0/0 · dotnet test 292 PASS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-06-17 10:33:32 +07:00
parent c556f6cfa2
commit 1f8947e763
5 changed files with 385 additions and 2 deletions

View File

@ -0,0 +1,297 @@
using System.Reflection;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using SolutionErp.Domain.Identity;
using SolutionErp.Infrastructure.Persistence;
using SolutionErp.Infrastructure.Tests.Common;
namespace SolutionErp.Infrastructure.Tests.Application;
// S69 test-after (UAT mode — GOLIVE "public Văn phòng số cho all user eoffice" shipped S69).
// Mirror HrmProfilePermissionSeedTests (S65/S66) — same reflection harness, but Office grant
// mở READ + CREATE (self-service tạo phiếu) trên allow-list 16 key, KHÁC HRM chỉ READ 2 key.
//
// DbInitializer permission-seed chain (SeedAsync order, line 2042-2055):
// 1. RevokeTemporarilyHiddenModulesAsync (S58, :2166) → set MỌI Hrm*/Off*/Personal = false
// cho non-Admin (StartsWith("Off") bắt cả Off, Off_Dashboard, Off_PhongHop_Manage, …).
// 2. SeedAllRolesOfficeModulePermissionsAsync (S69, :2277) → CHẠY SAU để THẮNG revoke,
// nâng RIÊNG allow-list 16 key = CanRead+CanCreate=true (upgrade-only). Excluded 3 key
// (Off_PhongHop_Manage / Off_AttendanceReport / Off_ChamCong) KHÔNG nâng → revoke vẫn che.
//
// Kết quả mong đợi (anh chốt public Văn phòng số self-service, ẩn admin-manage + report + chấm công):
// - non-Admin role: 16 allow-list key → CanRead=true AND CanCreate=true (mở menu + nút tạo).
// - non-Admin role: 3 excluded key → CanRead=false (revoke che, seed không nâng) ← LOAD-BEARING.
// - non-Admin role: CanUpdate/CanDelete trên allow-list VẪN false (grant chỉ mở read+create).
// - non-Admin role: HRM (trừ Hồ sơ NS S65) + Personal VẪN ẩn (Office grant không đụng).
// - Admin role: KHÔNG bị revoke (giữ nguyên — quản trị/CRUD phòng/duyệt phiếu).
//
// 2 method `private static` trong DbInitializer → invoke qua REFLECTION (BindingFlags.NonPublic
// |Static). Test ĐÚNG behavior code thật, KHÔNG re-implement. Signature cả 2:
// (ApplicationDbContext db, RoleManager<Role> roleManager, ILogger logger).
//
// FK lưu ý (PermissionConfiguration): Permission.MenuKey → MenuItem.Key (Cascade) +
// Permission.RoleId → Role (Cascade). → PHẢI seed MenuItem rows + Role TRƯỚC khi seed
// Permission rows (nếu không SQLite FK Error 19).
public class OfficeModulePermissionSeedTests
{
// Allow-list 16 key — sau chain PHẢI có CanRead=true AND CanCreate=true (non-Admin).
private static readonly string[] AllowListKeys =
{
MenuKeys.Off, MenuKeys.OffDashboard, MenuKeys.OffDanhBa,
MenuKeys.OffPhongHop, MenuKeys.OffPhongHopView, MenuKeys.OffPhongHopBook,
MenuKeys.OffDeXuat, MenuKeys.OffDeXuatList, MenuKeys.OffDeXuatCreate, MenuKeys.OffDeXuatInbox,
MenuKeys.OffDonTu, MenuKeys.OffDonTuLeave, MenuKeys.OffDonTuOt, MenuKeys.OffDonTuTravel,
MenuKeys.OffDatXe, MenuKeys.OffItTicket,
};
// Excluded 3 key — sau chain PHẢI GIỮ CanRead=false (revoke che, office-grant KHÔNG đụng).
// Đây là security invariant trọng tâm: admin-manage / báo cáo / chấm công KHÔNG public.
private static readonly string[] ExcludedOfficeKeys =
{
MenuKeys.OffPhongHopManage, // "Off_PhongHop_Manage" — Admin CRUD phòng họp
MenuKeys.OffAttendanceReport, // "Off_AttendanceReport" — báo cáo chấm công (admin)
MenuKeys.OffChamCong, // "Off_ChamCong" — chấm công GPS (re-parent Cá nhân, golive riêng)
};
// HRM (non-profile) + Personal — phải GIỮ ẩn sau Office grant (cross-module leak check).
// Hrm_HoSo cố ý KHÔNG đưa vào đây (S65 đã public riêng — Office chain không touch nó).
private static readonly string[] OtherModuleHiddenKeys =
{
MenuKeys.HrmDashboard,
MenuKeys.Personal,
};
// Tất cả MenuItem key cần seed (FK target cho Permission.MenuKey).
private static readonly string[] AllMenuKeys =
AllowListKeys
.Concat(ExcludedOfficeKeys)
.Concat(OtherModuleHiddenKeys)
.ToArray();
private static async Task InvokeRevokeAsync(IdentityFixture fix)
=> await InvokePrivateSeedAsync(fix, "RevokeTemporarilyHiddenModulesAsync");
private static async Task InvokeOfficeSeedAsync(IdentityFixture fix)
=> await InvokePrivateSeedAsync(fix, "SeedAllRolesOfficeModulePermissionsAsync");
// Reflection invoke private static (ApplicationDbContext, RoleManager<Role>, ILogger).
private static async Task InvokePrivateSeedAsync(IdentityFixture fix, string methodName)
{
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
var rm = fix.Services.GetRequiredService<RoleManager<Role>>();
var mi = typeof(DbInitializer).GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Static);
mi.Should().NotBeNull($"DbInitializer.{methodName} phải tồn tại (private static) — đổi signature thì cập nhật test");
var task = (Task)mi!.Invoke(null, new object[] { db, rm, NullLogger.Instance })!;
await task;
}
// Ensure roles tồn tại (Admin + non-Admin). Trả (adminId, nonAdminId).
private static async Task<(Guid adminId, Guid nonAdminId)> SeedRolesAsync(IdentityFixture fix)
{
var rm = fix.Services.GetRequiredService<RoleManager<Role>>();
var admin = new Role { Id = Guid.NewGuid(), Name = AppRoles.Admin };
var drafter = new Role { Id = Guid.NewGuid(), Name = AppRoles.Drafter };
await rm.CreateAsync(admin);
await rm.CreateAsync(drafter);
return (admin.Id, drafter.Id);
}
// Seed MenuItem rows (FK target cho Permission.MenuKey).
private static async Task SeedMenuItemsAsync(TestApplicationDbContext db)
{
foreach (var key in AllMenuKeys)
db.MenuItems.Add(new MenuItem { Key = key, Label = key });
await db.SaveChangesAsync(CancellationToken.None);
}
// Seed 1 Permission row (read-only mặc định — mô phỏng seed cũ trước revoke).
private static void AddPerm(TestApplicationDbContext db, Guid roleId, string menuKey, bool canRead)
=> db.Permissions.Add(new Permission
{
RoleId = roleId,
MenuKey = menuKey,
CanRead = canRead,
CanCreate = false,
CanUpdate = false,
CanDelete = false,
});
private static async Task<Permission?> GetPermAsync(TestApplicationDbContext db, Guid roleId, string menuKey)
=> await db.Permissions.AsNoTracking()
.FirstOrDefaultAsync(p => p.RoleId == roleId && p.MenuKey == menuKey);
private static async Task<bool> CanReadAsync(TestApplicationDbContext db, Guid roleId, string menuKey)
=> (await GetPermAsync(db, roleId, menuKey))?.CanRead ?? false;
// ============================================================
// Full chain: pre-grant Off* → Revoke → OfficeSeed (đúng SeedAsync order)
// ============================================================
[Fact]
public async Task Chain_NonAdmin_AllowList16_ReadAndCreateTrue_Excluded3_StayHidden()
{
using var fix = new IdentityFixture();
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
var (adminId, nonAdminId) = await SeedRolesAsync(fix);
await SeedMenuItemsAsync(db);
// PRE-STATE: non-Admin từng được grant CanRead=true trên MỌI Off* (mô phỏng seed cũ)
// → để revoke có gì đó để thu hồi (gồm cả 3 excluded). Admin cũng có (KHÔNG bị revoke).
foreach (var key in AllowListKeys.Concat(ExcludedOfficeKeys))
{
AddPerm(db, nonAdminId, key, canRead: true);
AddPerm(db, adminId, key, canRead: true);
}
await db.SaveChangesAsync(CancellationToken.None);
// CHAIN đúng thứ tự SeedAsync: revoke (Off* → false) → office-grant (allow-list → read+create).
await InvokeRevokeAsync(fix);
await InvokeOfficeSeedAsync(fix);
// non-Admin: allow-list 16 key mở READ + CREATE (self-service Văn phòng số).
foreach (var key in AllowListKeys)
{
var row = await GetPermAsync(db, nonAdminId, key);
row.Should().NotBeNull($"{key} phải có row sau office-grant");
row!.CanRead.Should().BeTrue($"{key} mở Read cho user thường (golive Văn phòng số)");
row.CanCreate.Should().BeTrue($"{key} mở Create cho user thường (self-service tạo phiếu)");
}
// ⭐ LOAD-BEARING: 3 excluded key VẪN ẩn (revoke che, office-grant KHÔNG nâng).
foreach (var key in ExcludedOfficeKeys)
(await CanReadAsync(db, nonAdminId, key)).Should()
.BeFalse($"{key} KHÔNG public — chỉ Admin (admin-manage / báo cáo / chấm công)");
}
[Fact]
public async Task Chain_NonAdmin_AllowList_UpdateAndDelete_StayFalse()
{
// Grant chỉ mở read+create — Update/Delete (sửa/xóa phòng, duyệt cấp cao) GIỮ false.
using var fix = new IdentityFixture();
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
var (_, nonAdminId) = await SeedRolesAsync(fix);
await SeedMenuItemsAsync(db);
foreach (var key in AllowListKeys)
AddPerm(db, nonAdminId, key, canRead: true);
await db.SaveChangesAsync(CancellationToken.None);
await InvokeRevokeAsync(fix);
await InvokeOfficeSeedAsync(fix);
foreach (var key in AllowListKeys)
{
var row = await GetPermAsync(db, nonAdminId, key);
row!.CanUpdate.Should().BeFalse($"{key} KHÔNG mở Update cho non-Admin (write quản lý khóa ở controller)");
row.CanDelete.Should().BeFalse($"{key} KHÔNG mở Delete cho non-Admin");
}
}
[Fact]
public async Task Chain_NonAdmin_OfficeGrant_DoesNotLeakIntoHrmOrPersonal()
{
// Office grant CHỈ đụng allow-list Off* — KHÔNG vô tình mở HRM (Dashboard) hay Personal.
using var fix = new IdentityFixture();
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
var (_, nonAdminId) = await SeedRolesAsync(fix);
await SeedMenuItemsAsync(db);
// Pre-grant cả HRM-dashboard + Personal (để revoke thu hồi) — office-grant không được nâng lại.
foreach (var key in AllowListKeys.Concat(OtherModuleHiddenKeys))
AddPerm(db, nonAdminId, key, canRead: true);
await db.SaveChangesAsync(CancellationToken.None);
await InvokeRevokeAsync(fix);
await InvokeOfficeSeedAsync(fix);
foreach (var key in OtherModuleHiddenKeys)
(await CanReadAsync(db, nonAdminId, key)).Should()
.BeFalse($"{key} GIỮ ẩn — Office grant KHÔNG mở HRM/Personal");
}
[Fact]
public async Task Chain_Admin_NotRevoked_KeepsAllOfficeReadAfterChain()
{
using var fix = new IdentityFixture();
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
var (adminId, nonAdminId) = await SeedRolesAsync(fix);
await SeedMenuItemsAsync(db);
// Admin có read trên cả allow-list + 3 excluded → KHÔNG bị revoke đụng.
foreach (var key in AllowListKeys.Concat(ExcludedOfficeKeys))
{
AddPerm(db, adminId, key, canRead: true);
AddPerm(db, nonAdminId, key, canRead: true);
}
await db.SaveChangesAsync(CancellationToken.None);
await InvokeRevokeAsync(fix);
await InvokeOfficeSeedAsync(fix);
// Admin GIỮ nguyên CanRead=true trên mọi Off* (kể cả 3 excluded) — revoke loại trừ Admin.
foreach (var key in AllowListKeys.Concat(ExcludedOfficeKeys))
(await CanReadAsync(db, adminId, key)).Should().BeTrue($"Admin KHÔNG bị revoke trên {key}");
}
// ============================================================
// SeedAllRolesOfficeModulePermissionsAsync — isolated behavior
// ============================================================
[Fact]
public async Task OfficeSeed_CreatesMissingRow_ReadCreateTrue_UpdateDeleteFalse()
{
// Row chưa có (DB mới) → tạo CanRead=true + CanCreate=true, Update/Delete=false (line 2312).
using var fix = new IdentityFixture();
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
var (_, nonAdminId) = await SeedRolesAsync(fix);
await SeedMenuItemsAsync(db);
// KHÔNG seed Permission row nào.
await InvokeOfficeSeedAsync(fix);
var off = await GetPermAsync(db, nonAdminId, MenuKeys.Off);
off.Should().NotBeNull("office-seed tạo row mới khi chưa tồn tại");
off!.CanRead.Should().BeTrue();
off.CanCreate.Should().BeTrue();
off.CanUpdate.Should().BeFalse();
off.CanDelete.Should().BeFalse();
// Excluded key KHÔNG được office-seed tạo row (chỉ allow-list).
var manage = await GetPermAsync(db, nonAdminId, MenuKeys.OffPhongHopManage);
manage.Should().BeNull("office-seed KHÔNG tạo row cho Off_PhongHop_Manage (ngoài allow-list)");
}
[Fact]
public async Task OfficeSeed_UpgradesExistingFalseRow_PreservesAdminRaisedUpdateDelete()
{
// Upgrade-only path (line 2302-2308): row CanRead=false + CanCreate=false (revoke vừa set)
// → nâng read+create=true. NHƯNG nếu admin đã chỉnh Update/Delete=true → KHÔNG hạ
// (office-grant chỉ đụng Read/Create). Lock invariant "upgrade-only, không phá quyền admin".
using var fix = new IdentityFixture();
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
var (_, nonAdminId) = await SeedRolesAsync(fix);
await SeedMenuItemsAsync(db);
// Row tiền tồn: Read/Create=false (như sau revoke) nhưng Update/Delete=true (admin nâng tay).
db.Permissions.Add(new Permission
{
RoleId = nonAdminId,
MenuKey = MenuKeys.OffDeXuatCreate,
CanRead = false,
CanCreate = false,
CanUpdate = true,
CanDelete = true,
});
await db.SaveChangesAsync(CancellationToken.None);
await InvokeOfficeSeedAsync(fix);
var row = await GetPermAsync(db, nonAdminId, MenuKeys.OffDeXuatCreate);
row!.CanRead.Should().BeTrue("upgrade nâng Read=true");
row.CanCreate.Should().BeTrue("upgrade nâng Create=true");
row.CanUpdate.Should().BeTrue("office-grant KHÔNG hạ Update đã được admin nâng");
row.CanDelete.Should().BeTrue("office-grant KHÔNG hạ Delete đã được admin nâng");
}
}