Files
lcbp3/specs/01-requirements/01-02-business-rules/01-02-01-rbac-matrix.md
admin ef16817f38
All checks were successful
Build and Deploy / deploy (push) Successful in 4m44s
260223:1415 20260223 nextJS & nestJS Best pratices
2026-02-23 14:15:06 +07:00

18 KiB

4. Access Control & RBAC Matrix (V1.8.0)


title: 'Access Control & RBAC Matrix' version: 1.8.0 status: APPROVED owner: Nattanin Peancharoen / Development Team last_updated: 2026-02-23 related:

  • specs/02-architecture/02-01-system-architecture.md
  • specs/03-implementation/03-02-backend-guidelines.md
  • specs/07-database/07-01-data-dictionary-v1.8.0.md
  • specs/05-decisions/ADR-005-redis-usage-strategy.md
  • specs/05-decisions/ADR-001-unified-workflow-engine.md references:
  • RBAC Implementation
  • Access Control

4.1. Overview and Problem Statement

LCBP3-DMS จัดการสิทธิ์การเข้าถึงข้อมูลที่ซับซ้อน ได้แก่:

  • Multi-Organization: หลายองค์กรใช้ระบบร่วมกัน แต่ต้องแยกข้อมูลตามบริบท
  • Project-Based: โครงการสามารถมีหลายสัญญาย่อย (Contracts)
  • Hierarchical Permissions: สิทธิ์ระดับบนสามารถครอบคลุมระดับล่าง
  • Dynamic Roles: สิทธิ์สามารถปรับเปลี่ยน Role หรือเพิ่ม Role พิเศษได้โดยไม่ต้อง Deploy ระบบใหม่.

Users และ Organizations สามารถเข้าดูหรือแก้ไขเอกสารได้จากสิทธิ์ที่ได้รับ ระบบออกแบบด้วย 4-Level Hierarchical Role-Based Access Control (RBAC) ដើម្បីรองรับความซับซ้อนนี้.

Key Requirements

  1. User หนึ่งคนสามารถมีหลาย Roles ในหลาย Scopes
  2. Permission Inheritance (Global → Organization → Project → Contract)
  3. Fine-grained Access Control (เช่น "อนุญาตให้ดู Correspondence เฉพาะใน Project A รวมถึง Contract ภายใต้ Project A เท่านั้น")
  4. Performance (Check permission ผ่าน Redis ต้องเร็ว < 10ms)

4.2. Permission Hierarchy & Enforcement

4-Level Scope Hierarchy

การออกแบบสิทธิ์ครอบคลุม Scope ลำดับขั้นดังนี้:

Global (ทั้งระบบ)
│
├─ Organization (ระดับองค์กร)
│  ├─ Project (ระดับโครงการ)
│  │  └─ Contract (ระดับสัญญา)
│  │
│  └─ Project B
│     └─ Contract B
│
└─ Organization 2
   └─ Project C

Permission Enforcement:

  • เมื่อตรวจสอบสิทธิ์ ระบบจะพิจารณาสิทธิ์จากทุก Level ที่ผู้ใช้มี และใช้สิทธิ์ที่ "ครอบคลุมที่สุด (Most Permissive)" ในบริบท (Context) นั้นมาเป็นเกณฑ์.
  • Example: User A เป็น Viewer ในองค์กร (ระดับ Organization Level) แต่มอบหมายหน้าที่ให้เป็น Editor ในตำแหน่งของ Project X. เมื่อ User A ดำเนินการในบริบท Context ของ Project X (หรือ Contract ที่อยู่ใต้ Project X), User A จะสามารถทำงานด้วยสิทธิ์ Editor ทันที.

4.3. Role and Scope Summary

Role Scope Description Key Permissions
Superadmin Global System administrator Do everything in the system, manage organizations, manage global data
Org Admin Organization Organization administrator Manage users in the organization, manage roles/permissions within the organization, view organization reports
Document Control Organization Document controller Add/edit/delete documents, set document permissions within the organization
Editor Organization Document editor Edit documents that have been assigned to them
Viewer Organization Document viewer View documents that have access permissions
Project Manager Project Project manager Manage members in the project (add/delete/assign roles), create/manage contracts in the project, view project reports
Contract Admin Contract Contract administrator Manage users in the contract, manage roles/permissions within the contract, view contract reports

Master Data Management Authority

Master Data Manager Scope
Document Type (Correspondence, RFA) Superadmin Global
Document Status (Draft, Approved, etc.) Superadmin Global
Shop Drawing Category Project Manager Project (สร้างใหม่ได้ภายในโครงการ)
Tags Org Admin / Project Manager Organization / Project
Custom Roles Superadmin / Org Admin Global / Organization
Document Numbering Formats Superadmin / Admin Global / Organization

4.4. Onboarding Workflow

  • 4.4.1. Create Organization
    • Superadmin สร้าง Organization ใหม่ (e.g. Company A)
    • Superadmin แต่งตั้ง User อย่างน้อย 1 คน ให้เป็น Org Admin หรือ Document Control
  • 4.4.2. Add Users to Organization
    • Org Admin เพิ่ม users (Editor, Viewer) เข้าสู่งองค์กร
  • 4.4.3. Assign Users to Project
    • Project Manager เชิญ/กำหนดผู้ใช้เข้าสู่ Project. ในขั้นตอนนี้จะกำหนด Project Role
  • 4.4.4. Assign Users to Contract
    • Contract Admin เลือก users จาก Project และมอบหมายหน้าที่เจาะจงใน Contract ขั้นตอนนี้จะกำหนด Contract Role
  • 4.4.5. Security Onboarding
    • บังคับ Users to change password เป็นครั้งแรก
    • ฝึกอบรม (Security awareness training) สำหรับ users ที่มีสิทธิ์สูงระดับแอดมิน.
    • บันทึก Audit Log ทุกเหตุการณ์เกี่ยวกับการมอบหมาย/ตั้งค่า Permissions.

4.5. Implementation Details

4.5.1 Database Schema (RBAC Tables)

-- Role Definitions with Scope
CREATE TABLE roles (
  role_id INT PRIMARY KEY AUTO_INCREMENT,
  role_name VARCHAR(100) NOT NULL,
  scope ENUM('Global', 'Organization', 'Project', 'Contract') NOT NULL,
  description TEXT,
  is_system BOOLEAN DEFAULT FALSE
);

-- Granular Permissions
CREATE TABLE permissions (
  permission_id INT PRIMARY KEY AUTO_INCREMENT,
  permission_name VARCHAR(100) NOT NULL UNIQUE,
  description TEXT,
  module VARCHAR(50),
  -- Scope Reference ENUM includes CONTRACT
  scope_level ENUM('GLOBAL', 'ORG', 'PROJECT', 'CONTRACT')
);

-- Role -> Permissions Mapping
CREATE TABLE role_permissions (
  role_id INT,
  permission_id INT,
  PRIMARY KEY (role_id, permission_id),
  FOREIGN KEY (role_id) REFERENCES roles(role_id) ON DELETE CASCADE,
  FOREIGN KEY (permission_id) REFERENCES permissions(permission_id) ON DELETE CASCADE
);

-- User Role Assignments with Context Map
CREATE TABLE user_assignments (
  id INT PRIMARY KEY AUTO_INCREMENT,
  user_id INT NOT NULL,
  role_id INT NOT NULL,
  organization_id INT NULL,
  project_id INT NULL,
  contract_id INT NULL,
  assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE,
  FOREIGN KEY (role_id) REFERENCES roles(role_id) ON DELETE CASCADE,
  FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
  FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
  FOREIGN KEY (contract_id) REFERENCES contracts(id) ON DELETE CASCADE,

  -- ควบคุมโครงสร้าง: ไม่ให้ระบุ Scope เลื่อนลอยข้ามขั้นต้องมีชั้นเดียวชัดเจน
  CONSTRAINT chk_scope CHECK (
    (organization_id IS NOT NULL AND project_id IS NULL AND contract_id IS NULL) OR
    (organization_id IS NULL AND project_id IS NOT NULL AND contract_id IS NULL) OR
    (organization_id IS NULL AND project_id IS NULL AND contract_id IS NOT NULL) OR
    (organization_id IS NULL AND project_id IS NULL AND contract_id IS NULL)
  )
);

4.5.2 Setup CASL Ability Rules

เพื่อให้ "การสืบทอดสิทธิ์ (Inheritance Logic)" ทำงานได้ถูกต้อง เช่น บทบาทระดับ Project Manager ให้แผ่คลุมไปถึงทรัพยากรระดับ Contract ด้านในด้วย (Logic project_id -> all embedded contracts).

// ability.factory.ts
import { AbilityBuilder, PureAbility } from '@casl/ability';

export type AppAbility = PureAbility<[string, any]>;

@Injectable()
export class AbilityFactory {
  async createForUser(user: User): Promise<AppAbility> {
    const { can, cannot, build } = new AbilityBuilder<AppAbility>(PureAbility);

    const assignments = await this.getUserAssignments(user.user_id);

    for (const assignment of assignments) {
      const role = await this.getRole(assignment.role_id);
      const permissions = await this.getRolePermissions(role.role_id);

      for (const permission of permissions) {
        // e.g. 'correspondence.create', 'project.view'
        const [subject, action] = permission.permission_name.split('.');

        // Apply Scope conditions based on the Role's specified scope level
        switch (role.scope) {
          case 'Global':
            // ได้รับสิทธิ์กับองค์ประกอบทั้งหมด
            can(action, subject);
            break;

          case 'Organization':
            // อนุญาตการ Action ทั้งในองค์กร และโปรเจกต์ภายใต้องค์กร
            can(action, subject, { organization_id: assignment.organization_id });
            // Advanced Case: In some queries mapped to Projects/Contracts, support fallback checks
            // can(action, subject, { '__orgIdFallback': assignment.organization_id });
            break;

          case 'Project':
            // สืบทอดสิทธิ์ไปยัง Project ID นั้นๆ เสมอ และสิทธิ์ครอบคลุมถึงทุก Contracts ที่ผูกกับ Project นี้
            can(action, subject, { project_id: assignment.project_id });
            break;

          case 'Contract':
            // จำกัดเฉพาะใน Contract นั้น ตรงเป้าหมาย
            can(action, subject, { contract_id: assignment.contract_id });
            break;
        }
      }
    }

    return build();
  }
}

4.5.3 Token Management & Redis Permission Cache

  • Payload Optimization: JWT Access Token ให้เก็บเฉพาะ userId และ ข้อมูล Sessions ขั้นต้น. โครงสร้าง Permissions List ปริมาณมากจะไม่อยู่ใน Token.
  • Permission Caching: นำโครงสร้างสิทธิ์ทั้งหมด (Ability Rules ที่ประกอบแล้วจาก 4.5.2) ไป Cache ไว้เป็น Key ภายใต้ฐานข้อมูล Redis โดยคงระยะการตั้ง TTL ประมาณ 30 นาที 1800 วินาที. ลดขนาด Payload ลง และเพื่อเป้าหมาย Performance ที่ < 10ms.

4.5.4 Permission Guard Enforcement (NestJS)

// permission.guard.ts
@Injectable()
export class PermissionGuard implements CanActivate {
  constructor(
    private reflector: Reflector,
    private abilityFactory: AbilityFactory,
    private redis: Redis
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const permission = this.reflector.get<string>('permission', context.getHandler());
    if (!permission) return true; // Route without specific permission requirements

    const request = context.switchToHttp().getRequest();
    const user = request.user;

    // Check Redis cache first
    const cacheKey = `user:${user.user_id}:permissions`;
    let abilityRules = await this.redis.get(cacheKey);

    if (!abilityRules) {
      const newAbility = await this.abilityFactory.createForUser(user);
      abilityRules = newAbility.rules;
      // Store in Cache (1800ms TTL)
      await this.redis.set(cacheKey, JSON.stringify(abilityRules), 'EX', 1800);
    } else {
      abilityRules = JSON.parse(abilityRules);
    }

    const ability = new PureAbility(abilityRules);
    const [action, subject] = permission.split('.');

    // Evaluate mapped payload / resource parameters directly into CASL engine
    const resource = { ...request.params, ...request.body };
    return ability.can(action, subject, resource);
  }
}

Controller Example Usage:

@Controller('correspondences')
@UseGuards(JwtAuthGuard, PermissionGuard)
export class CorrespondenceController {

  @Post()
  @RequirePermission('correspondence.create')
  async create(@Body() dto: CreateCorrespondenceDto) {
    // Accessible only when CASL Evaluate "correspondence.create" successfully fits the parameters limits (Project/Contract scope check)
  }

}

4.6. Cache Invalidation Strategy

เพื่อให้ระบบ Security Update ได้รวดเร็วและปลอดภัย ต้องมีกลไกสั่งรีเซ็ตสิทธิ์ (Invalidation):

  1. When Role Assignemnt Modified: เมื่อช้อมูลในตาราง user_assignments (เช่นลบ/เพิ่ม user ระดับโปรเจกต์) หรือตาราง role_permissions (สลับสิทธิ์ของกลุ่ม) เกิดการแก้ไขในฐานข้อมูลแล้ว.
  2. Execute Invalidation: API Services ต้องทำการลบ Cache เก่าที่ฝั่งขัดข้องทันที ผ่านคำสั่ง DEL user:{user_id}:permissions ณ ฝั่ง Redis.
  3. ระบบจะทำงานโปรโตซ้ำ (Cold Boot) เข้ามาหยิบ DB -> สร้าง CASL Factory ใหม่ ใน First API Request อีกครั้งในทันที.

4.7. Appendix: ADR-004 Decision History & Justification

ส่วนอ้างอิงและประวัติศาสตร์การตัดสินใจพิจารณาในอดีต (Reference) ก่อนการนำมาปรับใช้ร่าง Architecture ฉบับ V1.8.0.

Considered Options Before Version 1.5.0

  1. Option 1: Simple Role-Based (No Scope)
  • Pros: Very simple implementation, Easy to understand
  • Cons: ไม่รองรับ Multi-organization, Superadmin เห็นข้อมูลองค์กรอื่นมั่วข้ามกลุ่ม
  1. Option 2: Organization-Only Scope
  • Pros: แยกข้อมูลระหว่าง Organizations ได้ชัดเจน
  • Cons: ไม่รองรับระดับ Sub-level Permissions (Project/Contract) ไม่เพียงพอกับ Business Rule.
  1. Option 3: 4-Level Hierarchical RBAC (Selected)
  • Pros: ครอบคลุม Use Case สูงสุด (Maximum Flexibility), รองรับ Hierarchy สืบทอดสิทธิ์ (Inheritance), ขอบเขตจำกัดข้อมูลอย่างรัดกุมระดับ Contract (Data Isolation).
  • Cons: Complexity ทางด้านการ Implement, Invalidate Caching หางานให้ฝั่ง Operations, Developer Learning Curve.

Decision Rationale: เลือกแนวทางที่ 3 รองรับความต้องการของ Construction Projects ที่มีการแบ่งกลุ่มรับเหมาช่วงย่อยระดับ Contracts ต่อเนื่อง (Future proof). แก้ไขปัญหา Performance Cons ด้วยแนวคิดการประจุ Redis Caching ข้ามขั้ว และล้าง Invalidation เฉพาะเมื่อ Triggering Database Updates (อ้างอิงหัวข้อ 4.6).