📌 Mục lục

  1. Vì sao phải học bảo mật?
  2. RBAC là gì? — Câu chuyện tòa nhà văn phòng
  3. RLS là gì? — Câu chuyện tủ hồ sơ cá nhân
  4. RBAC vs RLS — Khi nào dùng cái nào?
  5. Firebase Auth — Cổng vào tòa nhà
  6. Firestore Security Rules — Bảo vệ tủ hồ sơ
  7. Áp dụng vào ProjectOS — Ai làm được gì?
  8. Thư viện Prompt mẫu cho Claude Code
  9. Thực hành: 5 bài tập có lời giải prompt
  10. Checklist kiểm tra sau khi Claude Code viết xong

1. Vì sao phải học bảo mật?

Hãy tưởng tượng bạn vừa thuê Claude Code xây xong ứng dụng ProjectOS. App chạy ngon, đẹp, ai cũng vào được. Nhưng có vấn đề:
  • 👤 Một nhân viên thử vọc → vô tình xoá toàn bộ task của Giám đốc.
  • 🕵️ Một thực tập sinh tò mò → đọc được bảng lương của cả công ty.
  • 😈 Một cựu nhân viên (vẫn còn tài khoản) → sửa ngân sách dự án từ ở nhà.
→ Tất cả đều xảy ra vì ứng dụng không có lớp bảo vệ.
Quy tắc vàng: Trong Firebase, không có server backend riêng. Trình duyệt của người dùng nói chuyện thẳng với Firestore. Nếu không có luật bảo mật, bất kỳ ai biết một chút kỹ thuật đều có thể đọc/sửa/xoá mọi thứ.
Có 2 lớp bảo vệ chính ta cần dựng lên:
LớpTên gọiTrả lời câu hỏi
🏢 Vĩ môRBAC (Role-Based Access Control)Vai trò của bạn là gì? Vai trò đó được làm gì?”
📁 Vi môRLS (Row-Level Security)“Bạn có phải là chủ của dòng dữ liệu này không?“

2. RBAC là gì?

🏢 Câu chuyện toà nhà văn phòng

Hãy hình dung công ty SOFTECH có một toà nhà 10 tầng:
  • Tầng 1 — Sảnh: Ai cũng vào được.
  • Tầng 2-5 — Văn phòng nhân viên: Cần thẻ nhân viên.
  • Tầng 6-8 — Phòng họp & tài liệu mật: Cần thẻ trưởng phòng trở lên.
  • Tầng 9 — Phòng tài chính: Chỉ kế toán + giám đốc.
  • Tầng 10 — Phòng giám đốc: Chỉ giám đốc.
Bảo vệ ở sảnh không nhớ mặt từng người — họ chỉ nhìn màu thẻ:
Thẻ (Role)Vào được tầng nào
🟢 Khách1
🔵 Nhân viên (member)1, 2-5
🟡 Trưởng phòng (manager)1, 2-8
🟠 Kế toán (finance)1, 2-5, 9
🔴 Giám đốc (admin)Tất cả
Đó chính là RBAC. Mỗi người được gán một vai trò (role), và mỗi vai trò có một danh sách quyền cố định.

🎯 RBAC trong app

Áp dụng vào ProjectOS:
RoleMô tảĐược làm
viewerKhách xemChỉ đọc dashboard, không sửa được gì
memberNhân viênTạo/sửa task của mình, comment, log time
managerTrưởng dự ánMọi thứ của member + duyệt sprint, đóng bug
financeKế toánXem/sửa Budget, Reports tài chính
adminQuản trịToàn quyền — bao gồm phân quyền cho người khác

🗝️ Role được lưu ở đâu?

Trong Firebase, role thường được lưu ở 2 nơi:
  1. Custom Claims (gắn vào tài khoản Auth) — Cách “xịn”, không sửa được từ trình duyệt.
  2. Document users/{userId} trong Firestore — Dễ làm, nhưng phải viết luật cẩn thận để người dùng không tự sửa role của mình.
💬 Khi prompt Claude Code, bạn chỉ cần nói: “Lưu role ở custom claims” hoặc “Lưu role trong collection users” — Claude sẽ tự chọn cách triển khai.

3. RLS là gì?

📁 Câu chuyện tủ hồ sơ cá nhân

Quay lại toà nhà SOFTECH. Bạn là nhân viên (member), bạn đã lên được tầng 3. Trong phòng có 100 tủ hồ sơ — mỗi tủ là một task của một người khác nhau.
  • Bạn chỉ được mở tủ của chính bạn (task assignee = bạn).
  • Bạn không được mở tủ của anh A ngồi cạnh — dù bạn đã vào được tầng.
Đó là RLS (Row-Level Security): bảo vệ ở mức từng dòng dữ liệu, không phải mức “có vào được hay không”.

🔍 RLS trả lời câu hỏi gì?

Câu hỏiLoại
”Bạn có phải là chủ task này không?”RLS
”Bạn có ở trong team được giao task này không?”RLS
”Document này có createdBy == bạn không?”RLS
”Bug này thuộc dự án bạn có quyền không?”RLS

💡 Sự khác biệt mấu chốt

RBACRLS
”Vai trò của bạn được làm gì?""Bạn có quyền với dòng dữ liệu cụ thể này không?”
Đúng/sai cho toàn bộ collectionĐúng/sai cho từng document
Kiểm tra: role == 'manager'Kiểm tra: resource.data.assignee == userId

4. RBAC vs RLS

Trong thực tế, luôn dùng cả hai cùng lúc. Đây là cách kết hợp: Khi Tùng gọi Firestore, Firebase tự đọc role: manager từ vé.

💬 Prompt mẫu để Claude Code thiết lập custom claims:

Tôi cần một Firebase Cloud Function để admin có thể gán role (viewer, member, manager, finance, admin) cho user khác qua custom claims. Yêu cầu:
  • Chỉ user có role == admin mới gọi được function này.
  • Function nhận targetUserIdnewRole.
  • Validate newRole phải nằm trong danh sách cho phép.
  • Cập nhật cả custom claim VÀ document users/{targetUserId}.role để FE đọc nhanh.
  • Viết theo Cloud Functions v2 (onCall), TypeScript, có log audit.

6. Firestore Security Rules

Đây là “bộ luật” mà Firebase đọc mỗi khi có ai cố đọc/ghi dữ liệu. Nó được viết bằng một ngôn ngữ riêng (gần giống JavaScript) và lưu trong file firestore.rules.

📜 Cấu trúc cơ bản

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Utility functions
    function isAuthed()  { return request.auth != null; }
    function userRole()  { return request.auth.token.role; }
    function isAdmin()   { return isAuthed() && userRole() == 'admin'; }
    function isManager() { return isAuthed() && userRole() in ['manager', 'admin']; }

    // Rules for each collection
    match /projects/{projectId}/tasks/{taskId} {
      // RBAC: any authenticated user can read
      allow read: if isAuthed();

      // RBAC + RLS: member can create only their own task
      allow create: if isAuthed() && request.resource.data.assignee == request.auth.uid;

      // RBAC + RLS: only owner or manager can update
      allow update: if isAuthed() && (resource.data.assignee == request.auth.uid || isManager());

      // RBAC: only admin can delete
      allow delete: if isAdmin();
    }
  }
}

🔍 Bóc tách từng dòng

Cú phápNghĩa đời thực
request.auth != null”Người này đã đăng nhập chưa?”
request.auth.uid”ID của người đang gửi yêu cầu”
request.auth.token.role”Role gắn trong vé điện tử”
resource.data.X”Giá trị X hiện tại trong document” (dùng cho update/delete)
request.resource.data.X”Giá trị X mà người dùng đang cố ghi vào” (dùng cho create/update)
allow read, write”Cho phép đọc/ghi nếu điều kiện sau là true”

⚠️ 3 cái bẫy hay gặp

  1. request.resource vs resource — Lẫn lộn 2 cái này = mở toang cửa.
    • resource = dữ liệu đã có trong DB.
    • request.resource = dữ liệu người dùng muốn ghi vào.
  2. Quên kiểm tra trường nhạy cảm khi update. Ví dụ: Cho member sửa task — nhưng không cấm họ tự đổi role thành admin.
  3. Cho client tự ghi role. Nếu role lưu trong Firestore và bạn cho user update document users/{me}, họ có thể tự thăng chức! → Luôn dùng request.resource.data.role == resource.data.role để cấm thay đổi.

7. Áp dụng vào ProjectOS

Đây là ma trận quyền đầy đủ cho ProjectOS. Đưa bảng này cho Claude Code, nó sẽ viết được rules.

📊 Bảng quyền

ModuleCollectionviewermembermanagerfinanceadmin
Dashboardconfig/dashboard👁️👁️👁️👁️👁️✏️
Tasksprojects/{id}/tasks👁️👁️✏️(của mình)👁️✏️🗑️👁️👁️✏️🗑️
Sprintprojects/{id}/sprints👁️👁️👁️✏️👁️👁️✏️🗑️
Backlogprojects/{id}/backlog👁️👁️✏️👁️✏️🗑️👁️👁️✏️🗑️
Bugsprojects/{id}/bugs👁️👁️✏️(của mình)👁️✏️🗑️👁️👁️✏️🗑️
Teamprojects/{id}/team👁️👁️👁️👁️👁️✏️🗑️
Budgetprojects/{id}/budget👁️👁️✏️👁️✏️🗑️
Riskprojects/{id}/risks👁️👁️👁️✏️🗑️👁️👁️✏️🗑️
Docsprojects/{id}/docs👁️👁️✏️(của mình)👁️✏️🗑️👁️👁️✏️🗑️
Meetingsprojects/{id}/meetings👁️👁️👁️✏️🗑️👁️👁️✏️🗑️
Reportsconfig/reports👁️👁️👁️👁️✏️👁️✏️
Activityprojects/{id}/activity👁️👁️👁️👁️👁️ (chỉ system ghi)
Comments.../{parent}/comments👁️👁️✏️(của mình)👁️✏️🗑️👁️👁️✏️🗑️
Usersusers/{uid}👁️(self)👁️(self)✏️(profile self)👁️👁️👁️✏️🗑️
Ghi chú ký hiệu: 👁️ đọc | ✏️ ghi/sửa | 🗑️ xoá | ❌ chặn hoàn toàn

🔑 Quy tắc đặc biệt

  • role trong users/{uid} — không user nào tự sửa được, kể cả chính họ. Chỉ admin qua Cloud Function.
  • Budget — bị chặn hoàn toàn với viewer/member (kể cả đọc).
  • Activity log — chỉ Cloud Function được ghi, FE chỉ đọc.

8. Thư viện Prompt mẫu

Đây là 8 prompt sẵn dùng — copy-paste vào Claude Code khi bạn cần.

📝 Prompt 1 — Thiết lập role system ban đầu

Tôi cần thiết lập hệ thống role cho ProjectOS với 5 vai trò:
viewer, member, manager, finance, admin.

Yêu cầu:
1. Lưu role dưới dạng Firebase Custom Claims (set qua Cloud Function).
2. Đồng thời mirror role sang field `role` trong document `users/{uid}`
   để FE đọc nhanh mà không cần refresh token.
3. Viết hook `useUserRole()` trả về role hiện tại + helper isAdmin(),
   isManager(), canEdit() — đặt trong src/hooks/useUserRole.ts.
4. Cập nhật AuthContext để load role ngay sau login.

Làm theo feature-playbook.md, dùng TypeScript, không dùng `any`.

📝 Prompt 2 — Viết Firestore Rules tổng thể

Đọc file .claude/docs/firebase.md và bảng phân quyền sau:

[PASTE BẢNG MA TRẬN QUYỀN TỪ MỤC 7]

Hãy viết file firestore.rules HOÀN CHỈNH cho ProjectOS với:
- Helper functions ở đầu: isAuthed(), userRole(), isAdmin(), isManager(),
  isFinance(), hasMinRole(role).
- Match từng collection theo schema thực tế trong .claude/docs/firebase.md.
- Áp dụng cả RBAC và RLS như bảng yêu cầu.
- Cấm tuyệt đối client tự sửa field `role` trong users/{uid}.
- Cấm tuyệt đối FE ghi vào activity logs (chỉ Cloud Function ghi).
- Comment tiếng Việt giải thích từng rule cho người non-IT đọc lại.

Sau khi viết xong, list ra các index Firestore cần thêm.

📝 Prompt 3 — Cloud Function gán role

Viết Cloud Function v2 (TypeScript, onCall) tên `setUserRole`:

- Chỉ gọi được bởi user có custom claim role == 'admin'.
- Input: { targetUserId: string, newRole: 'viewer'|'member'|'manager'|'finance'|'admin' }.
- Validate input bằng Zod.
- Set custom claim cho targetUserId.
- Update users/{targetUserId}.role và updatedAt.
- Ghi log vào collection `auditLogs/` với { actorId, targetUserId, oldRole, newRole, timestamp }.
- Trả về { success: true, newRole }.

Lưu vào functions/src/admin/setUserRole.ts. Export trong functions/src/index.ts.

📝 Prompt 4 — Test rules bằng Emulator

Viết bộ test cho firestore.rules dùng @firebase/rules-unit-testing.

Test các kịch bản:
1. viewer cố đọc /projects/default/budget → DENY
2. member tạo task với assignee = chính mình → ALLOW
3. member tạo task với assignee = người khác → DENY
4. member sửa task của người khác → DENY
5. manager xoá task → ALLOW
6. member cố update users/{me}.role thành 'admin' → DENY
7. finance đọc budget → ALLOW
8. user chưa đăng nhập → DENY mọi thứ

Đặt file ở firestore-tests/rules.test.ts. Hướng dẫn tôi cách chạy:
  npm install --save-dev @firebase/rules-unit-testing
  firebase emulators:exec --only firestore "npm test"

📝 Prompt 5 — UI: Ẩn nút theo role

Trong src/hooks/useUserRole.ts đã có sẵn useUserRole(), isAdmin(), isManager().

Hãy refactor các component sau để ẩn/disable nút theo role:
- src/modules/budget/components/BudgetTable.tsx → ẩn nút "Add Expense"
  nếu role không phải finance/admin.
- src/modules/tasks/components/TaskCard.tsx → ẩn icon thùng rác xoá task
  nếu user không phải owner và không phải manager+.
- src/modules/team/components/MemberRow.tsx → ẩn dropdown "Change Role"
  nếu user không phải admin.

Dùng pattern: const { isAdmin, isManager } = useUserRole();
Dùng `<Show when={...}>` nếu đã có, không thì conditional render thường.
KHÔNG hardcode role string trong UI — chỉ dùng helpers từ hook.

📝 Prompt 6 — Phát hiện rò rỉ quyền

Quét toàn bộ src/modules/ và src/app/(dashboard)/ để tìm:

1. Các nút/link cho phép sửa/xoá nhưng KHÔNG kiểm tra role trước.
2. Các hook gọi mutation (create/update/delete) mà UI không có guard.
3. Các page hiển thị data nhạy cảm (budget, salary, role) mà không
   có check role ở component cha.

Output dạng bảng Markdown:
| File | Dòng | Vấn đề | Đề xuất fix |

Không tự sửa code — chỉ liệt kê để tôi review trước.

📝 Prompt 7 — Audit log

Tôi cần audit log cho ProjectOS. Mỗi khi có hành động quan trọng,
ghi log vào collection `auditLogs/`.

Yêu cầu:
1. Cloud Function trigger `onDocumentWritten` cho:
   - users/{uid} (đổi role)
   - projects/{id}/budget/{expenseId} (mọi thay đổi)
   - projects/{id}/team/{memberId} (thêm/xoá thành viên)

2. Log entry: { actorId, action, collection, docId, before, after, timestamp }
3. Rules: ai cũng KHÔNG đọc được auditLogs từ client, chỉ admin qua
   Cloud Function `getAuditLogs(filters)`.

Đặt vào functions/src/audit/. Update firestore.rules để chặn read/write
auditLogs từ client.

📝 Prompt 8 — Migrate role cũ

Hiện tại tất cả user trong users/{uid} đều có role = 'member' (mặc định).
Tôi cần migrate:

1. User có email kết thúc bằng @softech.vn/finance.* → role 'finance'.
2. User trong document config/admins.uids → role 'admin'.
3. Còn lại giữ nguyên 'member'.

Viết script một lần (one-off) chạy bằng Node.js với firebase-admin SDK,
đặt ở scripts/migrate-roles.ts. Script phải:
- Dry-run trước (in ra ai sẽ đổi role gì) — không ghi DB.
- Hỏi xác nhận y/n trước khi chạy thật.
- Set cả custom claim VÀ field users/{uid}.role.
- In kết quả tổng kết cuối cùng.

Hướng dẫn tôi cách lấy service account key và chạy script.

9. Thực hành

Đây là 5 bài tập có lời giải prompt. Học viên đọc đề → tự nghĩ cách diễn đạt → so sánh với prompt mẫu.

🎯 Bài 1 — Cấm intern xem lương

Đề: Trong ProjectOS có module Team, mỗi member có field salary. Bạn muốn:
  • Member chỉ xem được lương của chính mình.
  • Manager xem được lương của mọi người trong team.
  • Finance/Admin xem được tất cả.
Prompt gợi ý:
Trong collection projects/{projectId}/team/{memberId} có field `salary`.

Hãy sửa firestore.rules để:
- Mọi user authed đọc được các field cơ bản (name, email, role, avatar).
- Field `salary` chỉ trả về nếu: member.uid == request.auth.uid
  HOẶC userRole() in ['manager', 'finance', 'admin'].

Vì Firestore rules không hỗ trợ field-level read, hãy:
1. Tách salary ra subcollection projects/{pid}/team/{mid}/private/compensation
2. Áp rule cho subcollection đó.
3. Update useTeam() hook để fetch compensation conditionally dựa trên role.

🎯 Bài 2 — Một user vừa là member dự án A, vừa là manager dự án B

Đề: ProjectOS hiện đang single-project. Sau này khi mở rộng multi-project, một user có thể có role khác nhau ở từng dự án. Prompt gợi ý:
Hiện tại role lưu ở custom claims (1 role cho cả app).
Cần refactor để mỗi user có role KHÁC NHAU theo từng project.

Đề xuất schema mới:
- projects/{projectId}/members/{userId} = { role: '...', joinedAt: ... }
- Custom claim chỉ giữ `globalRole` (cho super-admin app-wide).

Yêu cầu Claude:
1. Đề xuất 2-3 phương án (kèm tradeoff về read latency, security, complexity).
2. Cho tôi chọn rồi mới implement.
3. Sau khi chọn, viết:
   - Schema mới
   - firestore.rules sử dụng `get(/databases/.../projects/{pid}/members/{uid})`
   - Hook useProjectRole(projectId)
   - Migration script từ schema cũ sang mới.

🎯 Bài 3 — Cho phép guest xem dashboard public

Đề: Bạn muốn có một trang /public/{projectId} mà ai (kể cả chưa login) cũng xem được — nhưng chỉ thấy số liệu tổng quan, không thấy task chi tiết. Prompt gợi ý:
Tôi cần một public dashboard không cần đăng nhập.

Yêu cầu:
1. Tạo document riêng config/public/{projectId} chứa snapshot
   các số liệu công khai (totalTasks, completedRatio, sprintProgress).
2. Cloud Function chạy mỗi 1h để tổng hợp từ data thực và ghi vào doc public.
3. firestore.rules: cho phép `read` document config/public/* mà không cần auth.
4. Mọi collection khác vẫn bắt buộc auth + role như cũ.
5. Tạo route src/app/public/[projectId]/page.tsx hiển thị data này.

KHÔNG được expose bất kỳ data cá nhân hay tài chính nào ra public.

🎯 Bài 4 — Xoá task chỉ trong 5 phút sau khi tạo

Đề: Để chống “lỡ tay”, chỉ cho phép xoá task trong vòng 5 phút sau khi tạo, và chỉ bởi người tạo. Sau 5 phút, chỉ manager mới xoá được. Prompt gợi ý:
Sửa rule delete cho projects/{pid}/tasks/{taskId}:

allow delete: if isAuthed() && (
  // Case 1: Người tạo, trong 5 phút đầu
  (resource.data.createdBy == request.auth.uid
    && request.time < resource.data.createdAt + duration.value(5, 'm'))
  ||
  // Case 2: Manager+ luôn được xoá
  isManager()
);

Đồng thời:
- Trong TaskCard, nếu user là creator và còn trong 5 phút → hiện nút "Undo" countdown.
- Hết 5 phút thì ẩn nút đi.
- Dùng dayjs để tính countdown realtime.
- Manager+ luôn thấy nút thùng rác.

🎯 Bài 5 — Khoá tài khoản tạm thời

Đề: Khi một nhân viên bị nghỉ phép kỷ luật, admin muốn vô hiệu hoá tài khoản mà không xoá data. Prompt gợi ý:
Thêm field `disabled: boolean` và `disabledReason?: string` vào users/{uid}.

1. Cloud Function `setUserDisabled({ uid, disabled, reason })`:
   - Chỉ admin gọi được.
   - Set Firebase Auth: admin.auth().updateUser(uid, { disabled }).
   - Update users/{uid}.disabled + audit log.

2. firestore.rules: thêm hàm isActive() = get(users/$(uid)).data.disabled != true.
   Áp dụng isActive() vào MỌI rule write (không cho user bị khoá ghi gì cả).

3. AuthContext: nếu phát hiện disabled == true, force logout + show toast.

4. Admin UI: thêm nút "Disable" + textarea reason trong team page.

10. Checklist kiểm tra

Sau khi Claude Code viết xong, luôn chạy checklist này trước khi deploy:

✅ Checklist bảo mật Firestore

  • Default deny: File firestore.rules có dòng allow read, write: if false; ở cuối không?
  • Không có allow read, write: if true ở bất kỳ đâu (trừ khi có lý do rất rõ).
  • Trường role trong users/{uid} không thể bị client tự sửa (test bằng emulator).
  • Custom claims được set qua Cloud Function, không qua FE.
  • Mọi collection đều có rule explicit (không rơi vào “không có rule = mở toang”).
  • Phân biệt resource vs request.resource trong update rules.
  • Index đã tạo cho mọi query có where + orderBy (chạy app, đọc warning ở console).
  • Test với emulator cho ít nhất 5 kịch bản chính (xem Prompt 4).
  • Audit log đang ghi cho hành động nhạy cảm (đổi role, sửa budget…).
  • UI guard đồng bộ với Rules — không có nút “Delete” mà rule lại từ chối (gây confused UX).

⚠️ Cảnh báo cuối: Rules không thay thế UI

Có người tưởng “tôi đã ẩn nút Delete ở UI, rule không cần lo”. SAI HOÀN TOÀN. → Bất kỳ ai cũng có thể mở DevTools, gọi Firestore SDK trực tiếp từ console, bypass UI. → Rules là tuyến phòng thủ THẬT. UI guard chỉ là UX cho người dùng tốt bụng.

🎓 Tóm tắt 1 trang

RBAC = “Bạn là ai?” → role: viewer/member/manager/finance/admin RLS = “Cái này có phải của bạn không?” → so sánh uid với field trong document Firebase Auth = cấp ID Token + Custom Claims (vé có ghi role) Firestore Rules = bộ luật ở server, đọc vé + kiểm tra điều kiện Cloud Functions = nơi duy nhất an toàn để gán/thay đổi role
Quy trình prompt Claude Code:
  1. Mô tả ai được làm với dữ liệu nào (dạng bảng càng tốt).
  2. Yêu cầu Claude đề xuất 2-3 phương án trước khi code.
  3. Sau khi code xong, yêu cầu Claude viết test bằng emulator.
  4. Chạy checklist 10 mục ở mục 10.
  5. KHÔNG BAO GIỜ deploy rules mới mà chưa test bằng emulator.

Tài liệu kèm theo trong repo ProjectOS:
  • .claude/docs/auth.md — Auth flow chi tiết
  • .claude/docs/firebase.md — Schema thực tế
  • .claude/agents/firebase-expert.md — Agent chuyên Firestore
Chúc bạn xây ProjectOS an toàn! 🔐