---
name: permission-grant-two-layers
description: cấp/verify quyền = 2 tầng ĐỘC-LẬP (menu-display ⟂ API-authz) — verify tầng enforcement KHÔNG chỉ display
metadata: 
  node_type: memory
  type: feedback
  originSessionId: 23185661-d4a9-428f-98e1-0e79e92a14fe
---

RBAC thường có 2 tầng ĐỘC-LẬP: **(1) display-layer** (menu/nút hiện theo permission-flag `CanRead`) và **(2) enforcement-layer** (API/controller thực-thi quyền qua `[Authorize(Policy=)]`/`[Authorize(Roles=)]`). Grant permission-flag CHỈ đổi tầng display — hiệu-lực API THẬT ở tầng enforcement. Nhiều endpoint gate GET bằng `[Authorize]` trần (mở mọi authed-user) + gate write bằng role-attribute KHÔNG theo menu-key → grant "R/C/U" phần lớn = FE-button-visibility, delta API ≪ mong đợi. Ngược lại: menu bị ẩn KHÔNG có nghĩa API đóng (gõ URL/gọi API trực tiếp vẫn được).

**Why:** S118 SOLUTION_ERP — grant `Suppliers` R+C+U cho role Procurement tưởng "làm được mọi thứ"; THỰC TẾ `PUT/DELETE /suppliers` gate `[Authorize(Roles="Admin,CatalogManager")]` → PRO vẫn 403 sửa/xóa (chỉ Publish/Import theo `Suppliers.Update` là ăn). Đồng thời phát hiện `ReportsController` `[Authorize]` trần → mọi authed-user đọc tài-chính-HĐ toàn-công-ty dù menu đã bị S92 ẩn. Reviewer (adversarial) bắt vì **grep authz-attribute controller đích**; em-main-solo tin "menu-flag = quyền" thì SÓT (→ suýt báo "done" khi nút Sửa hiện nhưng bấm lỗi 403). Bài học generalize cross-project (BVAAU/NamGroup cũng RBAC 2-tầng). Gotcha #82 = bản project-cụ-thể.

**How to apply:** TRƯỚC khi tin hiệu-lực 1 grant permission → **grep authz-attribute của CHÍNH controller/endpoint đích** (`[Authorize(Policy=X.Y)]` vs `[Authorize(Roles=)]` vs `[Authorize]` trần) → verify tầng ENFORCEMENT khớp ý-định, KHÔNG chỉ display. Permission/security-change = reviewer-adversarial BẮT BUỘC ([[feedback_high_to_max_multiagent_quality]]). Endpoint lộ data nhạy mà `[Authorize]` trần = lỗ hổng (menu-ẩn ≠ API-đóng) → gate policy per-method. Liên gotcha #82 + #44 (silent 403).
