Private Docs

API Restriction · 02 — ให้ endpoint บอกว่า "ต้องทำอะไรได้" ไม่ใช่ "ใครเรียกได้"

แผน: เลิกเขียนชื่อ app หรือชื่อ role ลงใน endpoint แล้วย้ายคำตอบว่าใครเรียกได้ไปอยู่ในตาราง Permission ที่มีอยู่แล้ว — app ใหม่มา plug-in แค่ insert row

อัปเดต: 2026-09-07

⬅️ ที่มาของงานนี้ → 00 — ปัญหา มติที่เสนอ และตัวเลือกที่ตกรอบ 🔗 อ่านคู่กัน → 01 — role ที่ผูกกับ app ที่เรียกจริง

02. ให้ endpoint บอกว่า “ต้องทำอะไรได้” ไม่ใช่ “ใครเรียกได้”

🔵 หน้านี้คือข้อเสนอ ยังไม่ใช่มติ และยังไม่มีโค้ด

ปัญหา — ดูจากตัวอย่างเดียว

/users/me อยู่ที่ User Service ฝั่ง Platform แต่ FX ก็เรียก และอนาคตจะมี app อื่นเรียกอีก

ถ้าเราเขียนแบบนี้:

[Authorize(Roles = "PlatformUser, FxUser")]      // ❌
public async Task<IActionResult> GetMe() { ... }

พอมี app ที่ 3 เข้ามา ต้องกลับมาแก้บรรทัดนี้ แล้ว deploy ใหม่ · มี 10 endpoint แบบนี้ ก็แก้ 10 ที่ · มีอีก 13 service ก็ทวีคูณไปอีก

ไอเดีย — เปลี่ยนสิ่งที่เขียนบน endpoint

เขียนแบบนี้แทน:

[RequirePermission("users:self:read")]           // ✅
public async Task<IActionResult> GetMe() { ... }

บรรทัดนี้แปลว่า “ใครก็ตามที่ทำ users:self:read ได้ เรียกเส้นนี้ได้” — ไม่พูดถึง app ไหน ไม่พูดถึง role ชื่ออะไร เขียนครั้งเดียวตอนสร้าง endpoint แล้วไม่ต้องกลับมาแก้อีก

คำตอบว่า “ใครทำได้บ้าง” ย้ายไปอยู่ใน ข้อมูล — ตาราง RolePermission ที่มีอยู่แล้วในระบบ

app ใหม่มา plug-in ต้องทำอะไร

สมมติมี app ใหม่ชื่อ Trade เข้ามา และต้องใช้ /users/me:

เดิมใหม่
แก้ [Authorize(Roles = "…, TradeUser")] ในทุก endpoint ที่เกี่ยวไม่ต้องแตะโค้ด
bump Backend_Package + redeploy service ที่เกี่ยวไม่ต้อง deploy
รอรอบ releaseadmin insert row ได้เลย

สิ่งที่ admin ทำคือ: สร้าง App Trade → สร้าง Role TradeUser ใต้ AppId ของ Trade → ผูก RolePermission ชี้ไปที่ users:self:read ที่ Platform เป็นเจ้าของอยู่แล้ว

ไล่ทีละขั้นตอน

sequenceDiagram
    autonumber
    participant FE as หน้าจอ FX
    participant MW as middleware กลาง
    participant R as Redis UserInfo
    participant EP as GET /users/me

    FE->>MW: เรียก /users/me + header AppId = FX
    MW->>R: ดึง UserInfo ของ user คนนี้
    R-->>MW: apps ทั้งหมด พร้อม role และ permission ของแต่ละ app
    MW->>MW: หยิบเฉพาะ app FX (ตามข้อ 1) ไม่เจอ ให้ 401
    MW->>EP: ส่งต่อ พร้อม permission ของ user ใน FX
    EP->>EP: เส้นนี้ต้องการ users:self:read ของเจ้าของคือ User Service
    alt user มี users:self:read ที่เจ้าของตรงกัน
        EP-->>FE: 200 พร้อมข้อมูล
    else ไม่มี
        EP-->>FE: 403
    end

กติกา 3 ข้อที่ทำให้มันไม่รั่ว

1. permission ต้องมาจาก app ที่เลือกเท่านั้น ไม่ใช่รวมทุก app — ถ้าไม่ทำข้อนี้ Platform-Admin ที่เรียกผ่าน FX จะได้ permission ของ Platform ติดมาด้วย ซึ่งเป็นปัญหาเดิมที่แค่ย้ายลงไปอีกชั้น ⇒ ใน Redis blob ต้องเก็บ permission แยกใต้แต่ละ app ไม่ใช่กองรวมกันไว้ที่เดียว

2. ต้องรู้ว่า permission นั้น “ใครเป็นเจ้าของ” — ในระบบวันนี้ชื่อ permission unique แค่ต่อ AppId (PermissionsConfiguration.cs:73-75) แปลว่า users:self:read ของ Platform กับของ FX เป็นคนละแถวได้ ⇒ เวลาเทียบ ต้องเทียบทั้งชื่อและเจ้าของ

3. เจ้าของที่ endpoint ใช้เทียบ มาจาก config ของ service ไม่ใช่จาก header — ตรงนี้สำคัญ เพราะแปลว่าต่อให้ client ปลอม header มา ก็ขยับฝั่งขวาของการเทียบไม่ได้

ของที่มีอยู่แล้ว ไม่ต้องสร้างใหม่

ตรวจกับ Backend_UserService@25b1923 แล้ว:

  • ตาราง Permissions และ RolePermission มีอยู่ ใช้งานอยู่
  • ชื่อ permission ถูกประกอบเป็น {resource}:{action} ให้อัตโนมัติที่ Permissions.cs:33-56 ⇒ รูปแบบ users:self:read เป็นข้อตกลงที่ระบบใช้อยู่แล้ว ไม่ใช่ของที่เราคิดใหม่
  • ที่สำคัญที่สุด: read path ปัจจุบันยอมให้ role ของ app หนึ่ง ผูกกับ permission ที่อีก app เป็นเจ้าของได้อยู่แล้วGetRolePermMenuShellHandler.cs filter role ตาม AppId ที่เรียก แต่ตอนดึง permission ไม่ได้ filter ตาม Permission.AppId ⇒ ประโยค “app ใหม่ = insert row” เป็นจริงตามตัวอักษร ไม่ต้องแก้ query

ต้องเขียนอะไรเพิ่มบ้าง — 3 ชิ้น

  1. [RequirePermission("…")] — attribute ตัวใหม่ใน Backend_Package
  2. ตัวตรวจAuthorizationHandler ที่อ่าน permission จาก HttpContext.GetUserInfo() (ข้อมูลที่ middleware ดึงมาแล้ว ไม่ต้องยิง DB ต่อ request) + IAuthorizationPolicyProvider เพื่อไม่ต้องประกาศ policy ทุกชื่อไว้ล่วงหน้า
  3. เพิ่ม permission ลง Redis blob — แก้ที่ UsersMapper.ToUserInfoForRedis ใน Backend_UserService

🔴 การเพิ่ม field ต้องเป็นแบบ optional และห้ามแตะ CurrentSchemaVersion — repo นี้ทำแบบนี้มาแล้ว 4 ครั้ง (cardId, passportNo, custCode, amloStatus) และมี comment กำกับห้ามไว้ทุกจุด เหตุผลอยู่ใน 01

ข้อควรระวังตอน deploy: blob เก่าที่ยังอยู่ใน Redis จะไม่มี permission จนกว่าจะหมดอายุตาม TTL 86400 วินาที หรือถูก refresh ⇒ ถ้าเปิดใช้ทันทีหลัง deploy ทุกเส้นที่แปะ attribute จะ 403 พร้อมกัน ต้องเปิดผ่านช่วงสังเกตของ กลไก E ซึ่งมีเรื่องต้องแก้ก่อน ดู 03

ที่ยังไม่รู้ และกระทบการตัดสินใจตรง ๆ

  • ตาราง Permissions / RolePermission บน dev มีข้อมูลจริงกี่แถว — เห็นแค่ schema กับโค้ดที่อ่านมัน ยังไม่ได้เห็นข้อมูล ถ้าตารางว่างหรือไม่มีใคร maintain แผนนี้จะไม่มีข้อมูลให้ใช้
  • AppId (GUID) จริงของแต่ละ app และใครเป็นเจ้าของ /users/me — ต้องรู้ก่อนถึงจะตั้ง config ในกติกาข้อ 3 ได้