Private Docs

App Context Gate — คุมว่า user คนนี้กำลังทำงานในนาม app ไหน

อธิบาย feature app-context ใน SupApp_util_lib 10.31.0 แบบไล่ตาม request จริง — ปัญหาที่แก้, ด่านทั้ง 10, Revision, ทำไมต้องตัด role สองรอบ, checklist สำหรับ service ที่จะเอาไปใช้, และทำไมยังเปิดใช้ไม่ได้

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

1. ปัญหาที่ feature นี้แก้

user คนหนึ่งอยู่ได้หลาย app พร้อมกัน และแต่ละ app ให้ role คนละแบบ

แต่ token ที่ user ถือมามีแค่ใบเดียว และมันหอบ role ของ ทุก app มารวมกันในนั้น

ผลคือ user ที่เป็น Admin ใน app A จะผ่านด่าน Admin ของ app B ไปด้วย ทั้งที่ใน app B เขาเป็นแค่ Member ไม่มีใครตั้งใจให้เป็นแบบนั้น แต่ระบบไม่มีอะไรกันไว้

feature นี้บังคับสองอย่าง:

  1. ทุก request ต้องบอกมาว่า “ฉันกำลังทำงานในนาม app ไหน” (ส่งมาใน header)
  2. ระบบจะ ตัด role ของ app อื่นทิ้ง ก่อนส่งต่อให้ controller

2. ตาม request หนึ่งอันจากต้นจนจบ

สมมติ สมชาย:

approle ที่สมชายถือ
app A (aaaa…)Admin
app B (bbbb…)Member

สมชายเปิดหน้าจอของ app B แล้วกดปุ่ม หน้าจอยิง:

GET /api/reports
Authorization: Bearer <token>
X-App-Id: bbbbbbbb-....        ← app B

สิ่งที่เกิดขึ้นตามลำดับ

ขั้น 1 — UseAuthentication ทำงาน

framework ตรวจ token แล้วสร้าง context.User ขึ้นมา ตอนนี้หน้าตาแบบนี้:

identity #1 (จาก JWT)
  oid          = 2020...
  CustomRoles  = Admin       ← role ของ app A ติดมาด้วย
  CustomRoles  = Member
  tid          = tenant-123

ขั้น 2 — RedisUserInfoMiddleware ทำงาน (อยู่หลัง UseAuthentication ก่อน UseAuthorization)

มันอ่าน X-App-Id ได้ app B แล้วไปดึงข้อมูลสมชายจาก Redis มาดูว่า:

  • สมชายอยู่ใน app B จริงไหม → จริง
  • ใน app B เขามี role อะไร → Member อย่างเดียว

ขั้น 3 — ตัด role ทิ้งแล้วสร้าง context.User ใหม่

identity #1 (ใหม่ ชื่อ "AppContext")
  NameIdentifier = 1111-2222-...   ← internal Users.Id ของเรา ไม่ใช่ oid ของ Entra
  Role           = Member          ← เฉพาะ role ใน app B

identity #2 (ของเดิม แต่ role ถูกถอนออกหมด)
  oid          = 2020...
  tid          = tenant-123        ← claim ที่ไม่ใช่ role อยู่ครบ

Admin หายไปแล้ว ⇒ ถ้า controller เช็ค User.IsInRole("Admin") จะได้ false ซึ่งถูกต้อง

ขั้น 4 — ฝาก context ไว้ให้ controller อ่าน

var ctx  = HttpContext.GetAppContext();
// ctx.AppId  = app B
// ctx.UserId = 1111-2222-...
// ctx.Roles  = ["Member"]

var info = HttpContext.GetUserInfo();
// info.Apps     = [app B]        ← เหลือ app เดียว ไม่ใช่ทั้งสอง
// info.AllRoles = ["Member"]
// info.Email    = ...            ← ข้อมูลระดับ user อยู่ครบ

ข้อมูลที่ GetUserInfo() คืน ถูกตัดให้เหลือเฉพาะ app ที่เลือก ด้วย เพราะ handler ส่วนใหญ่อ่านจากตรงนี้ ไม่ได้อ่านจาก principal — ถ้าปล่อยให้คืนทุก app การตัด role ที่ทำมาทั้งหมดก็ไม่มีความหมาย

Companies ไม่ถูกตัด เพราะเป็นของ user ไม่ใช่ของ app

ขั้น 5 — UseAuthorization ทำงาน แล้ว request ถึง controller

flowchart LR
    A["token<br/>Admin + Member"] --> B[UseAuthentication]
    B --> C["context.User<br/>Admin, Member"]
    C --> D[RedisUserInfoMiddleware]
    D --> E["context.User<br/>Member เท่านั้น"]
    E --> F[UseAuthorization]
    F --> G[Controller]

ทำไมต้องถอน ไม่ใช่แค่เพิ่ม

จุดที่คนพลาดบ่อยที่สุด: การเพิ่ม identity ใหม่ที่มีแค่ Member ไม่ได้ปิดอะไรเลย

IsInRole ไล่ค้น ทุก identity บน principal ⇒ Admin ที่ยังนั่งอยู่บน identity เดิมก็ยังตอบ true ต้องลบ claim นั้นออกจริง ๆ เท่านั้น


3. ถ้าไม่ผ่านด่านจะได้อะไรกลับ

ด่านเรียงตามลำดับ ตกด่านไหนตอบทันที ไม่ทำต่อ

ด่านตกเมื่อตอบcode
0ยังไม่ login401APP_CONTEXT_UNAUTHENTICATED
1login แล้วแต่หา oid ในtoken ไม่เจอ503APP_CONTEXT_UNAVAILABLE
2header ขาด / ส่งซ้ำ / ไม่ใช่ GUID400APP_CONTEXT_BAD_REQUEST
3ดึงข้อมูล user ไม่ได้เลย (Redis + fallback ล่มทั้งคู่)503APP_CONTEXT_UNAVAILABLE
4ได้ข้อมูลมาแต่อ่านไม่ออก (schema/JSON/decrypt)503APP_CONTEXT_UNAVAILABLE
5ยืนยันไม่ได้ว่าข้อมูลใหม่พอ (ดูข้อ 4)503APP_CONTEXT_UNAVAILABLE
6app ที่ขอ ไม่ใช่ app ของ user คนนี้403APP_CONTEXT_FORBIDDEN
7อยู่ใน app จริง แต่ไม่มี role เลย403APP_CONTEXT_FORBIDDEN
8ข้อมูล permission เป็น null503APP_CONTEXT_UNAVAILABLE
9ถอน role ออกไม่สำเร็จ503APP_CONTEXT_UNAVAILABLE

body ที่ตอบกลับมีแค่ 5 field เท่านั้น: type title status code traceId

ไม่มี app id, ชื่อ role, oid, internal user id หรือเศษข้อมูลใด ๆ — ของพวกนั้นอยู่ใน log อย่างเดียว

ด่าน 6 กับด่าน 7 ตอบกลับ เหมือนกันเป๊ะ (ต่างแค่ traceId) ตั้งใจ เพราะถ้าแยก คนที่ยังไม่ได้ยืนยันตัวตนจะเดาได้ว่า app id ไหนมีอยู่จริงในระบบบ้าง


4. Revision — กันสิทธิ์ที่ถอนแล้วยังใช้ได้

เล่าเป็นเรื่อง:

  • 10:00 admin ถอน role Admin ของสมชายออกจาก DB
  • ข้อมูลสมชายที่ cache ไว้ใน Redis มีอายุ 24 ชั่วโมง
  • ถ้าการล้าง cache พลาด (Redis เขียนไม่ติด / process ตายกลางทาง) สมชายยังเป็น Admin ได้ถึง 10:00 พรุ่งนี้
  • SLA ที่ตกลงกันไว้คือ 30 วินาที

Revision คือตัวจับกรณีนี้ วิธีคือแยกความจริงเป็นสองที่:

เก็บที่คืออะไร
ในตัว cache เองเลข revision ณ ตอนที่ cache ก้อนนี้ถูกเขียน
key แยกอีกอันหนึ่งเลขจริงล่าสุด — บวกขึ้นทุกครั้งที่สิทธิ์ของ user คนนั้นเปลี่ยน

middleware อ่านทั้งสองมาเทียบกัน

  • เลขใน cache น้อยกว่า เลขจริง ⇒ cache ก้อนนี้เกิดก่อนการถอนสิทธิ์ ⇒ ใช้ไม่ได้ ⇒ 503
  • เลขใน cache หายไป ⇒ พิสูจน์ไม่ได้ ⇒ 503 เหมือนกัน

5. ทำไมต้องตัด role สองรอบ

ขั้น 3 ในข้อ 2 ตัด role ไปแล้วรอบหนึ่ง สำหรับ endpoint ส่วนใหญ่แค่นั้นจบ

แต่ ถ้า endpoint นั้นมี policy ที่ระบุ authentication scheme ไว้ framework จะ authenticate ใหม่อีกรอบ ก่อนตรวจ policy แล้วเขียนทับ context.User ด้วยผลลัพธ์ใหม่

flowchart TD
    A["ขั้น 3 จบ<br/>context.User = Member"] --> B[UseAuthorization]
    B --> C{policy ระบุ scheme ไหม?}
    C -- ไม่ --> D["ไม่มีอะไรเปลี่ยน<br/>Member — ถูกต้อง"]
    C -- ใช่ --> E["framework authenticate ใหม่<br/>เขียนทับ context.User"]
    E --> F["context.User = Admin + Member<br/>🔴 role ที่ตัดไปแล้ว กลับมา"]
    F --> G[AppContextPolicyEvaluator<br/>ตัดใหม่]
    G --> H["context.User = Member<br/>ถูกต้อง"]

โค้ดของ framework ที่เป็นต้นเหตุ:

foreach (var scheme in policy.AuthenticationSchemes)
{
    var result = await context.AuthenticateAsync(scheme);
    if (result != null && result.Succeeded)
        newPrincipal = SecurityHelper.MergeUserPrincipal(newPrincipal, result.Principal);
}
if (newPrincipal != null)
    context.User = newPrincipal;      // ← ของที่เราตัดไว้ หายตรงนี้

จุดตาย: MergeUserPrincipal เริ่มจาก null ไม่ได้เริ่มจาก context.User

⇒ มันไม่ได้ merge กับของเดิม แต่ ทับทิ้งทั้งก้อน ด้วย principal ที่มี role ครบทุก app เหมือนเดิม ⇒ Admin กลับมา ⇒ สมชายผ่านด่าน Admin ของ app B ได้

AppContextPolicyEvaluator คือตัวซ่อม มันดูว่า context.User ถูกเปลี่ยนไปหรือเปล่า ถ้าถูกเปลี่ยนก็ซ่อม

การเขียนทับนี้ทำให้ เส้นปิด (EnforceAppContext=false) เสียหายด้วย — identity ที่ RedisUserInfo แนบไว้ ถือ internal Users.Id อยู่ใน NameIdentifier แล้วมันหายไปพร้อมกัน ⇒ handler อ่านได้ Entra subject แทน ตัวซ่อมเอา identity นั้นใส่กลับ แต่ ไม่ตัด role เพราะเส้นปิดไม่เคยตัด role อยู่แล้ว

ไม่ใช่เรื่องสมมติBackend_WorkflowService/src/WorkFlow01.API/Base/ApiBaseController.cs:9 แปะ [Authorize(AuthenticationSchemes = "Bearer")] ไว้บน base controller ⇒ ทุก controller ที่สืบทอดโดนหมด (เป็นที่เดียวที่เจอ จาก 10 service ที่ grep)

เจอสถานการณ์นี้ทำอะไร
ไม่มี app context และไม่มี identity ของ RedisUserInfoไม่ทำอะไร
ไม่มี app context แต่ identity ของ RedisUserInfo (เส้นปิด) ถูกทับใส่กลับไปไว้หน้าสุด — ไม่ตัด role เพราะเส้นปิดไม่เคยตัด
context.User ยังเป็นก้อนเดิมไม่ทำอะไร — ตัดซ้ำจะถอน Member ทิ้งไปด้วย
context.User ถูกเขียนทับตัด role ใหม่
ตัดใหม่ไม่สำเร็จคืน principal เปล่า ⇒ 401 — ไม่มีทางผ่าน

6. Checklist สำหรับ service ที่จะเอาไปใช้

ตอนลงทะเบียน (Program.cs)

builder.Services.AddRedisUserInfo(builder.Configuration.GetSection("RedisUserInfo"));
builder.Services.AddApiPermissions(o => o.OwnerServiceName = "UserService");

ตอนต่อ pipeline — ลำดับสลับไม่ได้

app.UseAuthentication();
app.UseRedisUserInfo();          // ← ต้องอยู่ตรงนี้เท่านั้น
app.UseAuthorization();

app.MapControllers();
app.ValidateApiPermissionEndpoints();   // ← ลืมแล้ว startup พัง
app.Run();

ValidateApiPermissionEndpoints() เช็คว่าไม่มี endpoint ไหนแปะ [RequirePermission] พร้อมกับ AllowAnonymous / [SkipAppContext] / scheme — สามอย่างนี้ทำให้ permission ไม่ทำงานจริง ทั้งที่โค้ดยังอ่านเหมือนถูกป้องกันอยู่ ถ้าเจอมันจะบอกมาทุกตัวในข้อความเดียว

ตอนอ่านค่าใน controller

var ctx = HttpContext.GetAppContext();   // app ที่เลือก + role ใน app นั้น
var info = HttpContext.GetUserInfo();    // ข้อมูล user ทั้งก้อน

endpoint ที่ไม่มีคนเรียก (health probe, OpenAPI, static file) แปะ [SkipAppContext]

endpoint ที่แปะ marker นี้ยัง ได้ internal user id ตามปกติ (NameIdentifier) แต่จะ ไม่มี role เลย และ GetAppContext() / GetUserInfo() คืน null

เหตุผล: marker แปลว่า “route นี้ไม่มี app” ไม่ได้แปลว่า “route นี้ไม่มีคนเรียก” — endpoint ที่บันทึกว่าใครเรียก ต้องได้ id ที่ถูก ส่วน role ไม่ใส่เพราะไม่ได้เลือก app ⇒ ไม่มีชุด role ไหนที่ถูกต้อง

ขั้นนี้ ไม่มีทาง deny ถ้า Redis ล่มก็ปล่อยผ่านโดยไม่มี identity — probe ที่ตอบ 503 เพราะ Redis ล่ม แย่กว่า probe ที่ไม่มี identity · caller ที่ยังไม่ login ไม่มี oid ⇒ ไม่ยิง Redis เลย

config 3 ตัว

keydefaultผล
RedisUserInfo:EnforceAppContextfalsetrue = เปิดด่านทั้งหมด
RedisUserInfo:AppContextHeaderNameX-App-Idชื่อ header
ApiPermissions:OwnerServiceNameไม่มีชื่อ service เจ้าของ permission

7. ทำไมยังเปิดใช้ไม่ได้

ยังไม่มีใครเขียนข้อมูลสองอย่างที่ด่านต้องใช้ — ข้อมูล permission (ด่าน 8) และ เลข revision (ด่าน 5)

เปิด EnforceAppContext=true วันนี้ ⇒ ทุก request ตกด่าน 5 หรือ 8 ⇒ 503 หมด

รุ่น 10.31.0 คือ ฝั่งอ่านล้วน ๆ ครบแล้ว แต่ยังไม่มีข้อมูลให้อ่าน ฝั่งเขียนเป็นงานของ UserService


8. ภาคผนวก — เหตุผลเบื้องหลัง

ไม่ต้องอ่านตอนแรก อ่านตอนสงสัยว่า “ทำไมทำแบบนี้”

ทำไม 401 ใช้แค่ด่าน 0

FE interceptor ผูก 401 ไว้กับ “ไป refresh token” ถ้าตอบ 401 ตอน Redis ล่ม client ทุกตัวจะเข้า refresh loop พร้อมกันในวินาทีที่ backend รับไม่ไหวที่สุด ด่านที่เหลือจึงเป็น 503

ทำไม 503 ทุกสาเหตุใช้ code เดียว

แยกให้ละเอียด = บอก caller ที่ยังไม่ยืนยันตัวตนว่าตอนนี้ Redis ล่ม / schema เปลี่ยน / ข้อมูลเก่า ไม่มีเหตุผลที่ caller ต้องรู้ ส่วนคนที่ต้อง debug ดูจาก log ซึ่งมี check= บอกด่านชัดเจน

ทำไมเลขด่านเป็น enum ไม่ใช่ตัวเลขลอย

เดิมฟังก์ชันที่เขียน response รับ HTTP status กับเลขด่านเป็น คนละ argument มี call site 13 จุด และไม่มีอะไรผูกสองค่านั้นเข้าด้วยกัน

เขียน checkId: 6 คู่กับ Status503ServiceUnavailable ก็ compile ผ่าน แล้วได้ log ที่ขัดกับสิ่งที่ตอบไปจริง ซ้ำร้ายกว่านั้น การแปลง status เป็น code ถูกทำอีกรอบข้างในฟังก์ชัน = mapping สองชุดที่เพี้ยนจากกันได้

ตอนนี้ AppContextDenial ค่าเดียวตัดสินทั้งสามอย่าง และค่าตัวเลขของมัน คือ เลข check= ใน log

ทำไม key ใน HttpContext.Items เป็น Type ไม่ใช่ string

internal static readonly object UserInfoItemKey   = typeof(UserInfoForRedis);
internal static readonly object AppContextItemKey = typeof(AppContextResult);

internal const string LegacyUserInfoItemKey = "UserInfoForRedis";   // เขียนคู่ไว้ 1 release

HttpContext.Items เป็น dictionary ตัวเดียว ที่ middleware ทุกตัวในแอปเขียนร่วมกัน ตั้งชื่อ "AppContext" เฉย ๆ แล้วมีคนอื่นใช้ชื่อซ้ำ ค่าจะทับกันโดยไม่มี error key จึงต้องไม่ชนกับโค้ดที่ lib ไม่เคยเห็น วิธีปกติคือใส่ prefix แต่ prefix ที่อ่านรู้เรื่องย่อมมีชื่อ product อยู่ และ lib ตัวนี้ไม่ได้ผูกกับ product นี้ตลอดไป — ต้องมาเปลี่ยน runtime contract เพราะเปลี่ยนแบรนด์ ไม่คุ้ม

Items รับ key เป็น object อยู่แล้ว ⇒ ใช้ Type ไปเลย ชนไม่ได้โดยโครงสร้าง และไม่มีชื่ออะไรให้ล้าสมัย

แต่ลบ string เก่าทิ้งเลยไม่ได้ เพราะมี consumer ที่เซ็ต Items ด้วย literal เอง ไม่ได้ผ่าน package — Backend_NotificationService/tests/.../DiagnosticsControllerTests.cs:15 ประกาศ "UserInfoForRedis" ของตัวเอง ถ้าลบทิ้ง GetUserInfo() จะเริ่มคืน null โดยไม่มี error อะไรบอก

จึงเขียนสอง key ชี้ object เดียวกัน และ GetUserInfo() อ่านได้ทั้งสอง ลบ key เก่าเมื่อไม่มีใครอ่านแล้ว ส่วน AppContextResult ไม่มี key เก่า เพราะยังไม่มีใครใช้

ทั้งหมดเป็น internal ฝั่ง consumer ใช้ GetAppContext() / GetUserInfo() ไม่ต้องรู้ชื่อ key

กับดัก: RequireRole มองไม่เห็น role ฝั่ง Redis

identity จาก JWT ใช้ RoleClaimType = "CustomRoles" แต่ identity ที่ RedisUserInfo แนบเข้าไปใช้ ClaimTypes.Role

⇒ policy ที่เขียนด้วย .RequireRole(...) มองไม่เห็น role ที่มาจาก Redis ต้องใช้ .RequireClaim(...) แทน

ทำไมอ่าน Redis ทีละ key ไม่ใช่ multi-key ครั้งเดียว

สอง key นี้ไม่มี hash tag ร่วมกัน บน Redis แบบ cluster มันตกคนละ slot แล้ว multi-key read จะ fail

key ของ cache มีมาก่อน feature นี้ ใช้งานจริงอยู่ทุก env แล้ว จะเปลี่ยนชื่อให้ไปแชร์ hash tag ไม่ได้ — เปลี่ยนแล้ว cache ทั้ง fleet กลายเป็น miss พร้อมกัน

จึงยิงแยกสองครั้ง ยิงทั้งคู่ก่อนแล้วค่อย await ⇒ StackExchange.Redis pipeline ให้เอง ไม่เสีย round trip เพิ่ม และการอ่านแบบไม่ atomic ปลอดภัยทั้งสองลำดับ: cache เก่าเทียบเลขใหม่ = deny (ถูก), cache ใหม่เทียบเลขเก่า = ผ่าน (ถูกเหมือนกัน)

ทำไม ValidateApiPermissionEndpoints() ต้องให้ consumer เรียกเอง

เวอร์ชันแรกเช็คจาก hosted service ของตัวเอง ผิด และพิสูจน์แล้วว่าผิดบน host จริง

framework ย้ายรายการ endpoint เข้าที่ที่ hosted service อ่านได้ หลัง hosted service ของ user เริ่มไปแล้ว ⇒ ตอนเช็คมันเห็นรายการว่าง ไม่เจออะไร แล้วปล่อยให้ endpoint ที่ AllowAnonymous + [RequirePermission] ตอบ 200 จริง (probe วัดได้ DIEndpointsBeforeStart=0, DIEndpointsAfterStart=3)

ย้ายไป ApplicationStarted จะแย่กว่า — callback นั้นรันใน try/catch ของ host exception จะถูก log เฉย ๆ แล้ว process ยังรับ request ต่อ

ตอนนี้ PermissionStartupGuard เหลือหน้าที่เดียว: ถ้า consumer ลืมเรียก ให้ startup พัง

การเช็ค scheme ที่ guard ทำ เป็น ชั้นแรกจากสองชั้น — มันจับได้เฉพาะ endpoint ที่แปะ [RequirePermission] ส่วน endpoint ที่ gate ด้วย role ล้วนซึ่ง guard มองไม่เห็น ปิดด้วยตัวซ่อมในข้อ 5 ตอน runtime

ลำดับการ register ที่พลาดแล้วเงียบ

ถ้า service จะ register IPolicyEvaluator ของตัวเอง ต้อง register ก่อน AddRedisUserInfo

register ทีหลัง = ตัวสุดท้ายชนะ ⇒ ตัวซ่อมในข้อ 5 หลุดไปทั้งตัวโดยไม่มี error วันนี้ยังไม่มี service ไหนทำ

[RequirePermission] ทำงานยังไง

RequirePermissionAttribute สืบทอด AuthorizeAttribute และ เซ็ต Policy เอง ถ้าไม่ทำ policy provider จะไม่ถูกเรียกเลย และ attribute จะไม่ทำอะไรโดยไม่มี error

handler fail closed ทุกอย่างที่พิสูจน์ไม่ได้ — ไม่มี app context, ข้อมูล permission เป็น null, resolve ชื่อ service ไม่ได้, revision พิสูจน์ไม่ได้, หรือมี exception — ทั้งหมด Fail() ซึ่ง observe mode ปล่อยผ่านไม่ได้

มีกรณีเดียวที่มันไม่ทำอะไรเลย: ทุกอย่างครบถ้วนสดใหม่ แล้วแค่ไม่มีสิทธิ์ที่ตรง — นั่นคือสถานะเดียวที่ควรถูก stage

OwnerServiceName แยกเป็น key ของตัวเอง ไม่ derive จาก ServiceIdentity:ServiceName เพราะค่านั้นมี prefix ของ environment (DEV_UserService) ⇒ สิทธิ์ที่บันทึกบน env หนึ่งจะไม่ match บนอีก env และการถอด prefix ออกคือ pattern ที่เคยทำให้เกิด incident บน platform นี้มาแล้ว

Observe mode — เส้นไหน stage ได้บ้าง

AuthorizationObserve:Mode=LogOnly = บาง denial แค่ log แล้วปล่อยผ่าน ไม่ block จริง

flowchart TD
    A[ถูกปฏิเสธ] --> B{handler เรียก Fail เองไหม?}
    B -- ใช่ --> Z[block เสมอ]
    B -- ไม่ --> C{รู้ไหมว่าอะไร fail?}
    C -- ไม่รู้ --> Z2[block — เดาไม่ได้ก็ไม่ปล่อย]
    C -- รู้ --> D{endpoint กำลังย้ายไป permission?}
    D -- ใช่ --> E{ทุกอย่างที่ fail<br/>เป็น permission?}
    D -- ไม่ --> F{ทุกอย่างที่ fail<br/>เป็น role?}
    E -- ใช่ --> S[log แล้วปล่อยผ่าน]
    E -- ไม่ --> Z3[block]
    F -- ใช่ --> S
    F -- ไม่ --> Z4[block]

เปลี่ยนจากเดิมและเปลี่ยนผลลัพธ์จริง: เดิมโค้ดถามว่า policy มีอะไรอยู่ข้างใน ไม่ใช่ถามว่า อะไร fail

policy ที่รวม role กับ claim ไว้ด้วยกัน โดยส่วน role ผ่านแต่ส่วน claim fail จึงถูกจัดเป็น “role fail” แล้วปล่อยผ่าน

⚠️ service ที่รัน LogOnly อยู่ จะเห็น request แบบนั้นถูก block ทั้งที่เมื่อก่อนผ่าน ตั้งใจให้รัดขึ้น

ที่ต้องแยกสองเส้น เพราะ endpoint ที่กำลังย้ายไป permission มักยังมี role gate ติดอยู่ด้วย ใช้เงื่อนไขเดียวแล้ว role ที่ fail บน endpoint พวกนั้นจะถูกปล่อยผ่านไปด้วย = รั่ว ส่วน role ล้วนบน endpoint ทั่วไปยังต้องปล่อยผ่านต่อ ไม่งั้น probe ของ security uplift ข้อ 1.5 ที่รันบน dev จะถูกปิดไปเงียบ ๆ

ทำไม CurrentSchemaVersion ยัง = 1 และห้ามขึ้น

field ใหม่สองตัว (revision, apiPermissions) เป็น nullable ที่ ไม่มีค่า default

⇒ cache ที่เขียนโดยของเก่าจะอ่านได้เป็น null ⇒ ด่านแยกออกว่า “ยังไม่พร้อม” ต่างจาก “พร้อมแล้วแต่ไม่มีสิทธิ์”

ถ้าใส่ค่า default ด่านนี้จะปลดอาวุธตัวเองเงียบ ๆ และถ้าขึ้น schema version cache ทุกก้อนในระบบจะ mismatch แล้วทั้ง fleet ตอบ 503 จนกว่า TTL 24 ชั่วโมงจะหมุนครบ

ตอนเพิ่ม cardId passportNo custCode amloStatus ก็ไม่ได้ขึ้น version เหมือนกัน

request ที่ไม่ตรง route ไหนเลย

อยู่ในขอบเขต ไม่ได้ถูกยกเว้น — ไม่มี endpoint = ไม่มี metadata = ไม่มีเครื่องหมายยกเว้น

การยกเว้นตัดสินจาก endpoint metadata ฝั่ง server เท่านั้น ไม่มีเงื่อนไขไหนที่ caller ทำเองแล้วหลุดออกจากขอบเขตได้ รวมถึงการไม่ส่ง header


9. รายการที่ค้างในตัว lib

ไม่เหลือแล้ว ณ db2ad07 ห้าข้อที่เคยค้างปิดครบ:

เคยค้างปิดที่
GetUserInfo() คืนทุก app ทุก role แม้ผ่าน gate9b018f2
[SkipAppContext] ไม่ได้ internal user idd123ca6
เส้น EnforceAppContext=false เสีย NameIdentifier บน endpoint ที่ผูก scheme0d8cf76
AddApiPermissions ทิ้ง policy provider ของ consumerca92c28
UserInfoItemKey ไม่มี prefix — จบที่ใช้ Type เป็น key แทน string3f0c2b4 + db2ad07

ที่ยังเหลือ ไม่ใช่งานของ lib: ฝั่งเขียนข้อมูล permission และ revision เป็นงานของ UserService ตราบใดที่ยังไม่มี EnforceAppContext ก็เปิดไม่ได้ (ข้อ 7)

มีอีกหนึ่งอย่างที่ตั้งใจทิ้งไว้: LegacyUserInfoItemKey ต้องถูกลบเมื่อไม่มี consumer ไหนอ่านชื่อเก่าแล้ว


อ่านต่อ