Security Uplift · E — API restriction (observe mode)
ติดตั้งช่วงสังเกตก่อนบังคับ role ราย endpoint: DI, config, ทิศที่กลับด้านกับกลไกอื่น และวิธีอ่าน log ว่าใครจะโดน
อัปเดต: 2026-09-07
📘 แนวคิดเบื้องหลังกลไกนี้ → API Restriction 101 📕 สิ่งที่พังได้ในแต่ละขั้น → กับดัก E 📗 ภาพรวมทั้ง 6 กลไก · กฎ opt-in · การเปิดเป็นราย env → ติดตั้งใน service ของคุณ
E. API restriction / observe mode — เปิด role ราย endpoint แบบมีช่วงสังเกตก่อน
ปัญหาที่แก้: พอแปะ [Authorize(Roles = "…")] เส้นแรกเข้าไป มันเริ่มตอบ 403 ให้ผู้ใช้จริงทันที โดยไม่มีทางรู้ล่วงหน้าว่าใครจะโดน · ASP.NET Core ไม่มี shadow mode ให้ กลไกนี้เติมเข้าไป
🔴 ต้องมี role requirement อยู่ก่อน ถึงจะมีอะไรให้สังเกต — E สังเกตเฉพาะ outcome Forbidden ที่เกิดจาก role · endpoint ที่ไม่มี [Authorize(Roles = "…")] ไม่ถูกแตะในทุกโหมด แม้แปะ [RequireAuthorizationObserve] ไว้แล้ว ⇒ 2 เงื่อนไขต้องครบพร้อมกัน: อยู่ใน scope ของ Coverage และ มี role requirement
grep -rn "\[Authorize" src/{Service}01.API/ # 0 hit = ยังไม่มี role denial ให้สังเกต ทดสอบ E ไม่ได้
🔴 ไม่มีบรรทัด pipeline — มันเสียบผ่าน IAuthorizationMiddlewareResultHandler ใน DI ไม่ใช่ middleware
🔴 ทิศของ E กลับด้านกับ A/B/D — อ่านก่อนเปิด
ตารางนี้ใช้กับ endpoint ที่อยู่ใน scope เท่านั้น คือแปะ [RequireAuthorizationObserve] (ตอน Coverage=OptIn) หรือทุก endpoint ที่ไม่ได้แปะ [SkipAuthorizationObserve] (ตอน Coverage=RequireAll)
| โหมด | A / B / D | E (เฉพาะเส้นใน scope) |
|---|---|---|
Enabled = false | ปล่อยผ่าน (ยังไม่ตรวจอะไร) | ปฏิเสธตามปกติ — framework ตอบ 403 เหมือนไม่มี lib นี้ |
Enabled=true + LogOnly | ปล่อยผ่าน + log | ปล่อยผ่าน + log ⇒ endpoint ที่เคยตอบ 403 จะเปิดให้เข้าได้ |
Enabled=true + Enforce | ปฏิเสธจริง | ปฏิเสธตามปกติ (403 เหมือนตอนปิด) |
เส้นที่อยู่นอก scope ตอบ 403 ตามปกติทุกโหมด — เหมือนไม่ได้เปิด E เลย
⇒ เปิด LogOnly พร้อม Coverage=RequireAll บน service ที่มี [Authorize(Roles=…)] ใช้งานได้อยู่แล้ว = ถอดด่านของเส้นเหล่านั้นออกทั้ง service จนกว่าจะสลับเป็น Enforce · ตอน Coverage=OptIn (ค่าตั้งต้น) ความเสี่ยงจำกัดอยู่แค่เส้นที่แปะ [RequireAuthorizationObserve] ไว้เอง — นี่คือเหตุผลที่ต้องเริ่มที่ OptIn เสมอ
ลำดับที่ถูก คือ เปิด Enabled=true + LogOnly + Coverage=OptIn แล้วแปะ [RequireAuthorizationObserve] เฉพาะเส้นที่กำลังจะเริ่มบังคับ role ก่อน ค่อยไล่แปะ [Authorize(Roles=…)] บนเส้นนั้น — ไม่ใช่แปะ [Authorize] ก่อนแล้วค่อยเปิด
ทั้งเส้นทำงานยังไง
sequenceDiagram
autonumber
participant U as Caller
participant AZ as UseAuthorization
participant OB as AuthorizationObserveResultHandler
participant FB as handler เดิมของ service
participant H as Handler ของ endpoint
U->>AZ: เรียก endpoint
AZ->>AZ: ประเมิน policy ทุกตัวของ endpoint ตามปกติ
alt ผ่าน
AZ->>H: เข้า handler
else ผลเป็น Forbidden หรือ Challenged
AZ->>OB: ส่งผลให้ตัวสังเกตดูก่อน
alt AuthorizationObserve Enabled = false
OB->>FB: ส่งต่อ handler เดิมทันที — 403 ตามปกติ
else ผลไม่ใช่ Forbidden (ยังไม่ login)
OB->>FB: ไม่แตะ — ไม่งั้น LogOnly จะทำให้ Authorize เปล่า ๆ กลายเป็น anonymous
else endpoint ไม่อยู่ใน Coverage
OB->>FB: ไม่แตะ — 403 ตามปกติ
else ไม่ใช่การปฏิเสธจาก role ล้วน ๆ (claim/custom requirement หรือมี Fail)
OB->>FB: ปฏิเสธตามปกติ พร้อม log Denied (not role-only)
else role-only denial และอยู่ใน scope
alt Mode Enforce
OB->>FB: ปฏิเสธตามปกติ 403
else Mode LogOnly
OB->>OB: log Would have denied พร้อม requiredRoles และ callerRoles
OB->>H: ปล่อยเข้า handler ทั้งที่ role ไม่ผ่าน
end
end
end
Note over OB: อ่าน config ครั้งเดียวตอน startup เก็บเป็น singleton ⇒ เปลี่ยนโหมดต้อง restart
ติดตั้ง 2 ขั้น
1. DI — บรรทัดเดียว ไม่มี pipeline
builder.Services.AddAuthorizationObserveMode(options =>
builder.Configuration.GetSection("AuthorizationObserve").Bind(options));
- เรียกครั้งเดียว ตอน startup · เรียกซ้ำ = ห่อทับของเดิม ไม่มีตัวจับ
- วางก่อนหรือหลัง
AddAuthorization()ก็ได้ ชนะทั้งสองทาง - ถ้า service มี
IAuthorizationMiddlewareResultHandlerของตัวเองอยู่แล้ว (เช่นตัวที่ตอบ ProblemDetails ตอน 403) ตัวนั้นถูกเก็บไว้เป็น fallback ไม่ได้ถูกทิ้ง - ⚠️ ชื่อ section
"AuthorizationObserve"เป็นชื่อที่ ผู้เรียกเลือกเอง lib ไม่ได้บังคับ — ใช้ชื่อนี้ให้ตรงกันทุก service
2. config
"AuthorizationObserve": {
// false = ทุก outcome ส่งต่อ handler เดิม/ของ framework ไม่มีอะไรเปลี่ยน (ค่าตั้งต้น)
"Enabled": false,
// "LogOnly" (ค่าตั้งต้น) = role ไม่ผ่านก็ปล่อยเข้า แล้วเขียน log ว่าใครจะโดน
// "Enforce" = ปฏิเสธตามปกติ (403)
"Mode": "LogOnly",
// สังเกตเส้นไหน
// "OptIn" = เฉพาะ endpoint ที่แปะ [RequireAuthorizationObserve] (ค่าตั้งต้น)
// "RequireAll" = ทุก endpoint ยกเว้นที่แปะ [SkipAuthorizationObserve]
"Coverage": "OptIn"
}
ไม่ตั้ง Coverage = OptIn ⇒ เปิด Enabled=true แล้วยังไม่มีเส้นไหนถูกสังเกต และไม่มีเส้นไหนถูกปล่อยผ่าน จนกว่าจะแปะ [RequireAuthorizationObserve]
อ่านครั้งเดียวตอน startup เก็บเป็น singleton ⇒ เปลี่ยนค่าแล้วต้อง restart / redeploy ไม่ใช่สวิตช์สด
อะไรบ้างที่ E “สังเกต” — แคบกว่าที่คิด
สังเกตเฉพาะ outcome ที่เป็น Forbidden ที่เกิดจาก role ล้วน ๆ เท่านั้น คือ policy มี RolesAuthorizationRequirement และ ไม่มีใครเรียก context.Fail()
ทุกอย่างที่เหลือ ปฏิเสธตามปกติในทุกโหมด
Challenged(ยังไม่ได้ login) — ไม่แตะ ไม่งั้นเปิดLogOnly= ทำให้[Authorize]เปล่า ๆ กลายเป็น anonymous ทั้งระบบ- claim requirement / custom requirement ที่ไม่ผ่าน — ไม่แตะ
- มี
context.Fail()ชัดเจน — ไม่แตะ
อ่าน log ยังไง
[AuthorizationObserve] Would have denied {Method} {Path} endpoint=… oid=… requiredRoles=… callerRoles=… correlationId=… mode=LogOnly
[AuthorizationObserve] Denied (not role-only) {Method} {Path} …
บรรทัดแรก = เส้นที่จะโดนตอนสลับเป็น Enforce · บรรทัดที่สอง = ปฏิเสธไปแล้วจริง ไม่เกี่ยวกับ role (มีไว้ไม่ให้เข้าใจผิดว่า observe mode กลืนไป)
เช็คว่า E ทำงาน
| เช็ค | ผลที่ถูก |
|---|---|
Enabled=false | พฤติกรรม authorization เหมือนเดิมทุกอย่าง 403 ยังเป็น 403 |
Enabled=true LogOnly · endpoint ไม่มี [Authorize(Roles=…)] | ไม่ถูกแตะ |
Enabled=true LogOnly Coverage=OptIn · endpoint ไม่ได้แปะ [RequireAuthorizationObserve] · user ไม่มี role | 403 ตามปกติ — ถ้าได้ 200 แปลว่าตั้ง Coverage ผิด |
Enabled=true LogOnly · endpoint ใน scope · user ไม่มี role ที่ต้องการ | 200 ผ่านเข้า handler + log Would have denied |
Enabled=true LogOnly · ยังไม่ login | 401/challenge ตามปกติ ไม่ถูกปล่อยผ่าน |
Enabled=true Enforce · user ไม่มี role | 403 ตามปกติ |
| policy ล้มด้วย claim/custom requirement | ปฏิเสธตามปกติทุกโหมด + log Denied (not role-only) |
📕 กับดัก E — ทิศกลับด้าน · แทนที่ handler เสมอแม้ปิด flag · TFM ต่างจาก D/F