Private Docs

API Restriction · 03 — ลำดับการทำ ของที่ชนกับ E และหลักฐานที่ยังต้องเก็บ

กลไก E ที่มีอยู่แล้วครอบ permission denial ไม่ได้ และมี fail-open หนึ่งจุดที่ต้องแก้ก่อน พร้อมลำดับห้าขั้นที่สลับไม่ได้

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

⬅️ ที่มาของงานนี้ → 00 — ปัญหา มติที่เสนอ และตัวเลือกที่ตกรอบ 📗 กลไกที่หน้านี้พูดถึงตลอด → E — API restriction (observe mode)

03. ลำดับการทำ ของที่ชนกับ E และหลักฐานที่ยังต้องเก็บ

E มีอยู่จริงและ merge แล้ว

ตรวจแล้วที่ Backend_Package@development <Version>10.30.0</Version>:

  • src/Middleware/AuthorizationObserve/AuthorizationObserveResultHandler.cs, AuthorizationObserveOptions.cs, AuthorizationObserveAttributes.cs
  • src/Extension/AuthorizationObserveExtensions.cs
  • UserService ต่อไว้แล้วที่ SecurityServiceExtensions.cs:56 และ config อยู่ที่ appsettings.json:240-243 โดย Enabled = false และไม่ได้ตั้ง Coverage ⇒ ค่าเริ่มต้น OptIn

ยังไม่ verify: 10.30.0 ขึ้น feed แล้วหรือยัง — repo version กับ feed version ของ repo นี้ไม่ตรงกันได้ เพราะ merge เข้า development ไม่ publish ให้อัตโนมัติ ถ้ายังไม่ push service อื่นจะหยิบ E ไปใช้ไม่ได้แม้โค้ด merge แล้ว

E แก้คนละปัญหากับที่เรากำลังจะทำ

E เสียบผ่าน IAuthorizationMiddlewareResultHandler ซึ่งถูกเรียก หลังจาก policy ประเมินเสร็จแล้วAuthorizationObserveResultHandler.cs:95-99 รับ authorizeResult ที่ตัดสินมาเรียบร้อยแล้วเข้ามา

⇒ E เพิ่มเงื่อนไขให้ endpoint ไม่ได้ ทำได้อย่างเดียวคือ ผ่อนการปฏิเสธที่เกิดขึ้นแล้ว และเอกสารของ E ก็ระบุเองว่า “ต้องมี role requirement อยู่ก่อน ถึงจะมีอะไรให้สังเกต”

E คือ ช่วงซ้อมก่อนบังคับ ไม่ใช่คำตอบของ “app ไหนเรียก endpoint ไหนได้” ⇒ ทั้งสองอย่างต้องมี ไม่ใช่เลือกอย่างใดอย่างหนึ่ง

🔴 จุดที่ชนกันจริง — permission denial จะไม่มีช่วงซ้อมเลย

AuthorizationObserveResultHandler.cs:124-126:

var isRoleOnlyDenial =
    policy.Requirements.OfType<RolesAuthorizationRequirement>().Any() &&
    authorizeResult.AuthorizationFailure?.FailCalled != true;

ตามด้วย if (!isRoleOnlyDenial) ⇒ ส่งต่อให้ handler เดิมปฏิเสธจริง ทุกโหมด

ผลที่ตามมามีสองทาง และ ทั้งสองทางแย่:

  • endpoint ที่แปะแค่ [RequirePermission(...)] โดยไม่มี [Authorize(Roles=…)]policy.Requirements ไม่มี RolesAuthorizationRequirementisRoleOnlyDenial = falseปฏิเสธจริงตั้งแต่วันแรก ไม่มี LogOnly ให้ซ้อม วันเปิดจะเป็นหน้าผา
  • ถ้าเลี่ยงโดยแปะ [Authorize(Roles=…)] คู่ไปด้วย แล้ว handler ของ permission ปฏิเสธโดยไม่เรียก context.Fail()isRoleOnlyDenial = trueLogOnly จะปล่อยการปฏิเสธจาก permission ผ่านไปด้วย ทั้งที่ไม่ได้ตั้งใจ

จุดที่สองคือ fail-open: บรรทัด :125 ตรวจว่า requirement ชนิดนั้น มีอยู่ใน policy ไม่ได้ตรวจว่า requirement ตัวไหนเป็นตัวที่ทำให้ fail

⚠️ สังเกตว่าเงื่อนไข FailCalled != true ในโค้ดปัจจุบันแปลว่า ถ้า handler ของ permission เรียก context.Fail() ตอนปฏิเสธ (ซึ่งเป็น pattern ที่คนเขียนกันโดยสัญชาตญาณ) จะไม่มีช่วงซ้อมให้เลย ⇒ ต้องตัดสินใจตั้งแต่ตอนเขียน handler ว่าจะเรียก Fail() หรือไม่ และเขียนเหตุผลกำกับไว้ ไม่งั้นคนถัดไปจะงงว่าทำไม LogOnly ไม่มีผล

ทางแก้ที่เสนอ — เปลี่ยนไปอ่าน authorizeResult.AuthorizationFailure.FailedRequirements แล้วบังคับว่า requirement ที่ fail ทุกตัว ต้องเป็นชนิดที่กลไกนี้รับผิดชอบสังเกต ถ้ามีตัวใดไม่ใช่ ⇒ ปฏิเสธจริงตามเดิม วิธีนี้ปิด fail-open และเปิดทางให้ PermissionRequirement มีช่วงซ้อมไปพร้อมกัน

⚠️ ต้องแก้ E ก่อนหรือใน PR เดียวกัน กับที่ปล่อย [RequirePermission] ออกไป ปล่อยทีหลังแปลว่าช่วงระหว่างนั้นไม่มีช่วงซ้อมให้ใช้

ทิศของ E กลับด้านกับกลไกอื่น — ย้ำอีกครั้ง

เปิด Enabled=true + LogOnly + Coverage=RequireAll บน service ที่มี [Authorize(Roles=…)] ใช้งานจริงอยู่ = ถอดด่านของเส้นเหล่านั้นออกทั้ง service จนกว่าจะสลับเป็น Enforce

ใน UserService วันนี้มีเส้นที่แยกสิทธิ์จริงอยู่ 2 เส้น (AdminAmloController.cs:27, SecurityProbeController.cs:91) ⇒ ต้องเริ่มที่ Coverage=OptIn เสมอ ซึ่งเป็นค่าเริ่มต้นอยู่แล้ว ห้ามเปลี่ยนเป็น RequireAll เพื่อความสะดวก

ลำดับห้าขั้น

ขั้น 0 — เก็บหลักฐานที่ยังขาด (หัวข้อถัดไป) ทำก่อนเคาะ ไม่ใช่ก่อน deploy

ขั้น 1 — แก้ E ให้ครอบ permission denial และปิด fail-open ที่ :125 ทำที่ Backend_Package แล้ว publish

ขั้น 2 — งานข้อ 1 (map role ตาม app ที่ header ระบุ, ไม่เจอ ⇒ 401) ปล่อยโดย สวิตช์ปิด ก่อน แล้วเปิดทีละ service หลัง FE ส่ง header ครบ — ดู 01

ขั้น 3 — เติม permission ลง blob + [RequirePermission] + AuthorizationHandler + IAuthorizationPolicyProvider ยังไม่แปะ attribute บน endpoint จริงสักเส้น

ขั้น 4 — เปิดทีละเส้น แปะ [RequireAuthorizationObserve] + [RequirePermission] บนเส้นแรก เปิด Enabled=true LogOnly OptIn อ่าน log ว่าใครจะโดน แก้ข้อมูลจนไม่มีใครโดนผิด แล้วค่อยสลับ Enforce

ขั้น 1 ต้องมาก่อนขั้น 3 และขั้น 3 ต้องมาก่อนขั้น 4 — สลับไม่ได้ ส่วนขั้น 2 ทำขนานกับขั้น 1 ได้ เพราะแตะคนละ middleware

หลักฐานที่ยังต้องไปเก็บ

1. หักล้างหรือยืนยันข้อสมมติ “1 APP = 1 ROLE” — รันบน dev (AuthDb) เจอแถวไหนก็ตาม = ข้อสมมติถูกละเมิดใน data จริงแล้ว:

SELECT ur."UserId", r."AppId", COUNT(*)
FROM "UserRoles" ur
JOIN "Roles" r ON r."Id" = ur."RoleId"
GROUP BY 1, 2
HAVING COUNT(*) > 1;

2. มีข้อมูลใน Permissions / RolePermission จริงกี่แถว ต่อ AppId — ตัดสินว่า model ของข้อ 2 มีข้อมูลรองรับ หรือเป็นตารางเปล่า

3. มี Roles.Name ซ้ำกันข้าม AppId แล้วหรือยัง — วัดว่าเรื่อง role ชื่อซ้ำเป็นปัญหาที่เกิดแล้วหรือยังแค่เป็นไปได้

4. FX เรียกเข้ามาทางไหนบ้าง — ผ่าน browser ด้วย cookie facade อย่างเดียว หรือมี server-to-server ด้วย ถ้ามี s2s สมมติฐานเรื่อง header ทั้งหมดของข้อ 1 ต้องคิดใหม่

5. AppId (GUID) จริงของแต่ละ app และเจ้าของ /users/me ในตาราง Application

6. SupApp_util_lib 10.30.0 อยู่บน feed แล้วหรือยัง

7. export policy จริงของ APIM จาก Portal — ไฟล์ sentinel-facade-inbound-v6.xml:17-18 เตือนเองว่า live มี drift ⇒ ก่อนจะสรุปว่า header ของ client ถึง backend ดิบจริง ควรยืนยันกับของที่รันอยู่จริง ไม่ใช่ไฟล์ใน repo อย่างเดียว

ต้นทุนรวมที่ต้องนับ

  • ขั้น 1 และ 2 อยู่ใน Backend_Package ล้วน ส่วน ขั้น 3 คร่อมสอง repo — ฝั่งที่ อ่าน permission อยู่ใน Backend_Package แต่ฝั่งที่ เขียน field ลง blob คือ UsersMapper.ToUserInfoForRedis ใน Backend_UserService ⇒ ต้อง deploy UserService ก่อน แล้วรอ blob เก่าหมดอายุตาม TTL 86400 วินาที หรือบังคับ refresh cache ไม่งั้น handler จะเห็น permission เป็นค่าว่างทั้งระบบ
  • ทุกอย่างที่แตะ Backend_Package ⇒ ต้อง publish ด้วยมือทุกครั้ง แล้ว ~13-14 service ต้อง bump และ redeploy เอง (service ที่ยังไม่ adopt จะไม่พัง เพราะไม่ได้แปะ attribute และ field ใหม่เป็น optional)
  • config ใหม่ทุกตัว (ชื่อ header, สวิตช์เปิด/ปิดการบังคับ, AppId ของ service เจ้าของ endpoint) ต้องเติมใน Backend_Iac ครบทุก env ที่ service นั้น deploy — ขาด env ไหน env นั้นพังตอน deploy
  • Backend_Iac ยังไม่มี overlay ของ prod ⇒ ค่าเหล่านี้ยังไม่มีที่ให้ลงสำหรับ prod ต้องแก้เรื่องนั้นก่อนถึงจะพูดเรื่อง prod ได้