API Restriction · 00 — ปัญหา มติที่เสนอ และตัวเลือกที่ตกรอบ
ทำไม role ที่ inject อยู่ทุกวันนี้กว้างเกินไป ทำไม hardcode ราย endpoint ไปต่อไม่ได้ และข้อเสนอสองข้อที่ต้องเคาะก่อนเริ่มเขียนโค้ด
อัปเดต: 2026-09-07
📘 แนวคิดพื้นฐานเรื่อง authorization → API Restriction 101 📗 กลไก observe mode ที่มีอยู่แล้วในระบบ → E — API restriction (observe mode)
00. ปัญหา มติที่เสนอ และตัวเลือกที่ตกรอบ
ข้อ 1 คือแผนที่ตกลงกันแล้วว่าจะทำ ส่วนข้อ 2 ยังเป็นข้อเสนอ ยังไม่ใช่มติ — ทั้งคู่ยังไม่มีโค้ดสักบรรทัด เขียนไว้ให้อ่านแล้วเคาะข้อ 2 ได้ว่าจะเอาทางไหน ทุกข้อที่อ้างในหน้านี้ตรวจกับโค้ดจริงแล้วที่ Backend_UserService@25b1923 และ Backend_Package@10.30.0 (branch development ทั้งคู่ ณ 07/09/2026) ยกเว้นข้อที่กำกับว่า ยังไม่ verify ไว้ชัดเจน
โจทย์ที่ตั้งไว้
ข้อ 1 — role ที่ inject กว้างเกินไป ทุกวันนี้ middleware กลางเอา role ของ ทุก app ที่ user มี ยัดเข้า ClaimsPrincipal พร้อมกันหมด ทั้งที่ request หนึ่งมาจาก app เดียว ควร inject เฉพาะ role ของ app ที่กำลังเรียกเข้ามา
ข้อ 2 — endpoint ที่ใช้ร่วมกันข้าม app เส้นอย่าง /users/me ของ User Service ถูกเรียกจากทุก app ทั้งที่ User Service อยู่ฝั่ง Platform แต่ FX ก็เรียก การ hardcode ว่า endpoint ไหนอนุญาต app ไหนหรือ role ไหน แปลว่าทุกครั้งที่มี app ใหม่มา plug-in ต้องกลับมาแก้โค้ดและ redeploy ทุก service
สิ่งที่จริงวันนี้ — ตรวจกับโค้ดแล้ว
ระบบแทบยังไม่มี authorization บังคับใช้จริงเลย ใน UserService01.API/Controllers มี [Authorize] ที่ active อยู่ 63 จุด แต่ที่แยกสิทธิ์ต่างกันจริงมีแค่ 2 จุด คือ AdminAmloController.cs:27 ([Authorize(Policy = "AmloAdminAccess")]) กับ SecurityProbeController.cs:91 ([Authorize(Roles = "security-probe")]) ที่เหลือ 61 จุดเป็น [Authorize] เปล่า ซึ่งตรวจแค่ว่า login แล้วหรือยัง และยังมีอีก 13 จุดที่ถูก comment ทิ้งไว้ โดย AdminOnboardingController.cs:18-22 เขียน comment ยอมรับตรง ๆ ว่าทั้ง controller ไม่มี enforcement
ข้อนี้สำคัญต่อการตัดสินใจ เพราะแปลว่าเรากำลังออกแบบ model ทับของที่ยังไม่เคยบังคับใช้ — ความเสี่ยงเกือบทั้งหมดอยู่ที่ วันเปิด ไม่ใช่วันออกแบบ และเป็นเหตุผลที่ กลไก E มีอยู่ตั้งแต่แรก
ข้อสมมติหนึ่งข้อที่ต้องแก้ก่อนคุยต่อ
[Authorize] ทำ “มี role ใดใน N role ก็พอ” ได้อยู่แล้ว — ไม่ใช่ข้อจำกัดของ framework:
[Authorize(Roles = "Admin, Operator")]— comma คือ OR ไม่ใช่ ANDpolicy.RequireRole("User", "Admin")— ใช้จริงอยู่ที่AuthenticationExtensions.cs:135policy.RequireClaim(ClaimTypes.Role, amloAdminRoles)— ใช้จริงอยู่ที่AuthenticationExtensions.cs:153ผูกกับAdminAmloController.cs:27
ที่ทำไม่ได้จริงคือ AND (ต้องมีครบทุก role) ซึ่งต้องเขียน policy เอง — แต่ไม่ใช่โจทย์ของเรา
⇒ ข้อสรุปของข้อ 2 ยังถูก แต่ด้วยเหตุผลคนละอันกับที่เข้าใจกันไว้: ปัญหาไม่ใช่ any-of ทำไม่ได้ ปัญหาคือ ชื่อ role และ AppId ยังอยู่ในไฟล์ .cs ⇒ app ใหม่หนึ่งตัว = แก้โค้ด + bump lib + redeploy ~13-14 service
ข้อสมมติที่ schema ไม่ได้บังคับ
🔴 “1 APP = 1 USER = 1 ROLE” เป็น convention ไม่ใช่ constraint — unique index จริงคือ (UserId, RoleId, CompanyId) ที่ UserRoleConfiguration.cs:51-53 ⇒ user เดียวถือหลาย role ใน app เดียวได้ DB ไม่ห้าม
และโค้ดฝั่งอ่านก็ไม่ได้ถือว่าเป็นหนึ่ง: UsersMapper.cs:242-257 ทำ GroupBy(ur => ur.Role.AppId) แล้วเก็บ Roles เป็น list ส่วน GetUserShellPermissionsHandler.cs:30-31 หยิบ shell.Roles[0] ⇒ role ตัวที่สองหายเงียบ ไม่ error ไม่ log
ข้อเสนอไม่ต้องรอให้แก้ข้อนี้ — ออกแบบให้ inject role ทุกตัวของ app ที่เลือก ไม่ใช่ตัวแรก แล้วเรื่องจะเติม unique constraint (UserId, AppId) หรือไม่ เป็นมติแยกที่ตัดสินทีหลังได้ ไม่ใช่เงื่อนไขตั้งต้น
เรื่องชื่อ role ซ้ำข้าม app
ทีมยืนยันว่า ชื่อ role ไม่ซ้ำกันข้าม app และถ้าจะซ้ำ การแยกก็ควรแยกด้วย AppId ไม่ใช่ด้วยการตั้งชื่อให้ต่างกัน
ข้อจำกัดที่มีอยู่จริงในโค้ด: RoleConfiguration.cs:67-68 unique แค่ (AppId, Name) ⇒ DB ไม่ได้ห้ามชื่อซ้ำข้าม app และ role claim หลัง map แล้วเป็น string เปล่า ๆ ไม่ได้พก AppId ไปด้วย ⇒ การแยกด้วย AppId เกิดขึ้นจริงที่ ข้อ 2 ซึ่ง resolve permission เทียบ AppId ของ service เจ้าของ endpoint เอง ส่วนข้อ 1 พึ่ง convention ของทีมไปก่อน (ยังไม่ verify ว่าวันนี้มี row ชื่อซ้ำข้าม AppId จริงไหม อยู่ในรายการหลักฐานที่ต้องเก็บ)
มติที่เสนอ
ข้อ 1 → ทำตามแผนที่วางไว้ FE ส่ง GUID ของ app ที่ selected มาใน header, backend เอาไปหาใน UserInfoForRedis.Apps ว่า user มี role อะไรใน app นั้น แล้ว map แค่นั้น หาไม่เจอหรือไม่มี role ⇒ 401 เพราะเป็นไปไม่ได้ที่จะเรียกในนาม app ที่ตัวเองไม่มี role รายละเอียดอยู่ที่ 01 — role ที่ผูกกับ app ที่เรียกจริง
ข้อ 2 → endpoint ประกาศ “ความสามารถ” ไม่ใช่ “app หรือ role” ใช้ permission ที่มีอยู่แล้วในระบบเป็นสิ่งที่ endpoint ประกาศ แล้วให้ข้อมูลใน DB เป็นตัวบอกว่า app ไหน/role ไหนได้ permission นั้น ⇒ app ใหม่มา plug-in = insert row ไม่ต้องแตะโค้ด รายละเอียดอยู่ที่ 02 — permission model
ตัวเลือกที่ตกรอบ และเหตุผลที่ตกรอบ วันนี้
A. ตัดสินที่ APIM ด้วย allow-list ว่า app ไหนเรียก path ไหนได้ — ตกเพราะ policy ของ APIM ยัง paste เข้า Portal ด้วยมือ และตัวไฟล์ sentinel-facade-inbound-v6.xml:17-18,51-54 เตือนตัวเองว่า live มี drift จากไฟล์ใน repo · บวกกับ Backend_Iac ยังไม่มี overlay ของ prod ⇒ ประกาศสิทธิ์ที่ชั้นนี้ ไม่มีที่ให้ลงค่าสำหรับ prod · และถึงทำได้ก็ตัดสินได้แค่ระดับ app มองไม่เห็น role
B. ใช้ claim จาก token (appid / azp / aud) แทน header — น่าจะเป็นทางที่ดีที่สุดถ้าทำได้ เพราะ Entra เซ็นมาให้ เชื่อได้โดยไม่ต้อง validate เอง แต่ SentinelGateway01.API/Config/tenants.json มี ClientId อยู่ 2 ตัว และแบ่งตาม tenant (customer/CIAM กับ employee/workforce) ไม่ได้แบ่งตาม app ⇒ claim พวกนี้แยก FX ออกจาก Platform ไม่ได้ ข้อนี้พลิกได้ทันทีถ้า FX จด client registration ของตัวเองใน Entra
C. hardcode role ราย endpoint ต่อไป แล้วใช้ E ช่วยซ้อม — ทำได้และถูกที่สุดในระยะสั้น แต่แลกกับการเสียคุณสมบัติ “app ใหม่ไม่ต้องแก้โค้ด” ซึ่งเป็นโจทย์ตั้งต้นทั้งหมดของข้อ 2
เงื่อนไขที่ทำให้มติข้างบนพลิก
- ข้อ 1 ต้องคิดใหม่ ถ้ามี caller ที่ไม่ใช่ browser (server-to-server ของ FX) ที่ต้องส่ง AppId เองได้ เพราะ narrowing model ตั้งอยู่บนสมมติฐานว่า header ตัดสิทธิ์ได้อย่างเดียว เพิ่มไม่ได้ — ยังไม่ verify ว่า FX เรียกทางไหนบ้าง
- ข้อ 2 พลิกไป B ถ้า FX ได้ client registration แยกใน Entra
- ข้อ 2 พลิกไป A ถ้า APIM policy ถูก deploy ด้วย pipeline จาก
Backend_Iacจริง และ มี prod overlay และ ยอมรับว่าตัดสินได้แค่ระดับ app - ข้อ 2 ถอย ถ้าตาราง
Permissions/RolePermissionบน dev ว่างเปล่าหรือไม่มีใคร maintain — เห็นแค่ schema, repository และ read path เท่านั้น ยังไม่ verify ว่ามี row จริงกี่แถว