ทำความรู้จัก Casbin และทำไมมันถึงทำให้มือใหม่ปวดหัว
ถ้าคุณกำลังทำระบบที่มีเรื่องสิทธิ์การเข้าถึง เช่น แอดมิน (ผู้ดูแลระบบ) ทำอะไรได้บ้าง หรือ ผู้ใช้งานทั่วไป ห้ามเข้าหน้าไหน Casbin คือเครื่องมือที่ช่วยจัดการเรื่องนี้แบบจบในที่เดียว มันเป็น Library (ชุดคำสั่งสำเร็จรูป) ที่ใช้จัดการ Authorization (การตรวจสอบว่าใครมีสิทธิ์ทำอะไร) โดยแยกกฎเกณฑ์ออกจากโค้ดหลักของเรา
หัวใจของมันอยู่ที่ไฟล์ model.conf ซึ่งทำหน้าที่เป็นสมุดกฎกติกาของระบบ เหมือนกับกฎของหมู่บ้านที่บอกว่าใครเข้าซอยไหนได้บ้าง ส่วน policy คือข้อมูลจริงว่าใครเป็นใครในหมู่บ้านนั้น ความเจ๋งคือมันรองรับแทบทุกภาษาโปรแกรม แต่ความน่ากลัวคือถ้าคุณเขียนกฎผิดแม้แต่นิดเดียว ระบบจะตอบกลับมาแค่ false (ค่าความเท็จ) โดยไม่บอกว่าผิดที่บรรทัดไหน
การที่มันเงียบใส่แบบนี้แหละที่ทำให้นักพัฒนาหน้าใหม่หลายคนท้อจนอยากเลิกใช้ มือใหม่ส่วนใหญ่มักจะติดหล่มกับการตั้งค่าไฟล์ model.conf จนเสียเวลาแก้บั๊กข้ามคืน วันนี้เราจะมาเจาะลึก 5 ข้อผิดพลาดที่คนเจอบ่อยที่สุด พร้อมวิธีแก้เพื่อให้คุณเข้าใจมันได้ง่ายขึ้น
1. สลับลำดับใน Request และ Policy จนโปรแกรมหาค่าไม่เจอ
ความผิดพลาดอันดับหนึ่งคือการเรียงลำดับตัวแปรไม่ตรงกัน Casbin ต้องการให้คุณนิยาม Request Definition (รูปแบบของคำร้องขอ) และ Policy Definition (รูปแบบของกฎที่ตั้งไว้) ให้เป๊ะ ถ้าคุณสลับตำแหน่งผิด ผลลัพธ์ที่ได้จะเป็น false ตลอดกาลเพราะเครื่องมือมองว่ามันคือคนละเรื่องกัน
ลองนึกภาพว่าคุณเขียนกฎว่า คน, หน้าที่, สิ่งของ แต่เวลาส่งคำขอจริงคุณดันส่งไปว่า คน, สิ่งของ, หน้าที่ เครื่องมือจะพยายามเอาคนไปเทียบกับคน (ผ่าน) เอาหน้าที่ไปเทียบกับสิ่งของ (ไม่ผ่าน) ทำให้การตรวจสอบสิทธิ์ล้มเหลวโดยที่คุณไม่รู้ตัวเลยว่ามันผิดที่บรรทัดการนิยาม
กฎเหล็กคือต้องเรียงลำดับให้เหมือนกันทุกจุด ทั้งในส่วนของ request_definition, policy_definition และ matchers (ตัวเปรียบเทียบเงื่อนไข) เพื่อให้ระบบมองเห็นภาพเดียวกันเสมอ
# ใน model.conf
[request_definition]
r = sub, obj, act # ผู้ใช้, สิ่งของ, การกระทำ
[policy_definition]
p = sub, obj, act # ผู้ใช้, สิ่งของ, การกระทำ
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
ในตัวอย่างนี้ sub คือผู้ใช้งาน obj คือทรัพยากร และ act คือสิ่งที่ทำ หากคุณส่งคำสั่ง e.Enforce("alice", "data1", "read") ระบบจะนำไปเทียบกับกฎใน policy ที่ต้องเรียงลำดับแบบเดียวกันเป๊ะ หากใน policy เขียนว่า p, alice, read, data1 แบบนี้จะไม่มีวันตรงกันครับ
ผลลัพธ์ที่ควรได้คือ ถ้าข้อมูลใน policy ตรงกับ alice, data1, read ระบบจะคืนค่า true (ค่าความจริง) ออกมา แต่ถ้าเรียงสลับกัน ผลลัพธ์จะเป็น false ทันทีครับ
2. เข้าใจผิดว่า g() คือเวทมนตร์เสกสิทธิ์ให้เอง
มือใหม่หลายคนมักใส่ g = _, _ ลงใน role_definition แล้วคิดว่าทุกอย่างจะทำงานเอง แต่นั่นเป็นความเข้าใจที่ผิดครับ ฟังก์ชัน g() ย่อมาจาก Group (กลุ่ม) ซึ่งทำหน้าที่ตรวจสอบการสืบทอดสิทธิ์ เช่น ถ้า admin เข้าถึงข้อมูลได้ และ alice เป็น admin ดังนั้น alice ต้องเข้าถึงข้อมูลได้ด้วย
ปัญหาคือ g() ไม่รู้ว่าใครอยู่ในกลุ่มไหนถ้าคุณไม่บอกมันในไฟล์ policy คุณต้องระบุความสัมพันธ์ให้ชัดเจนว่าคนนี้สังกัดกลุ่มไหน การใส่แค่บรรทัดนิยามใน model.conf ไม่เพียงพอ เพราะนั่นเป็นเพียงการบอกว่า "เราจะมีกลุ่มนะ" แต่ไม่ได้บอกว่า "ใครอยู่กลุ่มไหน"
คุณต้องเพิ่มข้อมูลความสัมพันธ์ลงในไฟล์ policy เพื่อให้ g() ทำงานได้จริง ถ้าไม่มีข้อมูลนี้ ระบบจะมองว่าไม่มีใครมีสิทธิ์ในกลุ่มนั้นเลย และแน่นอนว่ามันจะตอบ false ตามระเบียบ
# ใน model.conf
[role_definition]
g = _, _
# ใน policy
p, admin, data1, read
g, alice, admin # บรรทัดนี้สำคัญมาก: บอกว่า alice คือ admin
บรรทัด g, alice, admin คือตัวเชื่อมที่บอกว่า alice มีสิทธิ์เหมือนกับ admin ทุกประการ ถ้าคุณลืมบรรทัดนี้ไป แม้ alice จะเป็น admin จริงๆ ระบบก็จะไม่ยอมให้เธอเข้าถึง data1 อยู่ดี
ผลลัพธ์ที่ควรได้คือ เมื่อเรียก e.Enforce("alice", "data1", "read") ระบบจะตรวจพบว่า alice เป็นสมาชิกของ admin และ admin มีสิทธิ์อ่าน data1 ทำให้ผลตอบกลับเป็น true ครับ
3. ปวดหัวกับ Policy Effect ที่ไม่เป็นดั่งใจ
Policy Effect (ผลลัพธ์ของกฎ) คือส่วนที่บอก Casbin ว่าถ้าเจอหลายกฎที่ขัดแย้งกัน ควรทำอย่างไร เช่น ถ้ามีกฎให้อ่านได้ แต่มีกฎอีกบรรทัดบอกว่าห้ามอ่าน ระบบจะฟังใคร? หลายคนมักจะปล่อยผ่านค่าเริ่มต้นจนเจอปัญหาเมื่อระบบเริ่มซับซ้อนขึ้น
ค่าเริ่มต้นมักจะเป็น allow-override (อนุญาตเป็นหลัก) ซึ่งบางทีอาจไม่ตอบโจทย์ถ้าคุณต้องการทำระบบที่ "ห้ามไว้ก่อน" (Deny) หรือระบบที่มีลำดับความสำคัญ คุณต้องรู้จักใช้คำสั่งอย่าง priority หรือ deny เพื่อควบคุมทิศทางของสิทธิ์
การเขียน policy_effect ให้ชัดเจนจะช่วยให้คุณไม่ต้องคอยลบหรือเพิ่มกฎซ้ำซ้อนใน policy เพียงแค่ระบุเงื่อนไขให้ฉลาดขึ้น ระบบก็จะจัดการเรื่องการอนุญาตหรือปฏิเสธให้เองโดยอัตโนมัติ
# ใช้ priority เพื่อให้กฎปฏิเสธ (deny) มีความสำคัญสูงสุด
[policy_effect]
e = priority(p.eft) || deny
ในตัวอย่างนี้ ถ้าคุณมีกฎ allow (อนุญาต) อยู่หลายบรรทัด แต่มีกฎ deny (ปฏิเสธ) โผล่มาเพียงบรรทัดเดียว ระบบจะยึดตามกฎ deny ทันที เพราะคำสั่ง priority ให้ความสำคัญกับความปลอดภัยสูงสุด
ผลลัพธ์ที่ควรได้คือ หากมีการตั้งค่า p, alice, data1, read, allow และ p, alice, data1, read, deny ผลลัพธ์สุดท้ายที่ได้จะเป็น false เสมอเพราะโดนปฏิเสธทับไว้ครับ
4. ลืมใส่ข้อมูล Role Graph แล้วงงว่าทำไมพัง
หลายคนชอบพลาดเรื่องการเชื่อมโยงข้อมูลในกราฟความสัมพันธ์ หรือ Role Graph (โครงสร้างกลุ่ม) คุณอาจจะนิยามสิทธิ์ไว้ดีแล้ว แต่ถ้าคุณลืมระบุว่าใครสังกัดกลุ่มไหนในฐานข้อมูล policy ทุกอย่างที่วิ่งผ่าน g() จะพังพินาศทันที
สิ่งนี้มักเกิดขึ้นตอนที่คุณย้ายจากระบบทดสอบไปยังระบบจริง หรือตอนที่คุณเปลี่ยนวิธีจัดการสิทธิ์จากรายบุคคลมาเป็นรายกลุ่ม คุณต้องมั่นใจว่าทุกครั้งที่เพิ่ม role ใหม่เข้าไป คุณต้องมีข้อมูลอัปเดตใน policy เสมอ ไม่เช่นนั้นมันก็เป็นแค่กฎที่ไม่มีคนใช้งาน
ลองเช็คดูว่าใน policy ของคุณมีบรรทัดที่ขึ้นต้นด้วย g, ครบถ้วนหรือไม่ การมองข้ามจุดนี้ไปคือสาเหตุที่พบบ่อยที่สุดที่ทำให้โปรแกรมเมอร์มือใหม่นั่งงงว่า "ทำไมฉันเขียนกฎไว้แล้ว แต่ระบบไม่ยอมให้ผ่าน"
# แบบที่ผิด: ลืมระบุความสัมพันธ์
p, admin, /api/v1, GET
# ผลลัพธ์: alice เข้าถึงไม่ได้ เพราะระบบไม่รู้ว่าเธอคือ admin
# แบบที่ถูก: ใส่ความสัมพันธ์ให้ครบ
p, admin, /api/v1, GET
g, alice, admin
บรรทัด g, alice, admin ทำหน้าที่ผูก alice เข้ากับ admin ทำให้เมื่อเรียกใช้สิทธิ์ของ admin ตัว alice จะได้รับสิทธิ์นั้นไปด้วยทันที
ผลลัพธ์ที่ควรได้คือ เมื่อรันคำสั่งตรวจสอบสิทธิ์สำหรับ alice ระบบจะวิ่งไปดูที่ g และพบว่าเธอคือ admin จึงอนุญาตให้เข้าถึง /api/v1 ได้สำเร็จครับ
5. ติดกับดัก Cache ไม่ยอมอัปเดต
ข้อนี้อาจไม่ใช่ปัญหาที่ model.conf โดยตรง แต่เป็นปัญหาที่มือใหม่มักเจอเวลาใช้ Casbin ร่วมกับ Caching (หน่วยความจำชั่วคราว) เพื่อเพิ่มความเร็ว ระบบพวกนี้จะจำผลลัพธ์ไว้ ถ้าคุณอัปเดต policy แต่ลืมล้าง cache ผลลัพธ์เก่าก็จะค้างอยู่แบบนั้น
เมื่อคุณเปลี่ยนกฎในไฟล์ policy แล้วรันโปรแกรมใหม่ แต่ผลลัพธ์ยังเป็นค่าเดิม ให้สันนิษฐานไว้ก่อนว่า cache ยังไม่ถูกอัปเดต มือใหม่หลายคนเสียเวลาแก้ไฟล์ model.conf ไปหลายชั่วโมง ทั้งที่จริงๆ แล้วกฎที่เขียนไว้นั้นถูกต้องตั้งแต่แรกแล้ว
วิธีแก้คือตรวจสอบว่าคุณใช้ SyncedCachedEnforcer (เครื่องมือตรวจสอบสิทธิ์พร้อมระบบจำค่า) หรือไม่ ถ้าใช่ คุณต้องสั่ง LoadPolicy() หรือล้าง cache ทุกครั้งที่มีการเปลี่ยนแปลงข้อมูล เพื่อให้แน่ใจว่าระบบกำลังใช้กฎชุดล่าสุดอยู่
// ตัวอย่างโค้ดในภาษา Go เพื่อโหลด policy ใหม่
e.LoadPolicy() // สั่งให้ Casbin โหลดกฎใหม่จากฐานข้อมูลหรือไฟล์
การเรียกใช้ LoadPolicy() จะเป็นการบังคับให้ Casbin ทิ้งค่าเก่าในหน่วยความจำและอ่านกฎใหม่ทั้งหมดเข้ามาแทนที่ เพื่อให้การตรวจสอบสิทธิ์มีความถูกต้องและเป็นปัจจุบัน
ผลลัพธ์ที่ควรได้คือ หลังจากรันคำสั่ง LoadPolicy() แล้ว ระบบจะเริ่มใช้กฎชุดใหม่ที่อัปเดตล่าสุด ทำให้การเรียกใช้งาน Enforce ได้ผลลัพธ์ที่ถูกต้องตามที่คุณต้องการครับ
สรุป: วิธีจัดการ Casbin แบบโปร
ปัญหาของ Casbin ไม่ใช่เพราะมันยาก แต่เพราะมันเป็นระบบที่เข้มงวดกับความถูกต้องสูงมาก หากคุณติดขัด ให้เริ่มจากตรวจสอบ ลำดับตัวแปร ในไฟล์ model.conf ให้ตรงกันก่อนเป็นอันดับแรก จากนั้นค่อยเช็คเรื่อง ความสัมพันธ์ของกลุ่ม ใน policy ว่าครบถ้วนไหม
ลองทำตามสถานการณ์นี้ดูครับ: สมมติว่าคุณกำลังทำระบบจัดการบทความ ถ้าคุณอยากให้ editor (บรรณาธิการ) แก้ไขบทความได้ทั้งหมด แต่ author (ผู้เขียน) แก้ได้เฉพาะบทความของตัวเอง ให้คุณนิยาม p = sub, obj, act และใช้ g เพื่อกำหนดกลุ่ม จากนั้นทดสอบด้วยการรันคำสั่งสั้นๆ 1-2 บรรทัดก่อนเสมอ อย่าเพิ่งเขียนกฎยาวๆ ทั้งหมดลงไปในทีเดียว
การเป็นโปรแกรมเมอร์ที่เก่งไม่ได้แปลว่าต้องจำทุกอย่างได้ แต่คือการรู้ว่าเมื่อเกิด false ขึ้นมา เราต้องไล่เช็คจากตรงไหน การทำความเข้าใจพื้นฐานเรื่อง model.conf และ policy จะเป็นสกิลติดตัวที่ทำให้คุณจัดการระบบความปลอดภัยได้มั่นใจขึ้นไปอีกระดับครับ
ที่มา: Stuck on Casbin's model.conf? 5 mistakes beginners hit most — DEV Community