Private Docs

API Restriction · 00 — ที่มา สภาพปัจจุบัน และทางที่เลือก

อ่านไฟล์เดียวแล้วเข้าใจว่าระบบวันนี้ทำงานยังไง ปัญหาคืออะไร และเราจะแก้ทางไหน — จุดตั้งต้นของเอกสารชุดนี้

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

00. ที่มา สภาพปัจจุบัน และทางที่เลือก

เอกสารชุดนี้มี 4 ไฟล์ อ่านตามลำดับ:

เนื้อหา
00 (ไฟล์นี้)ระบบวันนี้เป็นยังไง ปัญหาคืออะไร เลือกทางไหน
01ปัญหาที่ 1 — role ที่ผูกกับ app ที่เรียกจริง
02ปัญหาที่ 2 — dynamic role · spec ล้วน
03ลำดับ deploy · เรื่องที่ยังต้องตัดสิน · หลักฐานที่ยังขาด
04implementation — ลงมือจากไฟล์นี้ · ข้อขัดแย้งที่ตัดสินแล้ว · test charter · รายการที่บล็อก rollout
05ภาพรวมทั้งชุดด้วย sequence diagram
06สถานะและ checklist งานที่เหลือ — ไฟล์ที่ยึดถือได้ อ่านก่อนเริ่มงานต่อ

ทุกข้อในไฟล์นี้ตรวจกับโค้ดจริงแล้วที่ Backend_UserService@25b1923 และ Backend_Package@10.30.0 ยกเว้นที่กำกับ ยังไม่ verify

Security review เพิ่มเติม 08/09/2026: ข้อความสีแดงเป็นข้อแก้ไขหรือข้อกำหนดเพิ่มเติมของ spec ไม่ใช่การยืนยันว่า implementation หรือระบบที่ deploy แล้วทำงานตามนี้; ส่วนที่ยังต้องตัดสินต้องปิดก่อน rollout ตามไฟล์ 03


ระบบเราเป็นแบบไหน

SuperApp เป็น platform ที่ app อื่นมาเสียบ — วันนี้มี Platform (ตัวหลัก) กับ FX และจะมีเพิ่มอีก · app เหล่านี้ไม่ได้แค่มีหน้าจอของตัวเอง แต่เรียก API ของ service กลางด้วย เช่น FX เรียก User Service ที่อยู่ฝั่ง Platform

ฝั่งหลังบ้านมี service ประมาณ 14 ตัว แต่ละตัวเป็น .NET แยก repo และใช้ library กลางตัวเดียวกันชื่อ SupApp_util_lib (repo Backend_Package)

request หนึ่งเดินทางยังไง

sequenceDiagram
    autonumber
    participant FE as หน้าจอ
    participant APIM as APIM facade
    participant SG as Sentinel Gateway
    participant MW as middleware กลางใน lib
    participant R as Redis
    participant EP as endpoint ของ service

    FE->>APIM: request พร้อม cookie
    APIM->>SG: เอา cookie ไปแลก
    SG-->>APIM: access token ที่ Entra เซ็น
    APIM->>MW: แนบ Authorization Bearer แล้วส่งต่อ
    MW->>MW: validate JWT ได้ OID ของ user
    MW->>R: อ่าน user info ด้วย OID
    R-->>MW: ข้อมูล user พร้อม role ของทุก app
    MW->>EP: ใส่ role เข้า ClaimsPrincipal แล้วส่งต่อ
    EP->>EP: Authorize ตัดสินจาก claim ที่ได้

จุดที่เอกสารชุดนี้สนใจคือ ขั้นที่ 7-8 — ตอนที่ middleware กลางเอา role จาก Redis ใส่เข้า ClaimsPrincipal

ข้อมูลวันนี้หน้าตาเป็นยังไง

ในฐานข้อมูล (UserService):

ตารางเก็บอะไรข้อจำกัด
Applicationapp ที่มาเสียบ
Rolesrole ผูกกับ appunique (AppId, Name) — ชื่อซ้ำข้าม app ได้
UserRolesuser ถือ role อะไรunique (UserId, RoleId, CompanyId)
Permissionsสิทธิ์ผูกกับ appunique (AppId, Name) · Name = "{resource}:{action}"
RolePermissionrole ได้สิทธิ์อะไรFK ไปทั้ง Roles และ Permissions
Menu / MenuPermissionเมนูผูกกับสิทธิ์

🔴 Permissions วันนี้ใช้คุมหน้าจอ ไม่ได้คุม API — มันถูกส่งให้ FE ผ่าน shell เพื่อบอกว่าเมนูไหนควรโชว์ ไม่มีจุดไหนในระบบที่เอามันมาตัดสินว่า request ผ่านหรือไม่ผ่าน

ใน Redis — blob เดียวต่อ user เขียนโดย UserService อ่านโดย middleware กลาง:

{
  "userId": "…", "azureObjectId": "…",
  "apps": [
    { "appId": "a1111111-…", "roles": [{ "id": "r001", "name": "AmloAdmin" }] },
    { "appId": "f2222222-…", "roles": [{ "id": "r004", "name": "FxCompliance" }] }
  ],
  "schemaVersion": 1
}

blob แยก role ตาม app ไว้แล้ว — ข้อมูลถูกต้องตั้งแต่ต้นทาง

blob ตัวนี้เป็น cache ไม่ใช่แหล่งข้อมูลจริง

จุดที่ต้องเข้าใจก่อนคุยเรื่องอื่น: สิ่งที่ middleware อ่านตอน request เข้ามา ไม่ใช่ฐานข้อมูล แต่เป็นสำเนาที่ UserService เขียนไว้ล่วงหน้า

sequenceDiagram
    autonumber
    participant DB as ฐานข้อมูล UserService
    participant W as UserService ตอนเขียน cache
    participant R as Redis
    participant MW as middleware กลาง

    W->>DB: อ่าน user role app company
    W->>R: เขียน blob ทับ พร้อมตั้งอายุ
    Note over R: อยู่ได้ 86400 วินาที แล้วหายไปเอง
    MW->>R: อ่าน blob ตอนมี request
    alt เจอ
        R-->>MW: ใช้ค่านั้นเลย
    else ไม่เจอ
        MW->>W: ถามตรงผ่าน HTTP
        W-->>MW: ข้อมูลล่าสุด
        MW->>R: เขียนกลับเข้า cache
    end

สิ่งที่ตามมาจากการเป็น cache — สำคัญกับทุกอย่างที่คุยกันต่อจากนี้:

  1. แก้สิทธิ์ในฐานข้อมูลแล้วไม่มีผลทันที — ต้องรอ blob หมดอายุ หรือมีคนสั่งเขียนใหม่ · ค่าเริ่มต้นของอายุคือ 86400 วินาที (24 ชั่วโมง)
  2. การถอนสิทธิ์คือฝั่งที่อันตราย — เพิ่มสิทธิ์แล้วยังไม่มีผล คนแค่บ่นว่ากดไม่ได้ · แต่ ถอนสิทธิ์แล้วยังไม่มีผล แปลว่าคนที่ถูกถอนยังใช้สิทธิ์เดิมได้ต่อ จนกว่า cache จะเปลี่ยน
  3. มีทางสั่งเขียนใหม่ราย user ได้ ไม่ต้องรอครบ 24 ชั่วโมง ⇒ “เปลี่ยนแล้วมีผลทันที” เป็นไปได้จริง แต่ ต้องสั่งเอง ไม่ได้เกิดขึ้นเอง
  4. ถ้าคำสั่งเขียนใหม่ล้มเหลว จะเงียบ — คนที่กดถอนสิทธิ์เห็นว่าสำเร็จ ทั้งที่ของจริงยังไม่เปลี่ยน

ข้อ 3 กับ 4 คือเหตุผลที่ 02 กำหนดว่าการเขียน cache ใหม่ต้องเป็นส่วนหนึ่งของการแก้สิทธิ์ ไม่ใช่งานที่ยิงทิ้งแล้วไม่สนใจผล

คำว่า "เปลี่ยนแล้วมีผลทันที" ต้องหมายถึง request ที่ตรวจ authorization หลังยืนยัน revoke แล้วไม่สามารถใช้ grant เดิมได้ ไม่ใช่แค่ refresh สำเร็จหนึ่งครั้ง: request ที่อ่าน DB ก่อน revoke อาจเขียน blob เก่าทับภายหลังได้ ต้องมี revision/freshness check และป้องกัน stale write ตามไฟล์ 02; TTL 24 ชั่วโมงเป็นอายุ cache ไม่ใช่ revocation SLA ที่ยอมรับแล้ว และไม่ย้อนยกเลิก operation ที่ได้รับอนุญาตก่อน revoke

ปัญหาที่ 1 — middleware ไม่รู้ว่า request มาจาก app ไหน

blob แยก role ตาม app ไว้ให้แล้ว แต่ middleware ยุบทิ้ง

RedisUserInfoMiddleware.cs:341:

claims.AddRange(userInfo.AllRoles.Select(r => new Claim(ClaimTypes.Role, r)));

AllRoles (UserInfoForRedis.cs:95-100) คือ Apps.SelectMany(a => a.Roles).Select(r => r.Name).Distinct() — รวม role ของทุก app เข้าด้วยกันด้วยชื่อ

sequenceDiagram
    autonumber
    participant U as user ที่เป็น Admin ใน FX เท่านั้น
    participant MW as middleware กลาง
    participant EP as endpoint ของ Platform

    U->>MW: เรียก endpoint ของ Platform
    MW->>MW: AllRoles ยุบทุก app ได้คำว่า Admin
    MW->>EP: claim Role Admin
    EP->>EP: Authorize Roles Admin ผ่าน
    EP-->>U: 200 ทั้งที่ไม่มีสิทธิ์ใน Platform เลย

ทางแก้ที่ตกลงกันแล้ว — FE ส่ง GUID ของ app ที่ user เลือกอยู่มาใน header · backend เอาไปหาใน apps ของ blob · เจอ ⇒ ใส่ role ของ app นั้น · ไม่เจอ หรือไม่มี role หรือไม่ส่ง header มา ⇒ 401 เพราะเป็นไปไม่ได้ที่จะเรียกในนาม app ที่ตัวเองไม่มี role อยู่เลย

รายละเอียด → 01

ปัญหาที่ 2 — รายชื่อ role ที่เรียกได้ อยู่ในโค้ด

API 1 เส้น วันนี้เรียกได้ 3 role · อนาคต 4 · แล้ว 5

[Authorize(Roles = "A, B, C")]   // เพิ่ม D = แก้บรรทัดนี้ = deploy ใหม่

ทุกครั้งที่รายชื่อเปลี่ยน ต้องแก้โค้ดแล้ว deploy · ยิ่ง service เพิ่ม endpoint เพิ่ม ยิ่งหนัก

สิ่งที่ต้องการคือ: เพิ่ม role ได้จากภายนอก โดยไม่แก้โค้ด ไม่ deploy

⇒ รายชื่อ role ต้องออกจากโค้ดไปอยู่ในข้อมูล และบน endpoint เหลือ ชื่อที่ไม่มีวันเปลี่ยน แทน

permission = ชื่อของ endpoint ที่ไม่เปลี่ยน · role = ของที่เปลี่ยน

รายละเอียด → 02

ปัญหาที่ 1 กับ 2 ต่อกันยังไง

ข้อ 1 ตอบว่า request นี้มาจาก app ไหน · ข้อ 2 ตอบว่า สิทธิ์ของ app นั้นพอเรียกเส้นนี้ไหม

ขอบเขตที่พิสูจน์ได้: ข้อ 1 รู้เพียงว่า caller เลือก app context ใดจากสิทธิ์ที่มี ไม่ได้พิสูจน์ app ต้นทาง; ข้อ 2 ต้องเทียบ app context, permission name และ owner service ร่วมกัน การมี ownerService ตรงอย่างเดียวไม่ได้ป้องกันการเปลี่ยน AppId ไปยังอีก app ที่ user มีสิทธิ์อยู่แล้ว การแยก client/session ข้าม app เป็นข้อกำหนดอีกชุดที่ต้องยืนยันก่อน rollout

ข้อ 2 ตั้งอยู่บนข้อ 1 — ถ้าข้อ 1 ยังยุบ role ข้าม app ชุดสิทธิ์ที่ข้อ 2 เอามาเทียบก็จะเป็นของทุก app รวมกัน แล้วปัญหาเดิมจะย้ายลงไปอีกชั้นเดียวเท่านั้น ⇒ ต้องทำข้อ 1 ก่อนเสมอ


ทางที่ไม่เลือก และเหตุผล

ตัดสินที่ APIM ด้วย allow-list ว่า app ไหนเรียก path ไหนได้ — policy ของ APIM ยัง paste เข้า Portal ด้วยมือ และตัวไฟล์ sentinel-facade-inbound-v6.xml:17-18 เตือนตัวเองว่าของที่รันจริงมี drift จากไฟล์ใน repo · Backend_Iac ยังไม่มี overlay ของ prod ⇒ ไม่มีที่ให้ลงค่า · และตัดสินได้แค่ระดับ app มองไม่เห็น role

ใช้ claim จาก token (appid / azp / aud) — เป็นทางที่ดีที่สุดถ้าทำได้ เพราะ Entra เซ็นมาให้ ไม่ต้องเชื่อ header จาก client แต่ SentinelGateway01.API/Config/tenants.json มี ClientId อยู่ 2 ตัวและแบ่งตาม tenant (customer/CIAM กับ employee/workforce) ไม่ได้แบ่งตาม app ⇒ แยก FX ออกจาก Platform ไม่ได้ · ข้อนี้พลิกทันทีถ้า FX จด client registration ของตัวเองใน Entra

เอาสิทธิ์ API ไปใส่ตาราง Permissions ที่มีอยู่ — ตารางนั้น unique ที่ (AppId, Name) และ join กับ Menu ⇒ ถูกออกแบบให้ app เป็นเจ้าของหน้าจอของตัวเอง ไม่ใช่ให้ UserService เป็นเจ้าของ endpoint ของอีก 40+ repo · พอ service เพิ่มขึ้นเรื่อย ๆ จะไม่รอด ⇒ 02 จึงใช้ตารางแยก

ข้อสมมติที่ต้องแก้ก่อนคุยต่อ

[Authorize] ทำ “มี role ใดใน N role ก็พอ” ได้อยู่แล้ว[Authorize(Roles = "A, B")] คือ OR ไม่ใช่ AND · policy.RequireRole("User", "Admin") ใช้จริงที่ AuthenticationExtensions.cs:135 · ที่ทำไม่ได้คือ AND ซึ่งไม่ใช่โจทย์เรา ⇒ ปัญหาที่ 2 ไม่ได้เกิดจากข้อจำกัดของ framework แต่เกิดจากการที่ รายชื่ออยู่ในโค้ด

“1 APP = 1 USER = 1 ROLE” ฐานข้อมูลไม่ได้บังคับ — unique index จริงคือ (UserId, RoleId, CompanyId) (UserRoleConfiguration.cs:51-53) และ GetUserShellPermissionsHandler.cs:30-31 หยิบ Roles[0] ⇒ role ตัวที่สองหายเงียบอยู่แล้ววันนี้ · แผนไม่ต้องรอแก้ข้อนี้ แค่เขียนให้รองรับหลาย role ตั้งแต่แรก

ชื่อ role ไม่ซ้ำข้าม app ในทางปฏิบัติ ตามที่ทีมยืนยัน แต่ RoleConfiguration.cs:67-68 unique แค่ (AppId, Name) ⇒ ฐานข้อมูลไม่ได้ห้าม · การแยกด้วย AppId จริง ๆ เกิดที่ข้อ 2 ซึ่งเทียบเจ้าของสิทธิ์ ไม่ใช่เทียบชื่อ role