RBAC System
Role-Based Access Control — permission management, role hierarchy, resource authorization สำหรับ multi-tenant systems
What I Can Do
- ออกแบบ RBAC system ตั้งแต่ database schema ถึง middleware enforcement
- Implement role hierarchy และ permission inheritance
- สร้าง permission middleware สำหรับ Go (Gin/Fiber) และ Node.js (Express)
- ออกแบบ multi-tenant RBAC ที่แยก permissions per organization
- จัดการ dynamic permissions ที่เปลี่ยนได้ runtime โดยไม่ต้อง redeploy
RBAC Overview
RBAC แยก "ใครทำอะไรได้" ออกจาก code — แทนที่จะ hardcode if user.role == "admin" ในทุก handler ให้กำหนด permissions เป็น data แล้วตรวจผ่าน middleware
Core concepts:
- User — คนที่ใช้ระบบ
- Role — กลุ่มของ permissions เช่น admin, trader, viewer
- Permission — สิทธิ์เฉพาะเจาะจง เช่น
orders:create,users:delete - Resource — สิ่งที่ถูก protect เช่น orders, users, reports
Database Schema Design
Basic RBAC (User → Role → Permission)
-- Roles
CREATE TABLE roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(50) UNIQUE NOT NULL,
description TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Permissions
CREATE TABLE permissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
resource VARCHAR(50) NOT NULL,
action VARCHAR(50) NOT NULL,
description TEXT,
UNIQUE(resource, action)
);
-- Role ↔ Permission (many-to-many)
CREATE TABLE role_permissions (
role_id UUID REFERENCES roles(id) ON DELETE CASCADE,
permission_id UUID REFERENCES permissions(id) ON DELETE CASCADE,
PRIMARY KEY (role_id, permission_id)
);
-- User ↔ Role (many-to-many)
CREATE TABLE user_roles (
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
role_id UUID REFERENCES roles(id) ON DELETE CASCADE,
PRIMARY KEY (user_id, role_id)
);Permission Naming Convention
ใช้ resource:action format — ชัดเจน, query ง่าย, group ตาม resource ได้:
orders:create— สร้าง orderorders:read— ดู orderorders:update— แก้ไข orderorders:delete— ลบ orderorders:cancel— ยกเลิก order (custom action)reports:export— export reportusers:manage— จัดการ users (สร้าง/แก้/ลบ)
Seed Data Example
-- Roles
INSERT INTO roles (name, description) VALUES
('super_admin', 'Full system access'),
('admin', 'Manage users and view all data'),
('trader', 'Create and manage own orders'),
('viewer', 'Read-only access');
-- Permissions
INSERT INTO permissions (resource, action) VALUES
('orders', 'create'),
('orders', 'read'),
('orders', 'update'),
('orders', 'cancel'),
('users', 'read'),
('users', 'manage'),
('reports', 'read'),
('reports', 'export'),
('settings', 'manage');Role Hierarchy
Role hierarchy ให้ role ที่สูงกว่า inherit permissions ของ role ที่ต่ำกว่า — ลด duplication:
super_admin → admin → trader → viewerviewer— read orders, read reportstrader— ทุกอย่างของ viewer + create/cancel ordersadmin— ทุกอย่างของ trader + manage users, export reportssuper_admin— ทุกอย่างของ admin + manage settings
Implement ได้ 2 แบบ:
- Flatten permissions ตอน assign role — เร็วตอน query แต่ต้อง sync เมื่อ role เปลี่ยน
- Resolve hierarchy ตอน check — flexible แต่ต้อง query หลายชั้น (cache ช่วยได้)
Permission Check Flow
Request → Auth Middleware (JWT) → RBAC Middleware → Handler
↓ ↓
Extract userID Check permission
from token from user's roles- Auth middleware extract user จาก JWT token
- RBAC middleware query user's roles → permissions
- ตรวจว่า user มี permission ที่ต้องการหรือไม่
- ถ้ามี → ผ่านไป handler, ถ้าไม่มี → return 403
Middleware Implementation (Go)
func RequirePermission(resource, action string) gin.HandlerFunc {
return func(c *gin.Context) {
userID := c.GetString("userID")
if userID == "" {
c.AbortWithStatusJSON(401, gin.H{"error": "unauthorized"})
return
}
hasPermission, err := rbacService.CheckPermission(userID, resource, action)
if err != nil {
c.AbortWithStatusJSON(500, gin.H{"error": "permission check failed"})
return
}
if !hasPermission {
c.AbortWithStatusJSON(403, gin.H{"error": "forbidden"})
return
}
c.Next()
}
}
// Usage
r.POST("/orders", RequirePermission("orders", "create"), createOrderHandler)
r.GET("/reports", RequirePermission("reports", "read"), getReportsHandler)
r.DELETE("/users/:id", RequirePermission("users", "manage"), deleteUserHandler)Permission Query
Query permissions ของ user ผ่าน role:
SELECT DISTINCT p.resource, p.action
FROM permissions p
JOIN role_permissions rp ON rp.permission_id = p.id
JOIN user_roles ur ON ur.role_id = rp.role_id
WHERE ur.user_id = $1;ใช้ DISTINCT เพราะ user อาจมีหลาย roles ที่ permissions ซ้ำกัน
Caching Permissions
Query DB ทุก request แพงเกินไป — cache permissions ใน Redis:
- Key:
rbac:user:{userID}→ Set ของresource:action - TTL: 5-15 นาที (balance ระหว่าง performance กับ freshness)
- Invalidate cache เมื่อ: เปลี่ยน role ของ user, เปลี่ยน permissions ของ role
- ใช้ Redis SET +
SISMEMBERตรวจ permission — O(1)
Multi-Tenant RBAC
สำหรับระบบที่มีหลาย organizations — user อาจมี role ต่างกันในแต่ละ org:
CREATE TABLE org_user_roles (
org_id UUID REFERENCES organizations(id),
user_id UUID REFERENCES users(id),
role_id UUID REFERENCES roles(id),
PRIMARY KEY (org_id, user_id, role_id)
);Permission check ต้อง scope ด้วย org: CheckPermission(userID, orgID, resource, action)
Resource-Level Permissions
นอกจาก "ทำอะไรได้" ยังต้องตรวจ "ทำกับ resource ไหนได้":
- Role-level — trader สามารถ
orders:readได้ทุก orders (ของทุกคน) - Ownership-level — trader สามารถ
orders:cancelได้เฉพาะ orders ของตัวเอง
Implement ด้วย 2 ชั้น:
- RBAC middleware ตรวจ role-level permission
- Handler ตรวจ ownership:
if order.UserID != currentUserID { return 403 }
Audit Logging
Log ทุก permission-related actions สำหรับ compliance:
- ใครเข้าถึง resource อะไร เมื่อไหร่
- ใครเปลี่ยน role/permission ของใคร
- Failed access attempts (403)
- สำคัญมากสำหรับ fintech — ต้อง audit trail ครบ
Admin UI Considerations
- แสดง role → permissions mapping ให้ admin เห็นชัด
- ป้องกัน admin ลบ role ของตัวเอง (lock out prevention)
- Preview ก่อน apply: แสดงว่าเปลี่ยน permission แล้วกระทบ users กี่คน
- Super admin role ไม่ควรแก้ไขหรือลบได้
ปัญหาที่เจอบ่อย & วิธีแก้
Permission check ช้า — ทุก request query DB
Response time เพิ่มขึ้น 50-100ms ต่อ request เพราะ permission query
สาเหตุ: ทุก request query DB เพื่อ resolve user → roles → permissions ซึ่งต้อง JOIN หลาย tables
วิธีแก้:
- Cache permissions ใน Redis: key
rbac:user:{id}เก็บ set ของ permissions, TTL 5-15 นาที - Invalidate cache เฉพาะ user ที่ถูกแก้ ไม่ต้อง flush ทั้งหมด
- ใส่ permissions ใน JWT claims ถ้า permissions ไม่เปลี่ยนบ่อย — ไม่ต้อง query เลย แต่ต้อง refresh token เมื่อ role เปลี่ยน
Hardcode role names กระจายทั่ว codebase
if role == "admin" อยู่ในทุก handler — เพิ่ม role ใหม่ต้องแก้ code หลายจุด
สาเหตุ: ตรวจ role name ตรงๆ แทนที่จะตรวจ permission, ไม่มี abstraction layer ระหว่าง role กับ business logic
วิธีแก้:
- ตรวจ permission ไม่ใช่ role:
RequirePermission("orders", "create")แทนRequireRole("admin") - เพิ่ม role ใหม่แค่กำหนด permissions ใน DB ไม่ต้องแก้ code
- ใช้ middleware pattern — handler ไม่ต้องรู้เรื่อง authorization เลย
เปลี่ยน role แล้วไม่มีผลทันที
Admin เปลี่ยน role ของ user แต่ user ยังทำอะไรได้เหมือนเดิม
สาเหตุ: permissions ถูก cache (Redis หรือ JWT claims) ยังไม่ expire, user ยังใช้ token เก่าที่มี permissions เดิม
วิธีแก้:
- Invalidate Redis cache ทันทีเมื่อเปลี่ยน role:
DEL rbac:user:{id} - ถ้าใช้ JWT claims: force token refresh หรือ revoke token เดิม + ให้ login ใหม่
- ใช้ short TTL สำหรับ permission cache (5 นาที) เป็น balance ระหว่าง performance กับ freshness
Super admin ถูกลบ role — lock out ทั้งระบบ
Admin ลบ super_admin role โดยไม่ตั้งใจ ไม่มีใครเข้า admin panel ได้
สาเหตุ: ไม่มี protection สำหรับ critical roles, ไม่ตรวจว่าต้องมี super admin อย่างน้อย 1 คนเสมอ
วิธีแก้:
- Mark roles ที่ห้ามลบ:
is_system BOOLEAN DEFAULT false— super_admin เป็น system role - ตรวจก่อนลบ role จาก user: ถ้าเป็น super admin คนสุดท้ายห้ามลบ
- มี database seed/migration ที่สร้าง super admin กลับได้เสมอ
- Separate super admin management ออกจาก regular admin UI
Permission explosion — permissions เยอะเกินจัดการ
ระบบมี 200+ permissions, สร้าง role ใหม่ต้อง tick ทีละตัว
สาเหตุ: granularity สูงเกินไป — สร้าง permission ทุก action ของทุก resource, ไม่มี grouping
วิธีแก้:
- Group permissions ตาม resource: แสดง UI เป็น
orders: [create, read, update, cancel]ไม่ใช่ flat list - ใช้ wildcard permissions:
orders:*= ทุก actions ของ orders - สร้าง role templates: "Trader Template" มี preset permissions ที่ใช้บ่อย แล้ว customize ต่อ
- Role hierarchy: สร้าง base roles แล้ว inherit ลดการ assign ซ้ำ
Row-level permission ปนกับ role permission
ตรวจ ownership ใน handler บ้าง middleware บ้าง ไม่ consistent
สาเหตุ: RBAC middleware ตรวจได้แค่ "user มี permission X ไหม" แต่ไม่รู้ว่า resource นั้นเป็นของ user หรือไม่ — logic กระจายอยู่ในแต่ละ handler
วิธีแก้:
- แยก 2 ชั้นให้ชัด: middleware ตรวจ role-level, handler ตรวจ ownership
- สร้าง helper function:
canAccessOrder(userID, orderID)ใช้ทุก handler ที่เกี่ยวกับ orders - Document ให้ชัดว่า middleware ตรวจอะไร handler ตรวจอะไร — ป้องกัน logic หลุด
- สำหรับ admin roles: skip ownership check ด้วย permission พิเศษ เช่น
orders:read_all
Related Skills
- Gin — implement RBAC middleware สำหรับ Go APIs
- Fiber — implement RBAC middleware สำหรับ high-performance APIs
- GORM — data layer สำหรับ roles/permissions tables
- PostgreSQL — database ที่เก็บ RBAC schema
- Redis — cache permissions สำหรับ fast lookups
- REST API Design — API design ที่ใช้ RBAC protect endpoints