Mục tiêu học tập
- Phân biệt Authentication (bạn là ai) và Authorization (bạn được phép gì)
- Hiểu RBAC — roles và permissions, role hierarchy, ví dụ thực tế
- Hiểu ABAC — attributes (user, resource, environment) và policy engines
- Giới thiệu ReBAC — relationship-based (Google Zanzibar, SpiceDB)
- Nhận diện và tránh OWASP A01: Broken Access Control
1. Authentication vs Authorization
Tại sao phân biệt quan trọng?
Một số người nhầm: "Đã login thì truy cập được mọi thứ" → broken access control (OWASP A01 #1).
Ví dụ bug thực tế:
# User A xem order của mình
GET /api/orders/12345
# Server chỉ check "user authenticated?" → YES
# Không check "order 12345 có thuộc user A không?"
# → User A đổi ID, xem order của User B
GET /api/orders/99999 ← thuộc User B → vẫn trả về!
Đây là IDOR (Insecure Direct Object Reference) — vulnerability cực phổ biến.
2. RBAC — Role-Based Access Control
Khái niệm cơ bản
User được gán role → role có permissions → permission cho phép action trên resource type.
Ví dụ: blog platform
Role hierarchy
Roles có thể "thừa kế" permissions:
admin
↑ inherits
editor
↑ inherits
viewer
admin tự động có mọi permission của editor, editor có mọi permission của viewer. Đỡ duplicate.
Schema database
-- Roles
CREATE TABLE roles (
id SERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE,
parent_id INT REFERENCES roles(id) -- hierarchy
);
-- Permissions
CREATE TABLE permissions (
id SERIAL PRIMARY KEY,
name VARCHAR(100) UNIQUE -- "posts:create"
);
-- Many-to-many
CREATE TABLE role_permissions (
role_id INT REFERENCES roles(id),
permission_id INT REFERENCES permissions(id),
PRIMARY KEY (role_id, permission_id)
);
CREATE TABLE user_roles (
user_id INT REFERENCES users(id),
role_id INT REFERENCES roles(id),
PRIMARY KEY (user_id, role_id)
);
Code Node.js — middleware
// Lấy permissions của user (cached)
async function getUserPermissions(userId) {
const cached = await cache.get(`perms:${userId}`);
if (cached) return cached;
// Query roles + hierarchy + permissions
const perms = await db.query(`
WITH RECURSIVE role_tree AS (
SELECT id FROM roles WHERE id IN (
SELECT role_id FROM user_roles WHERE user_id = $1
)
UNION
SELECT r.parent_id FROM roles r
JOIN role_tree rt ON r.id = rt.id
WHERE r.parent_id IS NOT NULL
)
SELECT DISTINCT p.name FROM permissions p
JOIN role_permissions rp ON rp.permission_id = p.id
WHERE rp.role_id IN (SELECT id FROM role_tree)
`, [userId]);
const list = perms.rows.map(r => r.name);
await cache.set(`perms:${userId}`, list, 60);
return list;
}
// Middleware
function requirePermission(perm) {
return async (req, res, next) => {
const perms = await getUserPermissions(req.user.id);
if (!perms.includes(perm)) {
return res.status(403).json({ error: "Forbidden" });
}
next();
};
}
// Route
app.delete("/posts/:id",
requireAuth,
requirePermission("posts:delete"),
deletePostHandler
);
RBAC ưu / nhược
| Pros | Cons |
|---|---|
| Đơn giản, dễ hiểu, dễ implement | Không xử lý được "user A chỉ edit post của mình" |
| Tốt cho ít roles, rõ ràng | Role explosion: mỗi combination → role mới |
| Audit dễ (xem role có quyền gì) | Không context (thời gian, location, IP) |
| Hỗ trợ rộng (DB native, framework) | Static — khó change real-time |
Role explosion ví dụ: bạn có 5 phòng ban × 4 levels × 3 regions = 60 roles → quản lý nightmare.
3. ABAC — Attribute-Based Access Control
Khác RBAC ở chỗ nào?
RBAC: User → Role → Permission
ABAC: User attrs + Resource attrs + Environment → Policy → Allow/Deny
Quyết định dựa trên attributes (thuộc tính):
| Loại attribute | Ví dụ |
|---|---|
| User (Subject) | dept, level, clearance, location, age |
| Resource (Object) | owner, classification, type, dept |
| Action | read, write, delete, share |
| Environment | time, IP, device, MFA passed, request method |
Ví dụ chính sách ABAC
Policy: "Bác sĩ chỉ xem được hồ sơ bệnh nhân của khoa mình
trong giờ hành chính từ máy nội bộ"
ALLOW
IF user.role == "doctor"
AND resource.type == "patient_record"
AND user.department == resource.department
AND environment.time BETWEEN 08:00 AND 18:00
AND environment.ip IN hospital_network
Cùng user bác sĩ, cùng action read, cùng resource type, nhưng kết quả khác nhau theo context — RBAC không làm được.
Policy languages
1. XACML (eXtensible Access Control Markup Language)
- Chuẩn OASIS, XML
- Phức tạp, học thuật → ít dùng trong startup
2. AWS IAM Policy (JSON-based ABAC + RBAC hybrid)
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/${aws:username}/*",
"Condition": {
"IpAddress": { "aws:SourceIp": "10.0.0.0/8" },
"DateGreaterThan": { "aws:CurrentTime": "2025-01-01T00:00:00Z" }
}
}]
}
3. OPA / Rego (Open Policy Agent)
package authz
default allow = false
allow {
input.user.role == "doctor"
input.resource.type == "patient_record"
input.user.department == input.resource.department
time.now_ns() <= time.parse_rfc3339_ns(input.resource.expires_at)
}
OPA chạy như sidecar/service, app gửi input (user, resource, action), nhận về allow/deny. Phổ biến trong Kubernetes admission control, microservices.
4. AWS Cedar (2023)
permit (
principal in Group::"doctors",
action == Action::"viewRecord",
resource
) when {
principal.department == resource.department &&
context.time >= time("08:00:00") &&
context.time <= time("18:00:00")
};
Cedar có verifier formal (proved correct), schema-aware, syntax dễ hơn Rego.
Architecture ABAC
| Component | Vai trò |
|---|---|
| PEP | Intercept request, gọi PDP để hỏi quyết định |
| PDP | Evaluate policy, return allow/deny |
| PAP | Nơi admin viết policy |
| PIP | Provide attributes (user, resource) cho PDP |
RBAC vs ABAC
| RBAC | ABAC | |
|---|---|---|
| Quyết định dựa trên | Role | Attributes (user/resource/env) |
| Context-aware (time, IP) | Không (cần extend) | Có |
| "User edit chỉ post của mình" | Khó (cần per-resource policy) | Dễ (user.id == resource.owner_id) |
| Quản lý | Đơn giản, UI dễ | Phức tạp, cần ngôn ngữ policy |
| Audit | Dễ (list role → permission) | Khó (cần evaluate policy với data thật) |
| Khi nào dùng | Ít role, rõ ràng (CMS, basic SaaS) | Compliance phức tạp (healthcare, finance, multi-tenant SaaS) |
4. ReBAC — Relationship-Based Access Control
Khái niệm
ReBAC mô hình hóa permission như graph relationships:
Google Zanzibar (2019 paper)
Google publish paper mô tả hệ thống unified authorization phục vụ Drive, Docs, YouTube, Cloud — xử lý trillions of permission checks/day.
Mô hình tuple đơn giản:
<object>#<relation>@<subject>
doc:readme#viewer@user:alice
doc:readme#viewer@group:engineering#member
group:engineering#member@user:bob
Query:
Can user:alice view doc:readme?
→ Tìm path từ alice tới doc:readme với relation "viewer" hoặc cao hơn
→ Trả lời trong < 10ms với consistency guarantees
Implementation phổ biến
| SpiceDB | OpenFGA | Permify | |
|---|---|---|---|
| Origin | AuthZed | Auth0/Okta | Permify Inc |
| Inspired by | Zanzibar | Zanzibar | Zanzibar |
| Open source | Yes | Yes (CNCF) | Yes |
| Use case | Production, enterprise | Mainstream | Production |
Ví dụ schema (SpiceDB)
definition user {}
definition group {
relation member: user
}
definition document {
relation owner: user
relation viewer: user | group#member
relation editor: user | group#member
permission view = viewer + editor + owner
permission edit = editor + owner
permission delete = owner
}
# Write relationships
zed relationship create document:readme viewer user:alice
zed relationship create group:eng member user:bob
zed relationship create document:readme viewer group:eng#member
# Check
zed permission check document:readme view user:alice # true
zed permission check document:readme delete user:alice # false
zed permission check document:readme view user:bob # true (through group)
Khi nào ReBAC vượt trội?
Use cases hợp với ReBAC:
- File sharing (Google Drive, Dropbox, Notion)
- Collaborative docs (figma, Linear, GitHub repos)
- Multi-tenant SaaS (tenant → workspace → project → user)
- Social network (friend-of-friend, group access)
- Healthcare (patient → care team → records)
Đặc điểm chung: resource ownership và sharing là first-class concept, không thể nhồi vào "role" được.
5. Authorization Pitfalls — OWASP A01
Pitfall 1: Client-side authorization
<!-- ❌ SAI: Hide delete button cho non-admin -->
<button v-if="user.role === 'admin'" @click="deletePost">Delete</button>
<!-- API endpoint không check → attacker gọi trực tiếp: -->
<!-- curl -X DELETE /api/posts/123 -H "Authorization: ..." -->
UI chỉ là UX, không phải security. Mọi authorization check phải ở backend.
Pitfall 2: IDOR (Insecure Direct Object Reference)
// ❌ SAI
app.get("/orders/:id", requireAuth, async (req, res) => {
const order = await db.orders.findById(req.params.id);
return res.json(order); // không check ownership!
});
// ✅ ĐÚNG
app.get("/orders/:id", requireAuth, async (req, res) => {
const order = await db.orders.findById(req.params.id);
if (!order) return res.status(404).end();
if (order.user_id !== req.user.id && !req.user.isAdmin) {
return res.status(403).end(); // hoặc 404 để không leak existence
}
return res.json(order);
});
Pitfall 3: Missing function-level authz
// ❌ SAI: chỉ require auth, không require role
app.delete("/admin/users/:id", requireAuth, deleteUser);
// ✅ ĐÚNG
app.delete("/admin/users/:id",
requireAuth,
requirePermission("users:delete"),
deleteUser
);
Path bắt đầu bằng /admin/* không tự dưng "admin only" — phải explicit middleware.
Pitfall 4: Mass assignment
// ❌ SAI
app.put("/users/:id", requireAuth, async (req, res) => {
await db.users.update(req.params.id, req.body);
// Attacker POST: { name: "...", role: "admin" }
// → role bị overwrite thành admin
});
// ✅ ĐÚNG: whitelist field cho phép update
app.put("/users/:id", requireAuth, async (req, res) => {
const { name, email, phone } = req.body; // explicit pick
await db.users.update(req.params.id, { name, email, phone });
});
Pitfall 5: Default permissive
// ❌ SAI
function canAccess(user, resource) {
if (user.role === "guest") return false;
return true; // default allow!
}
// ✅ ĐÚNG: default deny
function canAccess(user, resource) {
if (user.role === "admin") return true;
if (user.role === "user" && resource.owner_id === user.id) return true;
return false; // default deny
}
Pitfall 6: Bỏ qua trong query
// ❌ SAI: filter ở app level → race condition, DB leak qua join
const allOrders = await db.orders.find();
return allOrders.filter(o => o.user_id === req.user.id);
// ✅ ĐÚNG: filter ngay trong query
const myOrders = await db.orders.find({ user_id: req.user.id });
PostgreSQL có Row-Level Security (RLS) — enforce ownership ở DB layer, không tin app:
CREATE POLICY user_orders ON orders
FOR SELECT USING (user_id = current_setting('app.user_id')::int);
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
6. Mô hình kết hợp trong thực tế
Apps lớn thường kết hợp nhiều model:
Layer 1: RBAC cho global roles
- admin, support, billing, user
Layer 2: ABAC cho conditions
- user_dept == resource_dept
- request_time IN business_hours
- mfa_passed == true (cho sensitive ops)
Layer 3: ReBAC cho resource ownership
- User own document
- User member of workspace
- Workspace shares folder with team
Ví dụ Notion-like app:
- Global roles (admin, member, guest) → RBAC
- Workspace/page/block ownership và sharing → ReBAC
- Time-based access (token expiry, business hours) → ABAC
7. Câu hỏi ôn tập
-
Authentication và Authorization khác nhau thế nào, và lỗi thường gặp khi chỉ làm Authentication?
Xem đáp án
Authentication = "bạn là ai" (kiểm tra danh tính qua password/MFA/OIDC). Authorization = "bạn được phép làm gì" (kiểm tra quyền với resource cụ thể). Lỗi thường gặp: chỉ check
requireAuthmiddleware → mọi user authenticated truy cập được mọi resource → IDOR (User A xem được order của User B chỉ bằng cách đổi ID trong URL). OWASP A01 (Broken Access Control) là vulnerability #1 năm 2021-2024 chính vì lỗi này. -
RBAC có "role explosion problem" — vấn đề gì và ABAC giải quyết ra sao?
Xem đáp án
Khi tổ chức có nhiều phòng ban × levels × regions × project, số roles bùng nổ theo cấp số nhân (5×4×3×10 = 600 roles). Mỗi user có thể cần nhiều role, mỗi role chỉ khác nhau ở 1-2 attribute. Quản lý nightmare. ABAC giải quyết bằng cách dùng attributes thay vì hardcode role: thay vì 600 roles, có 1 policy
user.dept == resource.dept AND user.level >= resource.required_level— dùng được cho mọi combination. Trade-off: ABAC khó audit và evaluate hơn. -
ABAC policy hữu ích trong tình huống nào mà RBAC không xử lý được?
Xem đáp án
Các tình huống cần context: (1) "User edit chỉ post mình tạo" — cần check
resource.owner_id == user.id, RBAC không có concept ownership per-resource. (2) "Truy cập admin chỉ trong giờ hành chính từ IP công ty" — cần check time và IP. (3) "Tài liệu classified chỉ user có clearance level ≥ tương đương" — cần compare attributes. (4) Multi-tenant SaaS: user thuộc tenant A không thể đọc data tenant B —user.tenant_id == resource.tenant_id. RBAC chỉ làm được nếu tạo 1 role per ownership/tenant → role explosion. -
ReBAC khác ABAC ở điểm cốt lõi nào? Cho ví dụ.
Xem đáp án
ABAC xét attributes (giá trị field) của user/resource/env. ReBAC xét graph relationships giữa các entity và traverse path. Ví dụ Google Drive: "alice có view doc:X?" — ReBAC trace:
alice → member of → team-eng → viewer on → folder:designs → contains → doc:X→ trả lời yes. Khó biểu diễn bằng attribute đơn giản. ReBAC tự nhiên cho collaborative apps (Drive, Notion, GitHub), social network (friend-of-friend), multi-level org structure. Google Zanzibar và open source clone (SpiceDB, OpenFGA) implement pattern này. -
Vì sao filter authorization ở app level (sau khi query) là pattern nguy hiểm?
Xem đáp án
(1) Race condition: nếu permission thay đổi giữa query và filter, dữ liệu lộ. (2) Bug nguy hiểm: dev quên filter sẽ leak data. (3) Performance: query trả về quá nhiều record, filter sau → lãng phí. (4) Logs/error messages có thể leak qua exception. (5) Side channels: số lượng record trả về có thể leak existence. Pattern đúng: filter ngay trong query (
WHERE user_id = ?) hoặc dùng Row-Level Security (Postgres RLS) để DB enforce — code app không bypass được.
Bài tập thực hành
# 1. RBAC từ đầu với Node.js + Postgres
mkdir authz-lab && cd authz-lab
docker run -d --name pg -p 5432:5432 -e POSTGRES_PASSWORD=pw postgres:16
# Tạo schema (xem section 2)
# Seed: admin, editor, viewer roles + permissions
# 2. Test IDOR
# - Implement /orders/:id KHÔNG check ownership
# - Curl với token user A, gọi GET /orders/<order_of_userB>
# - Quan sát leak
# - Sửa endpoint thêm check ownership, test lại
# 3. OPA (Rego) demo
brew install opa # hoặc download binary
cat > policy.rego << 'EOF'
package authz
default allow = false
allow {
input.user.role == "doctor"
input.user.department == input.resource.department
}
EOF
cat > input.json << 'EOF'
{
"user": { "role": "doctor", "department": "cardiology" },
"resource": { "department": "cardiology" }
}
EOF
opa eval -d policy.rego -i input.json "data.authz.allow"
# → true
# Đổi resource.department = "neurology" → false
# 4. SpiceDB demo
docker run -p 50051:50051 authzed/spicedb serve --grpc-preshared-key "secret"
# Cài zed CLI, write schema và relationships (xem section 4)
# 5. Postgres Row-Level Security
psql -h localhost -U postgres -d test -c "
CREATE TABLE notes (id SERIAL, user_id INT, content TEXT);
ALTER TABLE notes ENABLE ROW LEVEL SECURITY;
CREATE POLICY user_notes ON notes USING (user_id = current_setting('app.user_id')::int);
"
# SET app.user_id = '1' → chỉ thấy notes của user 1
Tài liệu tham khảo chính thức
- OWASP Top 10 — A01:2021 Broken Access Control
- NIST RBAC standard
- Google Zanzibar paper
- Open Policy Agent
- AWS Cedar
- SpiceDB