</>Học Dev
Bài học

Tuần 2 - Ngày 11: RBAC, ABAC & Authorization Patterns

Tuần 2 – Ngày 11

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

AUTHENTICATION(AuthN)AUTHORIZATION(AuthZ)"Whoareyou?""Whatcanyoudo?"-Password-Roles-MFA-Permissions-OIDC-Policies-Session-ResourceownershipOutput:useridentityOutput:allow/denyBưc1Bưc2(đãhcW2)(bàihômnay)

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

UserRolePermissionhasgrantsAction+Resource"read:orders""write:users"

User được gán role → role có permissions → permission cho phép action trên resource type.

Ví dụ: blog platform

Roles:adminpermissions:[posts:create,posts:read,posts:update,posts:delete,users:read,users:update,users:delete,site:configure]editorpermissions:[posts:create,posts:read,posts:update]viewerpermissions:[posts:read]anonymouspermissions:[posts:read]chpublicposts

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

ProsCons
Đơn giản, dễ hiểu, dễ implementKhông xử lý được "user A chỉ edit post của mình"
Tốt cho ít roles, rõ ràngRole 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 attributeVí dụ
User (Subject)dept, level, clearance, location, age
Resource (Object)owner, classification, type, dept
Actionread, write, delete, share
Environmenttime, 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

ClientRequest:GET/record/42PolicyEnforce-PolicyAdminmentPoint(PEP)Point(PAP)app/proxy(UIviếtpolicy)Query:allow?definePolicyDecisionPoint(PDP)(OPA,Cedar,AWSIAM)fetchattributesPolicyInfoPoint(PIP)(DB,LDAP,...)
ComponentVai trò
PEPIntercept request, gọi PDP để hỏi quyết định
PDPEvaluate policy, return allow/deny
PAPNơi admin viết policy
PIPProvide attributes (user, resource) cho PDP

RBAC vs ABAC

RBACABAC
Quyết định dựa trênRoleAttributes (user/resource/env)
Context-aware (time, IP)Không (cần extend)
"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
AuditDễ (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:

user:alicemembergroup:engineeringgroup:engineeringviewerdoc:design-specalicecóquynviewertrêndocuser:bobownerdoc:design-specboblàownercóquyncaohơn

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

SpiceDBOpenFGAPermify
OriginAuthZedAuth0/OktaPermify Inc
Inspired byZanzibarZanzibarZanzibar
Open sourceYesYes (CNCF)Yes
Use caseProduction, enterpriseMainstreamProduction

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

  1. 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 requireAuth middleware → 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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


Bài tiếp theo →