RBAC คืออะไร และออกแบบ permission ยังไงไม่ให้ระบบโตแล้วพัง
หลายระบบเริ่มต้นด้วยตรรกะสิทธิ์ที่ดูเรียบง่ายมาก
ถ้าเป็น admin ให้ทำได้ทุกอย่าง
ถ้าไม่ใช่ admin ให้ทำได้น้อยลง
ถ้าเป็น staff ให้เห็นเฉพาะบางหน้า
ถ้าเป็น user ปกติให้ใช้งานได้เฉพาะของตัวเอง
ช่วงแรกแนวทางนี้มักยังไปต่อได้ เพราะระบบยังเล็ก หน้าไม่เยอะ และคนในทีมยังจำกติกากันได้อยู่ แต่พอระบบเริ่มโตขึ้น มีหลายทีมใช้งาน มีหลาย workflow และมี action ที่ละเอียดขึ้น เช่น approve, reject, refund, override, export, impersonate หรือแก้ข้อมูลสำคัญ ปัญหาก็จะเริ่มโผล่ทีละอย่าง
ทำไม staff บางคนเห็นเมนูแต่กดใช้งานไม่ได้
ทำไม support reset บางอย่างได้ แต่ refund ไม่ได้
ทำไม finance ต้อง approve ได้แต่ไม่ควรแก้ customer profile
ทำไม admin แก้ role ได้ แต่ไม่มีหลักฐานย้อนหลัง
ทำไม permission ชุดเดิมไปผูกกับ endpoint ใหม่แล้วหลุดสิทธิ์โดยไม่ตั้งใจ
ปัญหาเหล่านี้ไม่ได้เกิดจาก RBAC อย่างเดียว แต่เกิดจากการที่ระบบใช้ role แบบหยาบเกินไป ใช้ชื่อ role แทน business intent และไม่มีโครงสร้างสิทธิ์ที่รองรับการเติบโตของระบบ
บทความนี้อธิบายว่า RBAC คืออะไร ควรใช้ตอนไหน และออกแบบ permission อย่างไรให้โตได้โดยไม่พังง่าย
TL;DR
ถ้าจะสรุปให้สั้นที่สุด
RBAC ที่ดีไม่ใช่แค่มีชื่อ role แต่ต้องแยก role ออกจาก permission และแยก permission ออกจาก business rule ให้ชัด
ถ้าคุณเขียนทุกอย่างเป็น if user.role === "admin" ไปเรื่อย ๆ วันหนึ่งระบบจะเริ่มแก้ยาก ตรวจยาก และเสี่ยงให้สิทธิ์เกินกว่าที่ตั้งใจ
RBAC คืออะไร
RBAC ย่อมาจาก Role-Based Access Control เป็นแนวคิดการควบคุมสิทธิ์โดยอาศัย “บทบาท” ของผู้ใช้เป็นตัวกลาง
แทนที่จะผูกสิทธิ์เข้ากับผู้ใช้ทีละคน ระบบจะกำหนดว่า role ไหนทำอะไรได้บ้าง เช่น
adminsupportfinanceopscustomerpartner
จากนั้นค่อยให้ผู้ใช้แต่ละคนได้รับ role ที่เกี่ยวข้องกับหน้าที่ของตัวเอง
แนวคิดนี้ช่วยลดความวุ่นวายได้มากกว่าการกำหนดสิทธิ์แบบ ad hoc เพราะสิทธิ์ถูกจัดเป็นกลุ่มที่เข้าใจได้ในระดับองค์กร
แต่จุดสำคัญคือ RBAC ที่ดีไม่ได้จบแค่การตั้งชื่อ role เพราะถ้าระบบมีแค่ role โดยไม่มีชั้น permission ที่ชัดเจน สุดท้าย role จะอ้วนขึ้นเรื่อย ๆ จนกลายเป็นถังรวมสิทธิ์
ปัญหาของระบบที่ใช้ role แบบหยาบเกินไป
ระบบจำนวนมากเริ่มต้นแบบนี้
if (user.role === "admin") {
// allow everything
}
หรือบางทีขยับขึ้นมาอีกนิดเป็น
if (user.role === "admin" || user.role === "manager") {
// allow refund
}
โค้ดแบบนี้ดูตรงไปตรงมาและใช้งานได้เร็ว แต่เมื่อระบบมี action มากขึ้น มันจะเริ่มสร้างหนี้ทันที เพราะ role เดียวอาจถูกใช้แทนสิทธิ์หลายความหมายปนกัน เช่น
- สิทธิ์อ่านข้อมูล
- สิทธิ์แก้ข้อมูล
- สิทธิ์อนุมัติ
- สิทธิ์ override
- สิทธิ์ export
- สิทธิ์ดูข้อมูลข้ามทีม
- สิทธิ์จัดการผู้ใช้
สุดท้าย role ที่ชื่อเหมือนเดิมจะไม่ได้สื่อความหมายชัดอีกต่อไป ทีมใหม่อ่านไม่ออกว่า manager จริง ๆ ทำอะไรได้บ้าง และทุกครั้งที่เพิ่ม flow ใหม่ก็ต้องคอยเดาว่าจะยัดเข้า role ไหน
นี่คือจุดเริ่มต้นของระบบ permission ที่โตแล้วพัง
ความต่างระหว่าง Role, Permission และ Policy
จุดที่หลายทีมพลาดบ่อยคือเอาสามอย่างนี้มาปนกัน
Role
Role คือ “กลุ่มหน้าที่” ในระดับองค์กรหรือระบบ เช่น support, finance, ops
Role ควรใช้เพื่อสื่อภาพรวมว่าผู้ใช้นี้อยู่ในบทบาทไหน ไม่ควรแบกกติกาทุกอย่างไว้ในตัวเอง
Permission
Permission คือสิทธิ์ที่เฉพาะเจาะจงขึ้น เช่น
customer.readcustomer.updaterefund.approverefund.rejectdocument.downloaduser.role.update
Permission คือหน่วยที่เอาไว้ตอบคำถามว่า “ทำ action อะไรได้”
Policy หรือ Business Rule
Policy คือกติกาเพิ่มเติมที่แม้มี permission แล้วก็ยังอาจทำไม่ได้ เช่น
- approve refund ได้เฉพาะรายการที่อยู่สถานะ
pending_review - ดูเอกสารได้เฉพาะ tenant ของตัวเอง
- แก้ role คนอื่นได้ แต่ห้ามแก้ role คนที่สูงกว่าตัวเอง
- download เอกสารได้เฉพาะช่วงเวลาที่ลิงก์ยังไม่หมดอายุ
ตรงนี้สำคัญมาก เพราะหลายระบบพยายามยัดทุกอย่างลง role หรือ permission จนกลายเป็นโครงสร้างที่อธิบายยาก
RBAC ที่ดีควรตอบคำถามอะไรได้บ้าง
ก่อนออกแบบ permission model ควรถามให้ชัดว่าระบบของคุณต้องตอบคำถามอะไร
ไม่ใช่แค่ “ใครเข้าเมนูไหนได้” แต่รวมถึง
- ใครอ่าน resource นี้ได้
- ใครแก้ไขได้
- ใครอนุมัติได้
- ใคร export ได้
- ใครจัดการสิทธิ์ผู้อื่นได้
- ใครทำได้เฉพาะข้อมูลของทีมตัวเอง
- ใครทำได้เฉพาะช่วงสถานะบางช่วง
- ถ้ามี incident เราตรวจย้อนหลังได้ไหมว่าใครใช้สิทธิ์อะไรไป
ถ้าระบบตอบคำถามเหล่านี้ไม่ได้ชัด แปลว่าโครงสร้างสิทธิ์ยังไม่พร้อมโต
วิธีคิดที่ปลอดภัยกว่า: ออกแบบจาก action ไม่ใช่จากหน้าเมนู
ข้อผิดพลาดที่เจอบ่อยคือเริ่มจาก “หน้าไหนให้ใครเข้าได้” แล้วค่อยพยายามผูกกับ role
วิธีนี้ใช้ได้แค่ระดับ UI แต่ไม่พอสำหรับระบบจริง เพราะสิทธิ์ที่แท้จริงไม่ได้อยู่ที่เมนู แต่อยู่ที่ action กับ resource
ตัวอย่างที่ควรคิดแทนคือ
- ใครอ่าน customer profile ได้
- ใครแก้ customer profile ได้
- ใคร approve refund ได้
- ใคร download contract ได้
- ใครเปลี่ยน role ผู้ใช้อื่นได้
เมื่อคิดจาก action กับ resource ก่อน เราจะเริ่มเห็น permission model ที่ชัดขึ้น เช่น
customer.readcustomer.updaterefund.readrefund.approverefund.rejectdocument.downloaduser.role.update
จากนั้นค่อยเอา permission ไปจัดกลุ่มเป็น role อีกที
ตัวอย่างการแตก permission ที่ดีขึ้น
สมมติระบบมี workflow การคืนเงิน ถ้าใช้ role แบบหยาบเกินไปอาจเป็นแบบนี้
adminทำได้ทั้งหมดfinanceทำได้ทั้งหมดเกี่ยวกับ refundsupportดูได้บางอย่าง
แต่ถ้าจะออกแบบให้โตได้ดีกว่า ควรแตก permission ให้ชัด เช่น
refund.readrefund.createrefund.reviewrefund.approverefund.rejectrefund.export
แล้วค่อยบอกว่า role ใดได้ permission ชุดไหน เช่น
supportได้refund.readfinance_reviewerได้refund.read,refund.reviewfinance_approverได้refund.approve,refund.rejectadminได้ชุดใหญ่กว่า แต่ยังควรถูก audit ทุกครั้ง
แบบนี้เวลา workflow โตขึ้น คุณจะเพิ่ม permission เฉพาะจุดได้ โดยไม่ต้องบิด role เดิมจนเสียความหมาย
Scope สำคัญพอ ๆ กับ Permission
สิทธิ์ที่ดีไม่ได้จบที่คำว่า “ทำได้หรือไม่ได้” แต่รวมถึง “ทำได้กับอะไร”
ตัวอย่างเช่น ผู้ใช้สองคนอาจมี permission เดียวกันคือ customer.read แต่ scope ไม่เหมือนกัน
คนหนึ่งอ่านได้เฉพาะลูกค้าใน tenant ของตัวเอง
อีกคนอ่านได้เฉพาะ region ของตัวเอง
อีกคนอ่านได้ทุก tenant เพราะเป็น internal audit
อีกคนอ่านได้เฉพาะ resource ที่ตัวเองเป็น owner
นี่คือเหตุผลที่หลายระบบมี permission แล้วแต่ยังพังอยู่ เพราะ permission ไม่มี scope
ดังนั้นในการออกแบบจริง ควรคิดร่วมกันเสมอว่า permission นี้มีขอบเขตอย่างไร เช่น
- own
- team
- tenant
- region
- all
เมื่อ scope ชัดขึ้น โค้ด authorization ก็จะสื่อความหมายมากขึ้นและเสี่ยงน้อยลง
เมื่อไร RBAC เพียงพอ และเมื่อไรต้องมี ABAC หรือ policy layer เพิ่ม
RBAC เหมาะมากกับระบบส่วนใหญ่ในระยะเริ่มต้นถึงกลาง เพราะเข้าใจง่ายและคุยกับทีมธุรกิจได้ตรงพอสมควร แต่เมื่อกติกาเริ่มขึ้นกับ attribute จำนวนมาก เช่น location, tenant, state, time window, ownership หรือ approval chain อย่างเดียว RBAC มักไม่พอ
ตัวอย่างเช่น
- ผู้ใช้เป็น finance role จริง แต่ approve ได้เฉพาะ invoice ของบริษัทลูกบางแห่ง
- support อ่านเอกสารได้เฉพาะเคสที่ตัวเองรับผิดชอบ
- manager อนุมัติได้เฉพาะวงเงินต่ำกว่าเพดานของตัวเอง
- document download ต้องห้ามเมื่อ ticket ถูกปิดแล้ว
กรณีแบบนี้ RBAC ยังใช้ได้เป็นฐาน แต่ต้องมี policy layer หรือ rule layer ช่วยตัดสินใจต่อ ไม่อย่างนั้น role จะถูกยัดเงื่อนไขจนยุ่งเกินไป
โครงสร้างที่ควรมีในระบบจริง
อย่างน้อยระบบ permission ที่โตได้ควรมีสามชั้นนี้
ชั้นแรกคือ roles
ชั้นที่สองคือ permissions
ชั้นที่สามคือ authorization checks ที่ดู context จริงของ request
ถ้าระบบมีแค่ role โดยไม่มี permission แยก จะเริ่มแก้ยากเร็วมาก
ถ้าระบบมี permission แต่ไม่มี context check ก็อาจให้สิทธิ์กว้างเกินไป
ถ้าระบบมี context check แต่ไม่ถูก audit เวลามี incident จะไล่ย้อนหลังยาก
ตัวอย่าง schema แบบเริ่มต้น
ตัวอย่างนี้เป็นโครงขั้นต่ำที่พาไปต่อได้ดีกว่าการ hardcode role ตรง ๆ
create table roles (
id bigserial primary key,
code text not null unique,
name text not null
);
create table permissions (
id bigserial primary key,
code text not null unique,
name text not null
);
create table role_permissions (
role_id bigint not null references roles(id),
permission_id bigint not null references permissions(id),
primary key (role_id, permission_id)
);
create table user_roles (
user_id bigint not null,
role_id bigint not null references roles(id),
primary key (user_id, role_id)
);
ถ้าระบบมี multi-tenant หรือมี role ต่อองค์กร อาจต้องเพิ่ม field เช่น
tenant_idscope_typescope_idexpires_at
เพื่อให้สิทธิ์ไม่ได้ลอยอยู่ทั้งระบบโดยไม่มีบริบท
ตัวอย่าง Express/Node.js แบบเริ่มต้น
ตัวอย่างนี้ไม่ได้พยายามสร้างระบบ authorization ครบทุกอย่าง แต่ตั้งใจให้เห็นหลักสำคัญคือแยก role ออกจาก permission และเช็ก permission จาก action จริง
const express = require("express");
const app = express();
app.use(express.json());
const rolePermissions = {
admin: [
"customer.read",
"customer.update",
"refund.read",
"refund.approve",
"user.role.update"
],
support: [
"customer.read",
"refund.read"
],
finance: [
"refund.read",
"refund.approve"
]
};
app.post("/refunds/:refundId/approve", async (req, res) => {
try {
const user = await getCurrentUser(req);
if (!hasPermission(user, "refund.approve")) {
return res.status(403).json({
error: "Forbidden"
});
}
const refund = await getRefundById(req.params.refundId);
if (!refund) {
return res.status(404).json({
error: "Refund not found"
});
}
if (refund.status !== "pending_review") {
return res.status(409).json({
error: "Refund is not in approvable state"
});
}
await approveRefund(refund.id, user.id);
return res.status(200).json({
success: true,
refundId: refund.id,
status: "approved"
});
} catch (error) {
console.error("Approve refund failed:", error);
return res.status(500).json({
error: "Approve refund failed"
});
}
});
function hasPermission(user, permission) {
const permissions = new Set();
for (const role of user.roles) {
const assigned = rolePermissions[role] || [];
for (const item of assigned) {
permissions.add(item);
}
}
return permissions.has(permission);
}
async function getCurrentUser(req) {
return {
id: "user_101",
roles: ["finance"]
};
}
async function getRefundById(refundId) {
return {
id: refundId,
status: "pending_review"
};
}
async function approveRefund(refundId, actorId) {
console.log(`Refund ${refundId} approved by ${actorId}`);
}
app.listen(3000, () => {
console.log("Server listening on port 3000");
});
โค้ดชุดนี้กำลังช่วยอะไร
จุดสำคัญของตัวอย่างนี้ไม่ใช่ความซับซ้อนของโค้ด แต่คือการแยกชั้นความหมายให้ชัดขึ้น
เราไม่ได้ถามว่า user เป็น finance ไหมแล้วจบ แต่ถามว่า user มี refund.approve หรือไม่
จากนั้นจึงค่อยตรวจต่อว่า refund นั้นอยู่ในสถานะที่อนุมัติได้หรือไม่
นี่คือความต่างระหว่าง permission กับ business rule
- permission ตอบว่า “คุณมีสิทธิ์ทำ action นี้ไหม”
- business rule ตอบว่า “resource นี้อยู่ในสภาพที่ action นี้ทำได้ไหม”
ถ้าสองอย่างนี้ปนกัน ระบบจะเริ่มอ่านยากและแก้ลำบากมาก
สิ่งที่ควรหลีกเลี่ยง
สิ่งที่ทำให้ระบบ permission พังเร็ว มักไม่ใช่เพราะเลือกแนวคิดผิดตั้งแต่แรก แต่เพราะปล่อยให้ shortcut สะสมเรื่อย ๆ
อย่างแรกคือการ hardcode role เต็มไปหมดใน controller, frontend, service และ job worker พร้อมกัน
อย่างที่สองคือการใช้ชื่อ role เป็นตัวแทน business process มากเกินไป เช่น super_admin_final หรือ manager_v2
อย่างที่สามคือการใช้เมนูฝั่ง frontend เป็นตัวตัดสินสิทธิ์ ทั้งที่ backend ไม่ได้ enforce จริง
อย่างที่สี่คือการเพิ่ม role ใหม่ทุกครั้งที่มีข้อยกเว้น แทนที่จะกลับมาแตก permission หรือเพิ่ม policy layer
ถ้าระบบเริ่มมี role แปลก ๆ มากขึ้นเรื่อย ๆ นั่นมักเป็นสัญญาณว่าการออกแบบกำลังหนีปัญหา ไม่ได้แก้ปัญหา
RBAC กับ Audit Trail ต้องมาคู่กัน
ระบบที่มีการเปลี่ยนสิทธิ์หรือมี action สำคัญตาม role ไม่ควรมีแค่ authorization check แต่ควรมี audit trail ด้วย
เหตุผลไม่ใช่แค่เพื่อ compliance แต่เพื่อให้ตอบคำถามย้อนหลังได้จริง เช่น
ใครเป็นคนเพิ่ม role ให้ user นี้
ใคร approve refund รายการนี้
ใครใช้สิทธิ์ export ข้อมูล
ใครแก้ permission mapping ของ role สำคัญ
ถ้าระบบมี RBAC แต่ไม่มีหลักฐานย้อนหลัง เวลามี incident หรือ dispute จะตอบคำถามยากมาก
RBAC กับ Incident Response เกี่ยวกันยังไง
เวลามี incident เรื่องสิทธิ์ ปัญหามักไม่ใช่แค่ “ใครเข้าไม่ได้” แต่รวมถึง “ใครเข้าได้เกินกว่าที่ควร”
เช่น
- user ธรรมดาเห็นข้อมูลข้าม tenant
- support ดาวน์โหลดเอกสารที่ไม่ควรเห็น
- role mapping ถูก deploy ผิด
- admin ให้สิทธิ์ชั่วคราวแล้วลืมเอาออก
ถ้าระบบไม่มีโครงสร้าง permission ที่ชัดและไม่มี logging ที่ผูกกับ request context การไล่ incident ประเภทนี้จะช้ามาก เพราะแยกไม่ออกว่า bug มาจาก role mapping, policy logic หรือ data scope
Request ID สำคัญกับ Authorization มากกว่าที่คิด
เวลาตามปัญหาสิทธิ์ย้อนหลัง การมี request_id หรือ correlation_id ช่วยมาก เพราะคุณจะเชื่อมข้อมูลหลายจุดเข้าด้วยกันได้
- request log
- application error
- audit trail
- permission evaluation
- downstream service calls
ถ้าไม่มี request context เดียวกัน เวลาหาสาเหตุว่าทำไม request หนึ่งถูก allow หรือ deny จะยากขึ้นมาก
รีวิวแนวทางนี้แบบ production-minded
Correctness
แนวทางที่แยก role, permission และ business rule ออกจากกัน จะช่วยให้การตัดสินสิทธิ์ชัดขึ้นและลดการให้สิทธิ์เกินโดยไม่ตั้งใจ
Security
permission model ที่ดีช่วยลดทั้ง accidental overreach และ privilege creep แต่ต้องมี backend enforcement จริง ไม่ใช่แค่ซ่อนเมนูใน frontend
Efficiency
RBAC ที่ออกแบบดีตั้งแต่ต้นช่วยลดต้นทุนการเพิ่ม feature ใหม่ เพราะคุณเพิ่ม permission เป็นหน่วยเล็ก ๆ ได้ แทนที่จะต้องแตก role ใหม่ตลอดเวลา
Error handling
ระบบจริงควรแยกให้ชัดว่า request ล้มเหลวเพราะ authentication, authorization, resource state หรือ policy mismatch และควร log ให้ตามย้อนหลังได้
Checklist สั้น ๆ ก่อนปล่อยระบบสิทธิ์ขึ้น production
- แยก
roleออกจากpermission - ไม่ใช้ role ชื่อเดียวแทนหลายความหมายเกินไป
- ออกแบบสิทธิ์จาก
action + resource - มี scope ของสิทธิ์ชัดเจนเมื่อจำเป็น
- backend เป็นผู้ enforce สิทธิ์จริง
- permission check แยกจาก business rule
- การเปลี่ยน role หรือ permission ถูก audit
- มี request id หรือ correlation id สำหรับตามย้อนหลัง
- มีวิธีทดสอบ role mapping และ edge cases
- ไม่มีการ hardcode role กระจายเต็มระบบโดยไม่ควบคุม
บทความที่ควรอ่านต่อ
- Audit Trail คืออะไร และต่างจาก Activity Log ยังไงในระบบจริง
- Incident Response Runbook สำหรับทีมเล็ก ควรมีอะไรบ้าง
- Slack Bot สำหรับ approval workflow ควรออกแบบยังไงไม่ให้มั่ว
- Request ID และ Correlation ID คืออะไร และช่วย debug production ยังไง
สรุป
RBAC ไม่ได้ยากเพราะแนวคิดซับซ้อน แต่ยากเพราะระบบจริงมีข้อยกเว้น มีหลาย workflow และมีแรงกดดันให้แก้เร็วด้วย shortcut ตลอดเวลา
ถ้าคุณเริ่มจาก role แบบหยาบเกินไป แล้วใช้ชื่อ role แทนทุกอย่าง สุดท้ายสิทธิ์จะเริ่มซ้อนกัน มั่วขึ้น และยากต่อการตรวจสอบ แต่ถ้าแยก role, permission และ policy ออกจากกันตั้งแต่ต้น ระบบจะโตต่อได้ง่ายกว่าและเสี่ยงพังน้อยกว่า
สรุปสั้นที่สุดคือ
role ใช้บอกบทบาท, permission ใช้บอกสิทธิ์, policy ใช้ตัดสินตามบริบทจริง