Private Docs

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 / DE (เฉพาะเส้นใน 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 ไม่มี role403 ตามปกติ — ถ้าได้ 200 แปลว่าตั้ง Coverage ผิด
Enabled=true LogOnly · endpoint ใน scope · user ไม่มี role ที่ต้องการ200 ผ่านเข้า handler + log Would have denied
Enabled=true LogOnly · ยังไม่ login401/challenge ตามปกติ ไม่ถูกปล่อยผ่าน
Enabled=true Enforce · user ไม่มี role403 ตามปกติ
policy ล้มด้วย claim/custom requirementปฏิเสธตามปกติทุกโหมด + log Denied (not role-only)

📕 กับดัก E — ทิศกลับด้าน · แทนที่ handler เสมอแม้ปิด flag · TFM ต่างจาก D/F