Skip to content
← All Skills
🔐

RBAC System

Backend

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)

sql
-- 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 — สร้าง order
  • orders:read — ดู order
  • orders:update — แก้ไข order
  • orders:delete — ลบ order
  • orders:cancel — ยกเลิก order (custom action)
  • reports:export — export report
  • users:manage — จัดการ users (สร้าง/แก้/ลบ)

Seed Data Example

sql
-- 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:

text
super_admin → admin → trader → viewer
  • viewer — read orders, read reports
  • trader — ทุกอย่างของ viewer + create/cancel orders
  • admin — ทุกอย่างของ trader + manage users, export reports
  • super_admin — ทุกอย่างของ admin + manage settings

Implement ได้ 2 แบบ:

  • Flatten permissions ตอน assign role — เร็วตอน query แต่ต้อง sync เมื่อ role เปลี่ยน
  • Resolve hierarchy ตอน check — flexible แต่ต้อง query หลายชั้น (cache ช่วยได้)

Permission Check Flow

text
Request → Auth Middleware (JWT) → RBAC Middleware → Handler
                ↓                       ↓
          Extract userID          Check permission
          from token              from user's roles
  1. Auth middleware extract user จาก JWT token
  2. RBAC middleware query user's roles → permissions
  3. ตรวจว่า user มี permission ที่ต้องการหรือไม่
  4. ถ้ามี → ผ่านไป handler, ถ้าไม่มี → return 403

Middleware Implementation (Go)

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:

sql
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:

sql
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 ชั้น:

  1. RBAC middleware ตรวจ role-level permission
  2. 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