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.cssrc/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ไม่มีRolesAuthorizationRequirement⇒isRoleOnlyDenial = false⇒ ปฏิเสธจริงตั้งแต่วันแรก ไม่มี LogOnly ให้ซ้อม วันเปิดจะเป็นหน้าผา - ถ้าเลี่ยงโดยแปะ
[Authorize(Roles=…)]คู่ไปด้วย แล้ว handler ของ permission ปฏิเสธโดยไม่เรียกcontext.Fail()⇒isRoleOnlyDenial = true⇒ LogOnly จะปล่อยการปฏิเสธจาก 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 ได้