11.3 · Identity across systems
v1 Volume 8.9 เคยสรุปไว้ว่า legacy 3 service (vtrc-api, centralize-api, cu-central-api) ไม่มี canonical employee ID เดียว — มี userId (UUID, MariaDB), EmpID/EmpCode (MSSQL HRMI) map กันด้วย field refId ที่เป็น TEXT ไม่ enforce format
v2 พบว่าปัญหานี้ ไม่ได้ถูกจำกัดอยู่แค่ legacy — เมื่อขยาย scope ไปอีก 22 repo พบว่าแต่ละ domain microservice, 2way platform, meeting platform, mobile, และ recruitment pipeline ต่างมีนิยาม "identity" ของตัวเองเพิ่มเติม โดยไม่มีตัวไหนอ้างอิงกลับไปที่ canonical source เดียวกันอย่างเคร่งครัด
3 โดเมนของ "identity" ที่ต้องแยกกันคิด
| โดเมน | ตัวแทน | ระบบที่เป็นเจ้าของ |
|---|---|---|
| Employee identity | EmpID/EmpCode (HRMI), userId (vtrc), empCode claim ใน JWT (domain microservices) | HRMI (Chula) เป็น source ที่ authoritative ที่สุดสำหรับข้อมูลบุคคล แต่ไม่มี service ใด treat มันเป็น single source of truth ในทางเทคนิค |
| Candidate identity (pre-employment) | citizenId, birthDate, applicant record ใน recruitment-api/recruitment_ai/recruitment-web/vtrc-ocr-api | recruitment-api เป็นเจ้าของ record หลัก — candidate ยังไม่มี EmpCode จนกว่าจะผ่านการจ้าง |
| External/social identity | LINE userId (2way, chat-center), SSO external account (sso-api) | ผู้ให้บริการภายนอก (LINE Platform, external SSO provider) — VTRC เก็บ mapping ไปหา employee identity เท่านั้น |
3 โดเมนนี้ทับซ้อนกันตรงจุดที่พนักงานคนเดียวมี LINE account, ใช้ SSO, และเคยเป็น candidate มาก่อน — แต่ไม่มี service ใดเก็บ mapping ครบทั้ง 3 โดเมนในที่เดียว
Employee identity — identity graph (ขยายจาก v1 Volume 8.9)
┌────────────────────────────────────────────────────────────────┐
│ HRMI dbHRMI_Center (MSSQL, Chula) — authoritative แต่ไม่ enforced │
│ Employee.EmpID (PK, STRING 36) + Employee.EmpCode (STRING 50) │
└───────────────────┬──────────────────────────────────────────────┘
│ EmpCode (string match, ไม่มี FK ข้าม engine)
┌────────────┼─────────────────┬─────────────────┬───────────────┐
▼ ▼ ▼ ▼ ▼
┌───────────────┐ ┌────────────┐ ┌─────────────┐ ┌───────────────┐ ┌─────────────┐
│vtrc-centralize│ │ benefit DB │ │recruitment DB│ │ pms DB │ │ workflow-api│
│.Users.refId │ │.empCode │ │(candidate → │ │.empCode │ │ (ไม่มี DB │
│(TEXT, MariaDB)│ │(string col)│ │ employee หลัง│ │(string col) │ │ ของตัวเอง │
└───────────────┘ └────────────┘ │ จ้างงาน) │ └───────────────┘ │ อ่าน empCode│
└─────────────┘ │ จาก 4 DB │
└─────────────┘
│
│ JWT payload มี empCode claim (shared secret 4 service — ดู 11.4)
▼
┌─────────────────────────────────────────────────────────────────┐
│ benefit-api / recruitment-api / ai-recruitment-api / pms-api │
│ — เชื่อ empCode จาก JWT payload โดยตรง ไม่ re-verify กับ HRMI │
└─────────────────────────────────────────────────────────────────┘จุดอ่อนที่เพิ่มจาก v1
- JWT payload กลายเป็น "identity provider" โดยพฤตินัย —
benefit-api,recruitment-api,ai-recruitment-api,pms-apiทั้ง 4 ตัว decode JWT ที่เซ็นด้วย secret เดียวกันแล้วอ่านempCodeตรงจาก payload โดยไม่ query กลับไปยืนยันกับ HRMI ว่าempCodeนั้นยังใช้งานอยู่จริง (ดู Volume 6 recruitment-api/04-auth-integrations, ai-recruitment-api/04-auth-integrations) — ถ้า secret รั่ว ผู้โจมตี forgeempCodeอะไรก็ได้ในระบบทั้ง 4 ตัว workflow-apiเป็นจุดเดียวที่เห็น empCode ของทั้ง 4 owner พร้อมกัน (vtrc, HRMI ×2,benefit) ผ่าน connection โดยตรง — แต่ตัวมันเองไม่มี JWT guard (SEC-WORKFLOW-01) จึงเป็น "single point of identity confusion" ที่มีความเสี่ยงสูงสุดvtrc-centralize.Users.refIdยังเป็น TEXT ไม่ enforce format เหมือนที่ v1 พบ — ปัญหาเดิมยังไม่ถูกแก้ใน v2
Candidate identity — namespace ที่แยกจาก employee โดยสิ้นเชิง
recruitment-api เก็บ candidate record ที่มี citizenId, birthDate เป็น PK-adjacent field ก่อนที่จะมี EmpCode — พบ 2 ความเสี่ยงที่เกี่ยวกับ identity โดยเฉพาะ:
recruitment_ai.dbo.jobReqPositionเป็นสำเนาคู่ขนานที่ augmented ของrecruitment.dbo.jobReqPosition(ดู 11.2 #12) — ไม่ใช่ปัญหาเรื่อง employee identity แต่เป็นปัญหาเรื่อง "record identity" (ตำแหน่งงานเดียวกันมี 2 primary record คนละ database)report-logcommitrequest_*.logที่มี citizenId, birthDate, empCode ของบุคคลจริงเข้า git (SEC-REPORTLOG-01, Critical) — แปลว่า candidate/employee identity รั่วออกจากทั้ง 3 โดเมนพร้อมกันในไฟล์เดียว เพราะ log อาจถูกเขียนจากทั้ง flow recruitment และ flow HR
External/social identity — LINE userId และ SSO account
| ระบบ | Identity ที่เก็บ | Mapping ไปหา employee identity |
|---|---|---|
2way-api / 2way-vtrc-api (userLine table) | LINE userId | map ไปหา empCode — แต่ 2way-vtrc-api's POST /2way/getProfile ไม่มี auth guard (SEC-2WAYVTRC-02, Critical) ทำให้ mapping นี้ query ได้โดยไม่ต้อง login |
chat-center-api (LINE webhook) | LINE userId | cross-schema query ไปหา dbHRMI_Center.dbo.temp_pee_na_table — ตารางที่ชื่อขึ้นต้นด้วย temp_ ถูกใช้เป็น identity bridge แบบ production ทั้งที่ชื่อบอกว่าเป็น scratch table |
sso-api | external SSO account | cross-schema query ไปหา temp_pee_na_table ตารางเดียวกันกับที่ chat-center-api ใช้ — สอง repo คนละกลุ่มพึ่งพา identity bridge ตัวเดียวกันที่ไม่มี schema contract |
meeting-vtrc-api (login table, "game-like fields" ตามที่ Volume 7 บันทึกไว้) | ไม่ชัดเจนว่า map กับ employee identity ผ่านช่องทางไหนโดยตรง — ดึงจาก "TwoWay" employee directory ภายนอกผ่าน cdbConnector (สงสัยว่าคือ cu-central-api แต่ยัง UNVERIFIED) | ดู Volume 7 meeting-vtrc-api/03-persistence-integrations |
temp_pee_na_table เป็นจุดที่ควรระวังที่สุดในหมวดนี้ — เป็น shared identity bridge ระหว่าง 2 repo คนละ platform group (chat-center-api ↔ Domain Microservices, sso-api ↔ Domain Microservices) โดยไม่มี document, ไม่มี versioning, และชื่อบอกเป็นนัยว่าไม่ได้ตั้งใจให้เป็น stable interface
Token/session-layer identity (frontend + mobile)
ทุก frontend (vtrc-web, vtrc-backoffice, vtrc-backoffice-new, vtrc-rc-backoffice) และ mobile app (vtrc-mobile, new-vtrc-mobile) เก็บ JWT ที่ payload มี id/uid (ตาม Volume 3 vtrc-api/03-auth-session) — เป็นตัวแทน vtrc-centralize.Users.userId เดียวกัน ไม่ใช่ identity ใหม่ แต่ ที่เก็บ token ต่างกันทำให้ blast radius ต่างกัน:
| App | ที่เก็บ token | risk |
|---|---|---|
vtrc-backoffice, vtrc-backoffice-new | localStorage (ไม่มี expiry เพราะ hardcoded isRemember=1) | token คงอยู่ตลอดไปแม้ user ปิด browser — ดู Volume 4 vtrc-backoffice/04-scorecard |
vtrc-mobile, new-vtrc-mobile | AsyncStorage (plaintext ใน new-vtrc-mobile, regression จาก vtrc-mobile) | ถ้า device ถูก root/jailbreak อ่าน token ได้ตรง — ดู Volume 8 new-vtrc-mobile/02-scorecard |
2way-meeting-backoffice | localStorage | เดียวกับ backoffice — ดู Volume 5 2way-meeting-backoffice/04-scorecard |
สรุป — canonical identity source มีจริงหรือไม่
ไม่มี ทั้งในทางเทคนิคและทางองค์กร — คำตอบเดียวกับ v1 แต่ scope กว้างขึ้น:
| Layer | Stable ID | เปลี่ยนได้ไหม | ใครถือเป็น source of truth ในทางทฤษฎี | ใครถือเป็น source of truth ในทางปฏิบัติ |
|---|---|---|---|---|
HRMI EmpID | stable | ไม่เปลี่ยน | HRMI (Chula) | HRMI (Chula) — ตรงกับทฤษฎี |
HRMI EmpCode | unstable | เปลี่ยนตาม org policy | HRMี (Chula) | HRMI เขียน, แต่ 6+ repo cache ค่าเก่าไว้ในตัวเอง (refId, empCode column ต่าง ๆ) โดยไม่มี sync event |
vtrc.Users.userId | stable | ไม่เปลี่ยน | vtrc-api | vtrc-api — แต่ frontend/mobile หลายตัวเก็บ token แยกกันไม่มี central revocation |
JWT empCode claim | derived, ไม่ verify ซ้ำ | ตามอายุ token | ไม่มีเจ้าของจริง — เป็น "fact" ที่ trust โดยไม่ตรวจ | 4 domain microservice เชื่อ claim นี้ตรง |
LINE userId | stable (จาก LINE) | ไม่เปลี่ยน (เว้นแต่ unlink) | LINE Platform | chat-center-api/sso-api ผ่าน temp_pee_na_table ที่ไม่มี contract |
ข้อเสนอ modernization (canonical employeeId เดียว, event-driven identity sync) เหมือนที่ v1 เคยเสนอไว้ใน Volume 8.12 — ยังใช้ได้ และตอนนี้มี evidence เพิ่มว่าปัญหาขยายไปทั่วทั้งแพลตฟอร์ม ไม่ใช่แค่ legacy core รายละเอียด debt ID ดู 11.5 Scorecard
secret ที่ทำให้ JWT payload กลายเป็น "identity provider" ได้ (เพราะ secret เดียวกันข้าม 4 service) อยู่ที่ 11.4 Shared secrets & credentials →