23 KiB
sub-reviewer (lane 5) — LĂNG KÍNH SECURITY + DATA-RISK — S160 dry-run KHKK→HĐ
VERDICT: SUA-TRUOC-THI-CONG — 3 HIGH (F-S1/F-S2/F-S3) phải sửa spec trước khi hmw chạy W6/W8; 6 MED; 5 LOW; 1 PASS-dương.
Đích: spec-wave-w6-duong-ong-hd-v2-29-07-2026.md + spec-wave-w8-authz-29-07-2026.md + plan-cha §4.
Mọi finding = {spec-file:dòng} + {bằng chứng ĐĨA file:line}.
0. Anchor audit — spec nói vs đĩa (đo 2026-07-29, git HEAD febe6b1)
| Anchor spec | Đĩa | Verdict |
|---|---|---|
ContractFeatures.cs:334-351 GetEligiblePhases |
:334 internal static List<ContractPhase> GetEligiblePhases(IReadOnlyList<string> userRoles) → body tới :351 |
✅ ĐÚNG |
ContractFeatures.cs:421-430 PhaseActorRoles |
:421 private static readonly Dictionary<ContractPhase,string[]> PhaseActorRoles (7 entry, hết ở :429) |
✅ ĐÚNG |
ContractFeatures.cs:488-489 Forbidden |
:489 throw new ForbiddenException("Bạn không có quyền xem HĐ này.") |
✅ ĐÚNG |
ContractFeatures.cs:298 admin bypass |
:298 if (!currentUser.Roles.Contains(AppRoles.Admin)) |
✅ ĐÚNG |
ContractFeatures.cs:113-118 gen mã create-time |
:118 entity.MaHopDong = await codeGenerator.GenerateAsync(...) |
✅ ĐÚNG |
ContractFeatures.cs:632-633 delete guard |
:632 if (entity.Phase >= ContractPhase.DangInKy) |
✅ ĐÚNG |
ContractWorkflowService.cs:49-66 Reject-trước-guard |
:49 if (decision == ApprovalDecision.Reject) … :65 return; :66 } — 0 role-check, 0 phase-check |
✅ ĐÚNG (lỗ THẬT) |
ContractWorkflowService.cs:73-78 guard trình |
:73-75 if (!isAdmin && !isSystem && !actorRoles.Contains(Drafter) && !actorRoles.Contains(DeptManager)) |
✅ ĐÚNG |
ContractWorkflowService.cs:191-198 admin override |
:191 if (isAdmin) { contract.Phase = targetPhase; … } |
✅ ĐÚNG |
ContractsController.cs:13 class-trần |
:13 [Authorize] (trần, 0 policy) |
✅ ĐÚNG |
ContractsController.cs:28-29 tiền lệ |
:28 [HttpGet("deleted")] :29 [Authorize(Policy="Contracts.Read")] |
✅ ĐÚNG |
ContractsController.cs:38-40 inbox |
:38 [HttpGet("inbox")] |
✅ ĐÚNG |
SuppliersController.cs:67 khuôn Master |
:67 [Authorize(Policy="Suppliers.Update")] |
✅ ĐÚNG |
MenuKeys.cs:41 KeHoachKyKet |
:41 public const string KeHoachKyKet = "KeHoachKyKet"; + :38-40 comment "CỐ Ý NGOÀI All" |
✅ ĐÚNG |
MenuKeys.cs:168 Contracts ∈ All |
:163 All = [ … :168 Contracts, Forms, Reports, |
✅ ĐÚNG |
AuthorizePolicyRegressionTests "ĐÃ TỒN TẠI" |
tests/SolutionErp.Infrastructure.Tests/Api/AuthorizePolicyRegressionTests.cs |
✅ ĐÚNG |
plan scripts/backup-sql.ps1 |
scripts/backup-sql.ps1 có mặt |
✅ ĐÚNG |
| W8 "22 endpoint ghi" | đếm tay ContractsController.cs: POST 11 (:46,61,68,85,123,130,137,144,151,158,165) + PUT 8 (:53,179,186,193,200,207,214,221) + DELETE 3 (:75,110,172) = 22 |
✅ ĐÚNG (F-S14 dương) |
⇒ 0 anchor sai. Lỗi của bộ spec này KHÔNG nằm ở anchor — nằm ở anchor THIẾU + ngữ nghĩa seeder + hố chưa khai.
F-S1 [HIGH] W8 §2 bước 3 "seeder grant" — RẤT DỄ THÀNH NO-OP IM LẶNG trên prod ⇒ 403 giết chính dry-run
Spec: spec-wave-w8-authz-29-07-2026.md:19 — "Seeder grant (gotcha #84 — CÙNG COMMIT): cấp Contracts Create/Update cho role tham gia dry-run… Grep MỌI site seed-permission (S155 >1 site)".
Đĩa: hàng Permission cho key Contracts ĐÃ TỒN TẠI SẴN cho mọi role — DbInitializer.cs:2156-2170 ContractMenuKeys() yield MenuKeys.Contracts + Ct_* vào reviewKeys của SeedAllRolesReviewReadPermissionsAsync (:2136), và nhánh tạo mới ở :2247-2254 set CanRead=true, CanCreate=isPe(=false), CanUpdate=false, CanDelete=false.
🔴 Chỗ chết: DbInitializer.cs:2231-2245 — với key non-Pe (tức Contracts), nhánh "row đã có" là:
// Key non-Pe: skip-existing (giữ nguyên như cũ).
continue;
Nghĩa là: nếu implementer cắm grant Create/Update vào chính ContractMenuKeys()/SeedAllRolesReviewReadPermissionsAsync (nơi tự nhiên nhất vì key Contracts đang sống ở đó), thì trên prod nó chạy vào continue và KHÔNG nâng gì cả — row Contracts đã có từ đợt S159 với CanCreate=false/CanUpdate=false. Build xanh, test xanh, log seeder im, deploy xong → mọi POST/PUT bị 403 im lặng (đúng gotcha #44 mà spec đang định tránh).
Khuôn ĐÚNG có sẵn trong cùng file: SeedProcurementMasterAccessAsync :2367-2374 (if (g.R && !row.CanRead) {…} if (g.C && !row.CanCreate) {…} = upgrade-if-exists), chạy SAU revoke để thắng (:2098-2101 comment).
Spec không hề nói row đã tồn tại, không nói phải dùng ngữ nghĩa UPGRADE, không trỏ method-khuôn. "Grep MỌI site" là lời dặn tìm site, không phải lời dặn về ngữ nghĩa ghi.
Acceptance cần thêm (mô tả, không phải code): spec phải (a) khai rằng row Contracts per-role ĐÃ tồn tại với 3 cờ false, (b) pin ngữ nghĩa upgrade-if-exists + trỏ khuôn SeedProcurementMasterAccessAsync:2367-2374, (c) acceptance đo CanCreate/CanUpdate chứ không đo "có row" (đếm row = 0-bit vì row đã có sẵn).
F-S2 [HIGH] Attachment 3 handler = KHÔNG có authz nào ngoài [Authorize] trần — W8 CỐ Ý để hở, plan §4 KHÔNG khai
Spec: spec-wave-w8-authz-29-07-2026.md:18 — "GET giữ class-trần (đọc mở như hiện trạng, trừ /deleted)". plan-cha §4 (dòng 51-57) không có mục nào về attachment.
Đĩa ContractAttachmentFeatures.cs:
:56-90 UploadContractAttachmentCommandHandler— ctor chỉ(IApplicationDbContext, IFileStorage, IChangelogService); 0ICurrentUser, 0 check Drafter, 0 check role, 0 check Phase. Load HĐ bằng Id rồi ghi thẳng (:63-64→:84-85).:111-127 DownloadContractAttachmentQueryHandler— 0 IDOR guard. Bất kỳ user đăng nhập nào biết{contractId}/{attId}→ tải được bản scan ký/đóng dấu của HĐ bất kỳ. Đây chính là vật GĐ4 (spec-wave-w7bản cứng) sẽ upload.:130-152 DeleteContractAttachmentCommandHandler— 0 IDOR, 0 phase-guard; xoá được attachment của HĐDaPhatHanh(file storage xoá best-effort:150-151).
Đối chiếu: GetContractQueryHandler CÓ IDOR guard (ContractFeatures.cs:482-489) — tức lá chắn tồn tại ở endpoint HĐ nhưng không lan sang endpoint con attachment (ContractsController.cs:85, :103, :110).
Hệ quả cho dry-run: dữ liệu thật đưa vào ở GĐ4 là scan hợp đồng đã ký + đóng dấu. Quyết định "GET giữ class-trần" của W8 giữ nguyên lỗ đọc này. Sau O-B, DELETE {id}/attachments/{attId} rơi vào Contracts.Delete → grant cho Drafter ⇒ mọi user role Drafter xoá được scan đã ký của HĐ bất kỳ.
Cần: W8 §① phải liệt attachment như ô đỏ thứ 3 (hoặc khai tường minh "chấp nhận có ý thức" như O-A), và plan-cha §4 phải có 1 gạch đầu dòng data-risk cho attachment. Hiện tại là hố CHƯA KHAI — đúng câu hỏi (3) của lane.
F-S3 [HIGH] W6 gate 5-anchor THIẾU 2/3 call-site của chính hàm nó bảo sửa ⇒ vá 1 site sót site cùng lớp
Spec: spec-wave-w6:25 gate 5-anchor liệt ContractFeatures.cs:334-351 · :421-430 · :488-489 · ContractWorkflowService.cs:73-78 · ContractsController.cs:38-40.
Đĩa — GetEligiblePhases có ĐÚNG 3 call-site:
:301ListContractsQueryHandler(danh sách HĐ) — KHÔNG trong anchor list:381ListDeletedContractsQuery(màn "Đã xoá" S159-đợt5) — KHÔNG trong anchor list:486GetContractQueryHandler(detail) — có, qua:488-489
Chỗ chết cấu trúc: chữ ký là GetEligiblePhases(IReadOnlyList<string> userRoles) — chỉ nhận role, không nhận userId, không nhận contract. Nó không thể biểu diễn được điều kiện W6 yêu cầu ("user là ApproverUserId của Level trong workflow đã pin của HĐ đó"). Cách rẻ nhất mà implementer sẽ làm là thêm ChoDuyet vào tập role → khi đó x.c.Phase == ChoDuyet khớp ở cả :301 (list) lẫn :381 (danh sách đã xoá) lẫn :486 ⇒ mở toang toàn bộ HĐ ChoDuyet cho mọi user mang role đó.
spec-wave-w6:17 viết "(hoặc precompute-set, xem 4)" — đặt phương án AN TOÀN làm lựa chọn ngang hàng trong ngoặc, không phải bắt buộc. Với đường mòn "sửa hàm 1 chỗ", default sẽ trôi về phương án hở.
Cần: (a) anchor bổ sung :298-302 + :378-381; (b) spec CHỐT phương án precompute-set (hoặc overload nhận contractId/userId) là BẮT BUỘC, cấm mở rộng tập role trả về từ GetEligiblePhases; (c) nêu rõ :381 là màn "Đã xoá" — mở nhầm ở đây = lộ HĐ đã xoá mềm.
F-S4 [HIGH→MED] W6: ca ÂM chống-mở-toang KHÔNG có test tự động, và ca ÂM thủ công 0-bit vì không pin ROLE
Spec: spec-wave-w6:27 liệt 3 test: Inbox_ApproverCapDangCho_ThayHopDongChoDuyet · Inbox_CapKhac_KhongThayHopDong (ÂM, inbox) · View_ApproverV2_KhongPhaiDrafter_XemDuocHopDongChoDuyet (DƯƠNG, view).
⇒ 0 test ÂM cho VIEW. Rủi ro lớn nhất (spec-wave-w6:32 mục C-1 "đổi view-guard làm hở HĐ cho MỌI user") chỉ được canh bằng 1 dòng prod thủ công :30.
:30 0-bit: "user ngoài workflow + không Drafter → GET /{id} vẫn 403". Guard là role-keyed (ContractFeatures.cs:486 truyền currentUser.Roles). Nếu người kiểm chọn 1 user role HrAdmin (chỉ có DangDongDau trong GetEligiblePhases:348) thì 403 vẫn đúng kể cả khi lỗ đã mở toang cho role CostControl/Procurement. Ca ÂM không pin role của user thử ⇒ pass được mà không mang thông tin.
Cần: thêm test ÂM cho VIEW (tên cụ thể) + ca ÂM prod phải pin role của user thử = đúng role bị nghi mở (hoặc quét 13/13 role). Kèm 1 phép ÂM cho :381 (list đã xoá).
F-S5 [MED] W8 grant theo ROLE ⟂ W6/V2 duyệt theo USER-ID — 6/13 role ngoài danh sách ⇒ chuỗi duyệt V2 có thể chết giữa đường
Spec: spec-wave-w8:19 grant cho 7 role: Drafter/ProjectManager/DeptManager/Procurement/CostControl/Director/Admin. spec-wave-w8:18 map POST {id}/transitions → Contracts.Update.
Đĩa: ContractWorkflowService.cs:255-266 ApproveV2Async match approver thuần theo ApproverUserId, KHÔNG hề đọc role:
var allowedUserIds = pendingLevelGroup.Select(l => l.ApproverUserId).ToHashSet();
if (!allowedUserIds.Contains(actorUserId.Value)) throw new ForbiddenException(...)
⇒ Designer V2 gán được user bất kỳ làm Cấp duyệt. Nếu user đó mang role ∉ 7-role-list (còn lại: Finance, Accounting, Equipment, HrAdmin, AuthorizedSigner, CatalogManager…), họ 403 ngay ở tầng policy trước khi tới ApproveV2Async → HĐ kẹt ở ChoDuyet.
Ngoài ra nhánh legacy: 2 transition CUỐI đời HĐ V1 thuộc AuthorizedSigner (DangTrinhKy→DangDongDau) và HrAdmin (DangDongDau→DaPhatHanh) — ContractFeatures.cs:426-427 (PhaseActorRoles) — cả hai đều ngoài grant-list.
Giảm nhẹ (đo được): workflow V2 seed sẵn QT-HD-V2-001 chỉ có 1 approver = binh.le@solutions.com.vn (Lê Văn Bình, CCM) — DbInitializer.cs:230-233, :256-262. CCM ↔ role CostControl CÓ trong grant-list ⇒ đường mặc định sống. Rủi ro chỉ bật khi dry-run tạo workflow mới qua Designer (W5 pin V2 cho phép chọn).
Cần: spec khai luật "grant phải phủ role của mọi user được gán làm Level trong workflow được pin", và acceptance thêm 1 bước đo: liệt approver của workflow sẽ dùng → đối chiếu role vs grant-list TRƯỚC deploy.
F-S6 [MED] W8 map "POST → Contracts.Create" nuốt cả comment / attachment / details ⇒ chặn nhầm người có Update mà không có Create; và test tính-chất KHÔNG bắt được map sai
Spec: spec-wave-w8:18 — "POST → Contracts.Create · PUT/PATCH → Contracts.Update · DELETE → Contracts.Delete · POST {id}/transitions → Contracts.Update. Neo TÍNH-CHẤT: MỌI action HttpPost/Put/Delete có policy — KHÔNG neo con số 22."
Đĩa: trong 11 POST có {id}/comments (ContractsController.cs:68), {id}/attachments (:85), và 7 {id}/details/* (:123,130,137,144,151,158,165). Về ngữ nghĩa đây là sửa HĐ đang có, không phải tạo HĐ. Map thành Contracts.Create ⇒ role được cấp Update-only (ví dụ approver chỉ cần góp ý) không comment được, không upload được.
Chỗ chết của acceptance: spec-wave-w8:28 test MoiEndpointGhi_CoAuthorizePolicy chỉ khẳng định "có [Authorize(Policy=...)]" — map SAI vẫn PASS. Test tính-chất đúng hướng (tránh Goodhart số 22) nhưng mù về nội dung map.
Cần: spec chốt bảng map per-endpoint (ít nhất tách comments/attachments/details sang Contracts.Update), và acceptance thêm 1 assert đối chiếu map mong đợi cho ≥3 endpoint đại diện.
F-S7 [MED] DeleteContractCommandHandler KHÔNG có check chủ sở hữu — plan §4 chỉ khai chiều "khó xoá", bỏ chiều "ai cũng xoá được"
Đĩa ContractFeatures.cs:625-639: handler nhận (IApplicationDbContext db) — 0 ICurrentUser, 0 check DrafterUserId, 0 check role. Chỉ có guard phase :632.
Plan: plan-cha:55 (§4 mục 4) chỉ nói "HĐ ≥ phase 5 KHÔNG xóa được qua API" — khai đúng nhưng chỉ chiều bất tiện rollback; chiều nguy hiểm (bất kỳ ai có quyền Delete xoá HĐ nháp của người khác) không được khai. Hiện trạng: 13/13 role gọi được (class-trần :13). Sau O-B: mọi user role Drafter.
Cần: khai vào §4 (hoặc W8 §C) + cân nhắc ràng "chỉ Drafter của HĐ hoặc Admin".
F-S8 [MED] W8 acceptance nói verify grant bằng sqlcmd RDP — MÂU THUẪN plan cha §7 ("sqlcmd chỉ để local/Dev")
spec-wave-w8:32: "grant seeder ăn (đếm Permission rows key Contracts per-role qua log seeder/sqlcmd RDP)".
plan-cha:84 (§7): "data prod verify bằng curl API (SSH-SQL client chết S134/S148; sqlcmd chỉ để local/Dev)".
⇒ Acceptance của W8 chỉ định một phương tiện mà plan cha vừa tuyên là không dùng được ở prod. Người thi công sẽ hoặc bỏ qua bước này, hoặc tự chế cách khác ⇒ nghiệm thu trôi.
Ghi chú kỹ thuật: "đếm row" cũng sai trục (xem F-S1) — phải đo cờ CanCreate/CanUpdate, và có đường curl hợp lệ: GET /api/permissions (role-matrix) hoặc GET /api/menus/me bằng chính user dry-run.
F-S9 [MED] W6 nới guard trình: trộn 1 vế PHẠM-VI-HẸP với 1 vế PHẠM-VI-RỘNG trong cùng một dòng, và tên field không khớp đĩa
spec-wave-w6:19: nới thành Drafter ∨ DeptManager ∨ CreatedBy==actor ∨ Procurement.
CreatedBy==actor= hẹp, đóng đúng triệu chứng "PMH tự trình phiếu của mình".Procurement= rộng theo role: mọi user Cung ứng trình được HĐ nháp của người khác. Không có ca ÂM nào trongspec-wave-w6:26-31canh vế này.- Tên field: đĩa dùng
Contract.DrafterUserIdcho mọi guard chủ-sở-hữu (ContractFeatures.cs:302,:485);CreatedBylà field củaBaseEntity(audit). Spec ghiCreatedBy⇒ implementer có thể so nhầm field, lệch với guard IDOR đang chạy.
Cần: tách 2 vế, nêu vế nào là bắt buộc; sửa tên field về DrafterUserId (hoặc khai rõ vì sao dùng CreatedBy); thêm ca ÂM nếu giữ vế Procurement.
F-S10 [MED-LOW] FormsController class-trần, 0 policy per-action — hố chưa khai (đúng giả thuyết lane)
src/Backend/SolutionErp.Api/Controllers/FormsController.cs:11 = [Authorize]; grep Authorize trong file trả đúng 1 dòng ⇒ không có [Authorize(Policy=...)] nào. MenuKeys.Forms ∈ All (MenuKeys.cs:168) nên policy Forms.* có sẵn — tức đây là lỗ có sẵn thuốc mà chưa uống. Ngoài phạm vi W8 (W8 chỉ ContractsController), và không xuất hiện trong plan-cha §4. Dry-run có đụng export/template HĐ ⇒ nên ít nhất khai.
F-S11 [LOW] plan §4 mục 4: so sánh SỐ chặn nhiều phase hơn plan mô tả
ContractFeatures.cs:632 entity.Phase >= ContractPhase.DangInKy với enum (ContractPhase.cs:17-27): DangInKy=5, DaPhatHanh=9, ChoDuyet=10, TraLai=98, TuChoi=99.
⇒ HĐ đang ChoDuyet, bị Trả lại, hoặc bị Từ chối đều KHÔNG xoá được. plan-cha:55 chỉ nói tới "HĐ test DaPhatHanh nằm lại". Dry-run chắc chắn sinh HĐ TraLai/TuChoi (3 trạm) ⇒ rác nhiều hơn plan liệu trước; đường thoát vẫn là admin-override :191-198 nhưng phải khai.
F-S12 [LOW] Q11 "2 đường song song" = cổng KHKK bị né ĐƯỢC, và dry-run KHÔNG chứng minh được cổng giữ
plan-cha:65 (§5 dòng 3): Q11 default "cảnh báo mềm — 2 đường song song (PE→HĐ tắt vẫn sống)".
Đây là quyết định nghiệp vụ hợp lệ và đảo được, nhưng hệ quả kiểm-soát chưa khai: trong suốt dry-run, POST /api/contracts (ContractsController.cs:46) tạo HĐ không cần phiếu KHKK đã duyệt ⇒ giá chốt của W3 finalize có thể bị bỏ qua. Nghĩa là vòng dry-run không sinh được bằng chứng "cổng 3 trạm giữ" — nó chỉ chứng minh đường thuận chạy. Nên khai ở §4 (data-risk) hoặc ghi vào acceptance E2E rằng ca ÂM "tạo HĐ không có KHKK" là ca cố ý để mở.
F-S13 [PASS + INFO] Mật khẩu trong artifact
- Trong phạm vi lane:
grep -riE "Admin@1234|TestUser@|password|mật khẩu|matkhau"trên toàn bộruns/2026-07-29-S160-khkk-dryrun-plan/(12 file) vàruns/2026-07-28-S157-ke-hoach-ky-ket-hd/→ 0 hit. ✅ Plan cha + 7 spec-wave + 2 sub-investigator + run.md KHÔNG lộ mật khẩu. - INFO ngoài phạm vi (không phải lỗi của bộ spec này):
git grep -lI "Admin@123456"= 25 file tracked, gồmREADME.md,docs/STATUS.md,docs/CLAUDE.md,.claude/agents/reviewer.md,.claude/agents/cicd-monitor.md,.claude/skills/iis-deploy-runbook/SKILL.md, 5 file.claude/agent-memory/*/MEMORY.md. Mật khẩu admin prod nằm trong repo tracked từ lâu — quyết định của owner, chỉ nêu để không bị "vắng mặt trông giống sạch".
F-S14 [PASS dương] Những chỗ chịu được soi
- "22 endpoint ghi" — đếm tay khớp 100% (11 POST + 8 PUT + 3 DELETE).
- W8 §① nói đúng vai của từng option: O-B thu hẹp ai bấm, O-C mới đóng ca Reject-sau-terminal (
:7,:12). Không có claim "O-B đóng 2 ô đỏ" ⇒ không phải lỗi. Cảnh báo còn lại: sau O-B, 7 role được grant vẫn Reject/TraLai được HĐ ở mọi phase kể cảDaPhatHanh(ContractWorkflowService.cs:49-66) — spec:12có nói, plan:73có nói "O-C cần anh gật riêng" ⇒ default KHÔNG lấn quyền owner ở điểm này. - Rủi ro "grant mới lật row false cố ý S92" (
spec-wave-w8:33) — đo đĩa: revokerRevokeTemporarilyHiddenModulesAsync(DbInitializer.cs:2278, gọi ở:2096) nay chỉ cònHrm*/Off*/Personal(:2291-2303, nhánh S92 cho Contracts/Master đã GỠ @S159). ⇒ grantContractskhông đụng vùng revoker; control-ÂM mà spec đề (:32) là đúng thứ cần đo. Không phải lỗ. - W6 rủi ro "inbox V2 quên lọc IsDeleted" (
:32) —db.Contracts.AsNoTracking()ở inbox (ContractFeatures.cs:447) vẫn ăn global query filter; chỉ vỡ nếu ai đó thêmIgnoreQueryFilters⇒ giữ dòng cảnh báo là hợp lý, không thừa.
F-S15 [LOW] Nhãn đo trong plan-cha §3 — "curl admin 6 phase" dưới-phủ enum 12 giá trị
plan-cha:43: "Prod: Contracts = 0 active + 0 deleted (curl admin 6 phase + tổng + /deleted)". ContractPhase có 12 giá trị (ContractPhase.cs:17-27: 2,3,4,5,6,7,8,9,10,98,99 + DangChon). Phép "6 phase" không phủ enum; vật chịu lực thật là "tổng" (list không filter, admin bypass ContractFeatures.cs:298 ⇒ trả mọi phase) và /deleted. Kết luận "0 active" vẫn ĐÚNG nhờ "tổng", nhưng câu chữ khiến người sau tưởng phép đo là enumeration-per-phase. Nên viết lại thành "tổng không-filter (phủ mọi phase) + /deleted; 6 phase chỉ là cross-check".
3. Trả lời trực tiếp 4 câu của lane
- W6 có làm hở HĐ cho user ngoài workflow không? — CÓ RỦI RO CAO như spec đang viết (F-S3): hàm bị sửa là role-only, 2/3 call-site không nằm trong gate, và phương án an toàn để trong ngoặc. Ca ÂM trong spec chưa đủ (F-S4: 0 test ÂM cho view; ca ÂM prod không pin role). PE data thật lộ thêm gì? — W6 tự nó không chạm PE. Đường lộ gián tiếp là qua attachment (F-S2): hồ sơ NCC/scan đính vào HĐ sinh từ KHKK tải được bởi mọi user đăng nhập, không cần thuộc workflow.
- O-B đóng đúng 2 ô đỏ không? — Không, và spec KHÔNG nói sai (F-S14):
spec-wave-w8:7,:12nói rõ O-B chỉ thu hẹp, O-C mới đóng Reject; plan:73/:90giữ O-C cho anh gật. Seeder-grant có lật row false S92 không? — Không (revoker sau S159 chỉ còn Hrm/Off/Personal). Nhưng seeder-grant có lỗi ngược lại nguy hiểm hơn: no-op im lặng (F-S1). "Policy chặn theo menu-permission, user được cấp vẫn đụng HĐ THẬT" — spec khai đủ chưa? —spec-wave-w8:11CÓ khai ("⚠️ thu hẹp AI, không thu hẹp BẤM VÀO ĐÂU"). Nhưng thiếu hệ quả cụ thể: 7 role được cấp vẫn Reject/xoá/xoá-attachment HĐ của người khác (F-S2/F-S7). - plan-cha §4 còn hố nào chưa khai? — 4 hố: attachment upload/download/delete 0-authz 0-phase-guard (F-S2) · delete-HĐ 0 owner-check (F-S7) ·
FormsControllerclass-trần (F-S10) · phase-guard chặn cả TraLai/TuChoi/ChoDuyet chứ không chỉ DaPhatHanh (F-S11). Cộng 1 hố "mềm": Q11 2-đường-song-song khiến dry-run không chứng minh được cổng (F-S12). - Mật khẩu trong artifact? — 0 hit trong toàn bộ plan + 7 spec + sub-file của cả 2 run-folder ✅. Ghi nhận INFO:
Admin@123456tồn tại ở 25 file tracked khác của repo (F-S13).