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
- User หนึ่งคนสามารถมีหลาย Roles ในหลาย Scopes
- Permission Inheritance (Global → Organization → Project → Contract)
- Fine-grained Access Control (เช่น "อนุญาตให้ดู Correspondence เฉพาะใน Project A รวมถึง Contract ภายใต้ Project A เท่านั้น")
- 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) เข้าสู่งองค์กร
- Org Admin เพิ่ม users (
- 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):
- When Role Assignemnt Modified: เมื่อช้อมูลในตาราง
user_assignments(เช่นลบ/เพิ่ม user ระดับโปรเจกต์) หรือตารางrole_permissions(สลับสิทธิ์ของกลุ่ม) เกิดการแก้ไขในฐานข้อมูลแล้ว. - Execute Invalidation: API Services ต้องทำการลบ Cache เก่าที่ฝั่งขัดข้องทันที ผ่านคำสั่ง
DEL user:{user_id}:permissionsณ ฝั่ง Redis. - ระบบจะทำงานโปรโตซ้ำ (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
- Option 1: Simple Role-Based (No Scope)
- Pros: Very simple implementation, Easy to understand
- Cons: ไม่รองรับ Multi-organization, Superadmin เห็นข้อมูลองค์กรอื่นมั่วข้ามกลุ่ม
- Option 2: Organization-Only Scope
- Pros: แยกข้อมูลระหว่าง Organizations ได้ชัดเจน
- Cons: ไม่รองรับระดับ Sub-level Permissions (Project/Contract) ไม่เพียงพอกับ Business Rule.
- 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).