ADR-013: 代理與模擬——ActingContext 框架能力
ADR 編號: 013 狀態: 已接受 (Accepted) 決策日期: 2026-07-08 決策者: Development Team 取代: 無 被取代: 無
摘要
框架以最薄的機制面支援「代理(Delegation)」與「模擬(Impersonation)」:提供 ① 簽發帶代理/模擬身份的 JWT、② 標準 claim schema、③ ActingContext 讀取工具、④ request-level acting 稽核事件能力。啟用時機、閘門、授權與啟用記錄、資料表、URL policy、instance 級過濾一律留給應用層(app-server / app-office 出參考實作),框架不提供業務表格與端點。
關鍵設計(無模式判別器):代理性與模擬性是兩個正交維度——模擬性由
actorclaim 是否存在表達、代理性由grantorsclaim 是否非空表達,兩者彼此獨立、可同時成立於同一 token(如「A 模擬 B,而 B 本身被 C 指派代理」→actor與grantors並存)。框架不設act_mode這類單一模式判別器(它承載零額外資訊、且天生表達不了兩維共存);是否共存由應用層簽發政策決定。
背景 (Context)
問題陳述
多數建於 appfuse 上的應用都需要兩種「以他人身份/權限操作系統」的能力,且語意根本不同:
- 代理(Delegation):登入者透過取得授權人的權限操作系統。對系統而言,實際執行者仍是登入者——權限解析須合併授權人的權限,稽核執行者仍是登入者,代理的事實由「代理本身的記錄」分辨。授權人可以多個。
- 模擬(Impersonation):登入者透過取得模擬對象的身份操作系統。對系統而言,實際執行者是被模擬對象——沒有權限合併問題,稽核執行者是被模擬對象,模擬的事實由「模擬本身的記錄」分辨。一次只能是一個身份。
首個消費者為 ict(ownership 隔離),次個為 florist-flora(tenant 隔離)。這是跨應用共通、且必須同時適用兩種資料隔離模式的機制——屬框架先行範疇,不應在各 app 各自重造。
框架現況(見 ADR-009):
- JWT 為簽發當下的快照、per-request 無狀態、不回查 DB;權限放在
authclaim,每次請求由 converter 還原成 authorities。 - 稽核 auditor(
AuditorAware)只讀authentication.getName(),即 JWT 的sub。 - tenant/ownership 的資料可見性都 key 在 principal 身份上:
tenant讀tenantIdclaim 餵TenantContext+ Hibernate@Filter;ownership由 service 層依身份/角色/擁有權過濾(見 ADR-001)。 - 完全沒有任何 delegation / impersonation / act-as / switch-user 設計。
限制條件
- 框架只擁有 tenant 隔離:tenant 以下的 resource / instance 級限制由應用層自理(既有設計哲學)。故代理下「看得到哪些資料列」的合併是應用層職責,框架僅需把授權人 subject 帶進 token 供其使用。
- JWT 無狀態快照:代理合併後的權限、代理/模擬身份都必須在簽發當下寫進 claim;per-request 不回查 DB。
- auditor 讀
sub:若代理維持sub = 登入者、模擬切sub = 被模擬者,則稽核@LastModifiedBy自動正確,無須改 auditor 預設。 - 各 app 政策差異大:何時可代理/模擬、誰可對誰、如何記錄,因應用而異——框架若硬塞資料表或端點會限制適應性。
- 撤銷機制既有:
sessionIdclaim + token 黑名單可即時撤銷整組 token,代理/模擬 session 應可沿用。
假設前提
- 代理與模擬的啟用是一個特權動作,其閘門(誰能做)由應用層以既有
@PreAuthorizeauthority 控制。 - 「代理/模擬的事實」只要被應用層記錄下來且可查,即足以在稽核時推算,不需在每筆資料列額外標記真實操作者。
- 權限高低是應用層主觀決定的,框架無客觀依據判斷「最高權限者」。
考量的方案 (Options Considered)
方案 A: 不提供框架支援,各 app 自建
說明: 框架不介入,ict、florist-flora 各自在 app 層實作 token、權限合併、身份切換與稽核。
優點:
- ✅ 框架零改動
缺點:
- ❌ 跨應用共通機制重複造,違反框架先行
- ❌ token claim 形狀各 app 不一致,稽核/工具無法共用
- ❌ 權限合併、身份切換牽涉密碼學與 SecurityContext 細節,各 app 各自弄錯的機會
評分: 1/5
方案 B: 框架提供完整代理/模擬子系統
說明: 框架出授權表、啟用記錄表、enter/exit 端點、閘門 authority、預設政策,一套到位。
優點:
- ✅ app 幾乎零工即可用
- ✅ 跨 app 行為一致
缺點:
- ❌ 授權模型(誰可對誰、scope、時限)與持久化因應用而異,框架硬塞表格會限制設計
- ❌ 端點/政策進框架後,retarget 彈性差
- ❌ 與「框架只擁有 tenant 隔離、resource 級留 app」的既有邊界矛盾
評分: 2/5
方案 C: 框架只提供 token 機制 + claim schema + ActingContext(採用方案)
說明: 框架交付三件事、且僅此三件:
- Token 簽發擴充:
JwtTokenProvider可產生- 代理 token:
sub維持登入者、auth併入指定 ROLE 展開的權限、帶grantorsclaim(授權人 stable subject 集合); - 模擬 token:
sub/auth/tenantId換成被模擬者、帶actorclaim(真實操作者); - 兩者沿用
sessionId。兩維 claim 無 key 衝突、可 merge 進同一 token。載入被代理/被模擬者、選哪些 ROLE 由應用層解析後傳入,框架只負責正確地簽。
- 代理 token:
- Claim schema 契約:制定兩個正交 claim 名(
grantors、actor),是「簽發端 ↔ converter ↔ ActingContext」round-trip 的唯一約定;無模式判別 claim。 ActingContextinterface + 讀取工具:讀當前 JWT,在框架 claim 邏輯下吐出{subject, grantors[], actor}與兩正交述詞isImpersonating()/isDelegating(),作為應用層寫「事實記錄」、做 instance 級過濾、推算稽核的原料。
啟用時機、閘門 authority、授權與啟用記錄、資料表、instance/resource 級過濾全部由應用層設計,app-server / app-office 出參考實作。
優點:
- ✅ 框架保持薄,最大化適應各種授權/持久化設計
- ✅ 與「框架只擁有 tenant 隔離、resource 級留 app」邊界一致
- ✅ claim schema 統一,稽核/工具跨 app 共用
- ✅ 模擬「換身份」靠既有 tenant/ownership 機制免費成立(
sub/tenantId一換就跟著走) - ✅ 代理的權限合併集中在框架簽發與 converter,密碼學/SecurityContext 細節不外流
缺點:
- ❌ app 需自建授權/啟用記錄與端點(由參考實作降低成本)
- ❌ 稽核靠推算(方案 A 稽核,見權衡),資料列不直接顯示「憑代理權執行」
評分: 5/5
決策 (Decision)
選擇方案: C(框架只提供 token 機制 + claim schema + ActingContext)
核心理由:
- 薄框架、最大適應性:授權模型與持久化因應用而異,框架不塞表格與端點,才能同時服務 ict(ownership)與 florist-flora(tenant)及未來各種設計。
- 對齊既有邊界:框架只擁有 tenant 隔離、resource/instance 級留應用層——代理的資料 scope 合併因此是應用層工作,框架只需把授權人 subject 帶進 token。
- 模擬幾乎免費:既有 tenant/ownership 機制都 key 在 principal 身份,模擬換掉
sub/auth/tenantId即自動跟隨,框架不改邏輯。 - 稽核自動正確:
sub語意(代理=登入者、模擬=被模擬者)讓AuditorAware免改即正確;ActingContext供應用層補記「事實」。
兩維正交(設計核心)
代理與模擬是兩個彼此獨立、可自由組合的維度,不是一個要三選一的模式。各維度自身的 claim 對 sub/auth/tenantId 的效果不對稱:
| 代理 Delegation(代理維) | 模擬 Impersonation(模擬維) | |
|---|---|---|
| 由哪個 claim 表達 | grantors 非空 | actor 存在 |
sub(稽核身份) | 維持登入者 A | 切成被模擬者 B |
auth(權限) | A ∪ 借用 ROLE 的權限 | = B 的權限 |
tenantId | 維持 A(不跨租戶) | 換成 B(可跨租戶) |
| 額外 claim | grantors(多筆,0..n) | actor(單筆,真實操作者 A) |
| 對象數 | 多 | 單 |
| 資料可見性合併 | 應用層用 grantors 折進 ownership 過濾 | 隨 B 身份自動成立 |
- 兩維可共存於同一 token:
grantors與actor是獨立 claim,無互斥、無act_mode仲裁。判別靠存在性——actor在 → 模擬中、grantors非空 → 代理中,兩者可同時為真。 - 跨租戶:代理不跨租戶(技術上
TenantContext/Hibernate filter 為單租戶,且實務罕見);模擬可跨租戶(管理/客服超能力)。模擬不取代代理,兩者用途不同。 - 鏈式與提權由共存自然成立:框架不做任何管控——「可否代理/模擬」是應用層授的一個 authority,那是唯一閘門;過閘門後目標選擇與鏈式(代理中模擬、模擬中取得代理權)皆自由,因為兩維本就正交、可並存。框架亦不判「最高權限者」(權限高低為應用層主觀)。
兩個共存情境(皆為應用層政策,框架不預設)
- 情境一:A 模擬 B,而 B 本身被 C 指派代理 → 應用層簽出
sub=B, actor=A, grantors=[C], auth=B∪C(兩維並存,你以 B 身份操作、且握有 B 借自 C 的權限)。 - 情境二:A 被 C 指派代理,A 去模擬 B → 應用層簽出
sub=B, actor=A,不帶grantors(身份換成 B,A 自己的代理權不隨身份過去)。
情境一 vs 二的差別,完全由應用層決定要不要把 grantors 放進模擬 token——框架的機制對兩者都成立。
Claim schema
| claim | 代理 | 模擬 | 說明 |
|---|---|---|---|
sub | 登入者 A | 被模擬者 B | 既有;auditor 讀此 |
auth | A ∪ 借用 ROLE 權限 | B 的權限 | 既有;converter 還原 authorities |
tenantId | A 的 tenant | B 的 tenant | 既有;TenantContext 解析 |
sessionId | 有 | 有 | 既有;黑名單撤銷 |
grantors | [stable subject, …] | —(除非共存情境一) | 新增;代理維,非空即代表代理性 |
actor | —(除非共存情境二不適用) | stable subject | 新增;模擬維,存在即代表模擬性 |
無
act_mode:模式判別完全由grantors/actor的存在性承擔——act_mode承載零額外資訊,且單一值天生表達不了兩維共存,故不設。安全性不靠它把關:token 完整性由 JWT 簽章保證,無法在不重簽(必經JwtTokenProvider)的情況下注入假 claim。身份一律以不可變 stable subject 識別(對齊
sub)。AuthPrincipal.subject()是簽發來源,grantors/actor與 auditor/ownership 皆使用同一 stable key; username、email、顯示名等可變欄位只在 application reference boundary 經PrincipalDirectory解析,不進 framework claim contract。Refresh Session family 以
subject/actorSubject保存身份維度;refresh 時重載目前 principal 與 delegation, 並重驗SessionPolicyBinding的期限、必要 actor authority 與應用SessionEligibilityPolicy, 再重鑄grantors/actor/actingPolicyclaim。Refresh Credential 本身是不透明 secret,不承載 JWT claim。
ActingContext interface
public interface ActingContext {
String getSubject(); // 有效身份 stable subject(代理=A、模擬=B,即 JWT sub)
List<String> getGrantors(); // 代理授權人 stable subject 全集(0..n;無代理回空)
String getGrantor(); // 單數便利:>1 丟 IllegalStateException、0 回 null
String getActor(); // 模擬者 stable subject;非模擬時回 subject 自身
String getSessionId(); // access/refresh family id
String getActingPolicy(); // 應用政策識別
boolean isImpersonating(); // 帶 actor claim
boolean isDelegating(); // 帶非空 grantors;可與 isImpersonating() 同時為 true
}
- 身份型別即
String(stable subject)——不另立身份型別,契約最薄。 - 無
getMode()/ActingMode:兩維由isImpersonating()/isDelegating()獨立回報,可同時為 true。 getGrantor()是給「只支援單一授權人」的應用的便利存取:存在多筆卻以單數存取即丟例外(不允許把多筆當一筆讀),零筆回null。getActor()在模擬時回actorclaim,其餘情況回subject自身(真實操作者恆為當前 principal)。
權衡分析 (Trade-offs)
我們獲得什麼 (Gains)
- ✅ 跨應用共通的 token/claim 契約,稽核與工具可共用
- ✅ 框架薄、無表格無端點,適應各種授權與持久化設計
- ✅ 模擬靠既有 tenant/ownership 機制免費成立
- ✅ 稽核
@LastModifiedBy免改即正確
我們放棄什麼 (Losses)
- ❌ app 需自建授權/啟用記錄與 enter/exit 端點(參考實作降低成本)
- ❌ 方案 A 的預設稽核:業務資料列只記有效身份(代理=A、模擬=B),「A 憑 B 的代理權執行」不直接塞進每列。存取行為由 request-level
AuditEvent記錄;若需對 Entity 各版本直接關聯 actor/grantors,另選配 changeset 級 Entity history(R3),仍不擴充所有業務 Entity。
風險與緩解措施 (Risks & Mitigations)
| 風險 | 嚴重性 | 機率 | 緩解措施 |
|---|---|---|---|
| 代理權限快照過時(授權人權限中途變動) | 中 | 中 | 同一般登入的既有取捨;短 token TTL + sessionId 黑名單重新換發 |
| 各 app claim 形狀分歧 | 中 | 低 | claim schema 由框架制定為契約;ActingContext 為唯一讀取路徑 |
| 應用層漏掛 request audit hook,導致未改資料的 acting 存取沒有軌跡 | 中 | 中 | reference feature 統一掛載;整合測試驗證事件落庫 |
| 誤把模擬當跨租戶代理使用 | 低 | 中 | 文件明定代理不跨租戶、模擬才跨租戶,用途不同 |
| 代理 token 洩漏放大受害範圍 | 中 | 低 | 併入的僅為選定 ROLE、非全權;sessionId 可即時撤銷;短 TTL |
影響 (Consequences)
正面影響
- ➕ ict / florist-flora 及未來應用共用同一 token 機制與工具
- ➕ 框架攻擊面小(無新表、無新端點)
- ➕ 稽核正確性由
sub語意自動保證
負面影響
- ➖ app 首次導入需實作端點、閘門、記錄(參考實作緩解)
- ➖ 稽核靠推算,非每列自解釋
中性影響
- 🔸
AuditorAware維持不變(讀sub),ActingContext 為新增旁路,不改既有稽核預設 - 🔸
TenantContext既有runAs/tenant 解析被模擬複用,非新機制
實作指南 (Implementation Guidelines)
必須遵守的規則
- acting 核心只交付四件:token 簽發擴充、claim schema、
ActingContext讀取工具、ActingAuditInterceptor事件解算。中立 Entity history context/adapter 是另一個獨立 optional capability;兩者都不得在框架加入授權表、啟用記錄表、enter/exit 端點、URL policy 或閘門 authority。 - 授權人/被模擬者的解析在應用層:框架簽發 API 接受「已解析的授權人 stable subject/借用 ROLE」或「已解析的被模擬者身份」為輸入;載入帳號、選 ROLE、決定是否允許為應用層職責。
- claim schema 為契約、兩維正交:
grantors/actor為唯一約定、無act_mode,簽發端與ActingContext讀取端一致;兩 claim 無 key 衝突、可 merge 進同一 access token;Refresh Session 保存 actor context 並於 refresh 重鑄。 - 代理維持
sub、模擬切sub:確保AuditorAware自動正確;框架不為此新增 auditor 分支。 - 代理不跨租戶、模擬可跨租戶:代理 token 維持登入者
tenantId;模擬 token 換成被模擬者tenantId。 - 通用生命週期管控:框架禁止 nested impersonation、於 refresh 重驗 subject/actor 狀態與
policy binding,並提供
SessionEligibilityPolicy與依 policy reference 主動撤銷 family 的接點; 角色、保護帳號、consent 與 break-glass step-up 仍由應用層政策決定。 - 撤銷沿用
sessionId:代理/模擬 token 帶sessionId,可經既有黑名單即時撤銷。
建議的最佳實踐
- acting 存取稽核軌不可省:Entity history 只在 transaction 實際變更 audited Entity 時產生 revision;未修改資料的讀取、拒絕與 HTTP 結果仍須由 request-level hook(
ActingAuditInterceptor)+ 持久AuditEventRepository記錄——不需 session 表(JWT 每請求自帶脈絡)。 - instance 級過濾讀
getGrantors():ownership 應用把授權人 subject 集合折進自己的擁有權過濾;tenant 應用同租戶多半免處理。 - 前端明示(兩級、各綁一個維度,可同時顯示):模擬覆蓋 identity(
sub=被模擬者),由isImpersonating()觸發醒目橫幅「模擬中:B」+ 結束鍵,結束以actor換回登入者 token;代理身份未變(sub=行為者自身、只是多了權限),由isDelegating()觸發低調指示器(Header chip「代理中:{grantors}」,純顯示、無停止鍵——grant 模型下代理人不能自行卸下,關閉由授權人撤銷 grant)即可,不用全幅橫幅。兩述詞獨立判定 → 前端天生支援同時顯示橫幅+指示器(框架容許兩維共存);惟參考實作為維持兩功能獨立、模擬保持純粹,florist 不會由模擬觸發共存(見「參考實作」節)。判斷句:帶actor→ 橫幅;grantors非空 → 指示器;兩者各自獨立判定。 - 短 TTL 降低快照過時與洩漏風險。
檢查清單
-
JwtTokenProvider具代理/模擬簽發能力;兩維 claim 可 merge 進同一 access token,Refresh Session family 可保存並重鑄 actor context -
ActingContext以isImpersonating()/isDelegating()兩正交述詞回報(無getMode()/ActingMode),語意(兩維可共存/多筆單數例外/actor 退化)與本 ADR 一致 - 框架未新增授權表、啟用記錄表、端點、閘門 authority、
act_mode模式判別 claim - 代理
sub=登入者且不跨租戶;模擬sub/tenantId=被模擬者 - 參考實作(app-server/app-office)示範端點、閘門 authority、授權關係/存取級 acting 稽核軌、ownership 折入、前端切換 UI
- 文件區分
AuditableBase、optional Entity history 與 request-levelAuditEvent;未修改資料的 acting 存取仍由 request audit 軌記錄
參考實作 (Reference Implementation)
框架只出機制;下列政策與持久化由 app-server / app-office 出參考實作,屬 @reference-surface(見 m-reference-code.md),下游 retarget:
兩功能獨立、可各自 cherry-pick:Impersonation 與 Delegation 各有獨立的 controller / service(
ImpersonationController/Service、DelegationController/Service),彼此零依賴——下游只想要其一時整組拿走即可運行。GET /auth/acting(當前代行脈絡)是兩者共用的中性讀取面,由獨立的ActingController直接讀框架ActingContext(只用模擬 →delegating恆 false;只用代理 →impersonating恆 false)。
- 模擬端點(
ImpersonationController):一般 consent 路POST /auth/impersonation、獨立緊急路POST /auth/impersonation/break-glass、案件結案/family 撤銷端點、exit 與受impersonation:enter保護的 candidates。break-glass 要求 IdP MFA step-up、理由、案件編號、 OPEN case 與短效 family;一般端點不因 break-glass authority 自動繞過 consent。Reference 不提供會假裝完成 MFA 的 local UI/mock。純模擬——以被模擬者自身 authorities 簽發, 不併入其代理權。 - 代理端點(
DelegationController,grant 模型,見下節):有界、可篩 active/history 的GET /auth/delegation/grants、POST/DELETE /auth/delegation/grants(授權人管理其代理人)+GET /auth/delegation/candidates。代理人無 exit 端點——grant 模型下代理人不能自行卸下 被授予的權限,關閉代理只能由授權人DELETE /grants/{id}撤銷或代理人登出。 登入整合:/auth/login於代理人登入時套用其收到的有效授權(呼叫generateDelegationToken)。 - 代行脈絡端點(
ActingController,中性):GET /auth/acting回{impersonating, delegating, subject, actor, grantors},兩功能共用;subject/actor/grantors是為 UI 投影的 username(查無時退回 stable subject)。JWT 與ActingContext本身始終只承載 stable subject。 - 應用身分解析器(
service/auth/AuthenticationIdentityResolver,structural):集中區分 stable subject 與可變顯示資料。currentUserId(authentication)直接回Authentication#getName()(JWTsub/Account ID,不查 DB);findBySubject、usernameOrSubject、displayNameOrSubject才經框架PrincipalDirectory查目錄。它依賴應用帳號模型、屬可 retarget 的 application structural code,不是 framework capability、Feature core 或 local seam;下游若有以 username 關聯的既有資料,可在自家 resolver 增加明確的相容方法,參考實作不預設這個 legacy 契約。 - 候選名單依功能拆兩端點:
GET /impersonation/candidates(同租戶 / 跨租戶能力)與GET /delegation/candidates(同租戶 / 已指派),各由自己的 service 過濾——不再用單一?mode=混流。 - 閘門 authority:
impersonation:enter(+跨租戶impersonation:cross-tenant)守模擬;代理指派為個人動作,參考實作以isAuthenticated()守(名稱與粒度由參考實作定,非框架)。 - 稽核(三層獨立互補、無 session 表):① 目前狀態 JPA Auditing(
AuditableBase,AuditorAware讀sub→@LastModifiedBy,不動)。② Entity changeset optional Entity history(只對@AuditedEntity 保存版本;revision metadata 記 stablesubject/actor/grantors)。③ 存取級 request-level acting hook(ActingAuditInterceptor讀ActingContext:代行脈絡下的每個請求發ACTING_ACCESS,轉場發IMPERSONATION_STARTED/ENDED、DELEGATION_APPLIED)→PersistentAuditEventRepository持久落audit_event表。兩張 acting 專屬 session 表已移除;代理的授權關係DelegationGrant另行持久化(活的授權、非稽核)。 - 共存情境不 wire:框架容許「模擬 B 且帶 B 的代理維」(把
ActingClaims.delegation(...)merge 進模擬的extraClaims),但參考實作刻意不 wire——為維持兩功能獨立、可各自 cherry-pick。需要該組合的下游自行在enter併入即可。 - instance 級過濾:ownership 應用讀
getGrantors()(代理時=授權人集合)折入擁有權過濾。 - 前端(app-office-mockup → app-office):模擬經「切換身份」對話框(候選 Select)進入,
isImpersonating()→ 醒目橫幅「模擬中:B」+ 結束鍵;代理由授權人在「我的代理人」設定頁指派,代理人登入即生效,isDelegating()→ 低調指示器(Header chip「代理中:{授權人}」,純顯示無停止鍵;關閉由授權人撤銷 grant)。兩述詞獨立、前端天生支援同時顯示,但 florist 不 wire 共存故不會由模擬觸發。
代理啟用:授權(grant)模型
代理只有一種啟用方式——授權(grant)。 「代理」的語意即「權限持有者把權限授給他人代為行使」;權限由上而下從授權人流出,不存在「受益人自行挑選要借誰的權限」這回事(那不是代理,是自我提權)。因此代理一律是:授權人(grantor)指派代理人(grantee)→ 代理人登入時自動取得授權人的權限。
| 面向 | 授權(grant) |
|---|---|
| 誰發起 | 授權人指派代理人 |
| 何時生效 | 代理人登入時自動 |
| 持久性 | 持久授權關係(跨 session,直到撤銷) |
grantors claim 語意 | 授權人(權限來源) |
- 參考實作(florist):授權人 A 在「我的代理人」指派代理人 B(
DelegationGrant);B 登入時/auth/login查其收到的授權、合併 A 的權限、以generateDelegationToken(B, [A], …)簽 token。B 身份不變(sub=B)、多了 A 的權限,grantors=[A]。 - 框架的 token/claim 機制只負責把「合併後的權限 +
grantorsclaim」簽進sub維持登入者的 token;授權關係的持久化、誰可指派誰、撤銷政策是應用層職責(DelegationGrant等)。ActingContext.getGrantors()回的一律是「授權人集合」。
florist(tenant)為首個 grant 參考實作;ownership 應用(如 ict)可依相同框架機制實作各自的授權政策,驗證同一機制在兩種隔離模式下皆成立。
界線修訂 (Boundary Revisions)
方案 C 選定後的實作與檢討中,發現原「機制 vs 政策」二分需精煉。本節記錄由框架維護者在框架 repo 內直接發起的界線調整(非下游提案,故不走
/rfc——/rfc是下游→框架的收件口;維護者側以本 ADR 修訂 + framework CHANGELOG/版本紀律取代)。
R1(2026-07-08):通用稽核基礎設施升入框架
背景:方案 C 把「機制」留框架、「政策+持久化」全推應用層。但參考實作的稽核落點裡,有一塊是領域無關的通用基礎設施——持久化 Actuator AuditEvent 的 sink(audit_event 表 + AuditEventRepository 實作)。它與 cache/mail 同性質:不碰 app 的 User/Role/租戶模型,任何 app 都想要。留給每個下游自建 = 重複造輪子,違反框架先行。
修訂後的界線(二分 → 三分):
| 類別 | 歸屬 | 例 |
|---|---|---|
| 機制 primitive | 框架(已在) | ActingContext/ActingClaims/JwtTokenProvider 代行簽發/ActingAuditInterceptor 事件解算 |
| 領域無關基礎設施 | 框架(opt-in 服務,本次新增) | 持久 AuditEventRepository + audit_event(通用稽核 sink) |
| 授權政策+領域持久化 | 應用層 reference domain(retarget) | 模擬/代理 service、DelegationGrant 表、閘門 authority、端點、acting 稽核 hook 的 URL 範圍、前端;Feature 僅留 DelegationApplication 等窄契約 |
判準(沿用
m-methodology-hygiene的 primitive-vs-convention 精神):碰不碰 app 的身份/授權/領域模型? 不碰(如稽核 sink 只存AuditEvent)→ 基礎設施 → 可升框架;碰(如 grant 的 ROLE→authority 合併)→ 政策 → 留應用層。方案 C 當初否決「方案 B:框架提供完整子系統」的理由(授權模型因應用而異)只約束政策層,不約束領域無關的基礎設施。
本次升入框架的範圍:
- 升:通用持久稽核 sink——
AuditEventRecordBase+ 泛型 JPA repository base +PersistentAuditEventRepository(implements ActuatorAuditEventRepository)。具體 entity 與 repository 依 ADR-017 留在應用 reference domain。非 acting 專屬:Spring Security 登入/授權事件、acting 事件皆可落此 sink。 - opt-in 模式(對齊 cache/mail):框架只提供持久 capability;audit Feature 提供
in-memory fallback,需要持久化時才由應用 reference domain 以
AuditPersistenceConfig提供 repository provider。Spring Boot 本身不會自動建立 in-memory repository。 - 升固定機制、保留應用政策:
ActingAuditInterceptor的事件解算升 jar;掛載哪些 request 路徑仍由 referenceservice/acting/ActingWebConfig決定。模擬/代理 service/端點/閘門 authority、DelegationGrant、前端留應用層。模擬(token 流程) 可作後續獨立評估的框架 starter(對齊 Spring SecuritySwitchUserFilter);代理 grant 因深度耦合授權模型,維持參考實作。
影響:
- 新增
io.leandev.appfuse.audit.*公開 API → 觸發appfuse-serverCHANGELOG 條目、版本 bump、下游/upgrade-appfuse-server。 - app-server 參考實作改為消費框架版,移除自建的
AuditEventRecord/repository/PersistentAuditEventRepository與 acting 事件解算拷貝;只保留持久 sink 與 URL 掛載政策。 - 未 opt-in 的下游行為不變。
R2(2026-07-28):stable subject 與授權人語意收斂
背景:認證邊界已由可變 username 收斂為 AuthPrincipal.subject(),而 delegation grant
模型中 token 集合實際承載的是「把權限授給目前登入者的人」,不是代理人本身。原
delegates 名稱同時造成身份穩定性與方向語意錯置。
決策:
- JWT 以
grantors取代 pre-release 的delegates;actor、grantors、sub一律承載 stable subject。 - 公開 API 改為
CLAIM_GRANTORS、getGrantors()、getGrantor();解析時 trim、 去除空值與重複 subject。 - application reference 以 structural
AuthenticationIdentityResolver包裝PrincipalDirectory;ActingController只在 application wire boundary 將 stable subject 投影為 username(查無時保留原 subject),其他 application wire 若需要顯示名稱則使用同一 resolver,不污染 framework claim contract。 Authentication#getName()與ActingContext#getSubject()都是 stable subject,不是 username。需要 ID 時使用currentUserId(authentication),不得為取得相同 ID 多做一次 帳號查詢;只有 wire 或既有業務契約明確需要可變欄位時才走目錄解析。JpaAuditingConfig的AuditorAware繼續讀Authentication#getName(),因此createdBy/lastModifiedBy記錄的是有效主體的 stable Account ID;模擬時為被模擬者, 真實 actor 與 delegation grantors 由 acting audit event 及選配的 Entity revision metadata 記錄,不塞入每個 entity。- reference
ActingWebConfig必須注入 Feature core 提供的 interceptor,不得在缺 bean 時自行補建;core default 與 app URL policy 的責任因此可被 provisioning 如實驗證。
這是 4.0.0 正式版前的 breaking cleanup;舊 token 不做雙讀相容,部署後既有 acting session 應重新登入/重簽。
R3(2026-08-04):Entity changeset history 成為獨立 optional capability
背景:AuditableBase 只能回答目前資料列的建立者/最後修改者;request-level
AuditEvent 能記錄存取與 HTTP 結果,卻不能直接查某 Entity 的歷次狀態。把 actor/grantors
加進所有 Entity 會污染 schema,也無法保存每一版。
決策:
- 框架新增 engine-neutral
AuditChangeContext/provider/target 契約;impersonation、 delegation 與 tenant 都是可選 context 來源,不是 prerequisite。 - Hibernate Envers 僅是 optional adapter;具體 revision entity、table 與 tenant 欄位由應用 擁有。reference app 以獨立 Gradle feature seam 引入 Envers。
- 每個 Entity 仍須以
@Audited明確 opt in。revision 以 stable subject 保存subject/actor/grantors,grantors 使用正規化子表;AuditableBase完全不變。 - request-level acting audit 仍保留,因為讀取、拒絕或沒有 Entity 變更的請求不會產生 revision。
完整啟用與 schema 邊界見 Entity 歷程稽核。
相關文檔 (References)
內部文檔
- Spring Security 配置:
../guides/auth/security.md - Bearer Token 認證:
../guides/auth/bearer-token-authentication.md - Tenant 解析鏈:
../guides/auth/tenant.md - Authority 權限模型:
../guides/design/authority-model.md - 資料庫設計(稽核欄位):
../guides/design/database-design.md - Entity 歷程稽核:
../guides/design/entity-audit.md
相關 ADR
- ADR-009: 認證授權架構——雙模式資源伺服器(本 ADR 疊加其上):
./009-auth-dual-mode-resource-server.md - ADR-001: 多租戶數據隔離策略(tenant/ownership 隔離、tenant claim 解析):
./001-multi-tenant-data-isolation.md
外部資源
- RFC 8693(OAuth 2.0 Token Exchange,
act/may_actclaim 的既有先例,供 claim 命名參考) - Spring Security
SwitchUserFilter(switch-user 既有模式參考,本 ADR 採 token-based 而非 filter-based)
變更歷史 (Change Log)
| 日期 | 變更內容 | 變更者 |
|---|---|---|
| 2026-07-08 | 初版(提議中) | Development Team |
| 2026-07-08 | 代理參考實作改採 grant 模型(授權人指派代理人、代理人登入時自動生效、持久授權);框架 token/claim 機制不變(中立於啟用模型),更新「參考實作」段(新增代理啟用模型節、候選名單端點)與前端明示 bullet | Development Team + AI |
| 2026-07-08 | 移除 act_mode 判別器,改兩維正交模型(狀態 → Accepted):模擬性/代理性由 actor/grantors 的存在性獨立表達、可共存於同一 token;ActingContext 移除 getMode()/ActingMode、改 isImpersonating()/isDelegating() 兩述詞;ActingClaims 移除 act_mode 常數(兩 claim 產物無 key 衝突、可 merge)。參考實作:ActingResponse 改 impersonating/delegating 兩布林、前端橫幅與指示器改各綁一維可同時顯示。框架 + 參考實作 + 前端皆已實作並通過測試 | Development Team + AI |
| 2026-07-08 | 參考實作拆分 Delegation/Impersonation 為獨立 controller/service:候選名單拆 GET /impersonation/candidates + GET /delegation/candidates(去 ?mode=);GET /acting 抽到中性 ActingController;ImpersonationService 去除對 delegation 的一切依賴(enter 改純模擬、不 wire 共存情境一)——下游可各自 cherry-pick 單一功能。並移除代理人自助 exit(借用模型殘影;關閉代理改由授權人撤銷 grant)。前後端 + 文檔一致、測試通過 | Development Team + AI |
| 2026-07-08 | 移除 DelegationSession 表,代理稽核改 log-based:grant 模型下該表退化為 write-only、且與 [AUDIT] Login with delegation log 行重複(「Session」概念本身是借用模型殘影)→ 刪除 entity/repository、applyAtLogin 改只寫 audit log;ImpersonationSession 保留(有 enter/exit 生命週期)。並移除因「純模擬」而變 dead code 的 DelegationService.effectivePowers。測試改以登入回應驗證權限合併 | Development Team + AI |
| 2026-07-08 | 界線修訂 R1:新增「界線修訂」節,記錄方案 C 的「機制 vs 政策」二分精煉為三分——多出「領域無關基礎設施」一格;決定把通用持久稽核 sink(AuditEventRepository 實作 + audit_event)升入 appfuse-server 為 opt-in 服務(對齊 cache/mail),app-server 改消費框架版。ActingAuditInterceptor/模擬/代理政策維持應用層。由框架維護者在 repo 內發起,不走 /rfc(以 ADR 修訂 + CHANGELOG/版本紀律取代) | Development Team + AI |
| 2026-07-08 | 稽核做對到底:移除 ImpersonationSession 表,改「JWT-per-request + 存取級 hook + 持久 AuditEventRepository」。洞察:JWT 每次認證請求皆自帶代行脈絡,ActingContext.current() 隨時可還原 → 稽核落存取層即可,session 表冗餘。新增泛用 audit_event 表(AuditEventRecord + PersistentAuditEventRepository 實作 Actuator AuditEventRepository,/actuator/auditevents 可查);ActingAuditInterceptor(/api/**)對代行請求發 ACTING_ACCESS,enter/exit/applyAtLogin 發 IMPERSONATION_STARTED/ENDED、DELEGATION_APPLIED。JPA Auditing(AuditableBase,列級=sub)維持不變、與存取級兩層互補。清掉殘留 denorm:DelegationGrant.granteeName(改即時解析)/.tenantId、DelegationSession 全移除。全 app-server 測試通過(含 E2E 稽核事件斷言) | Development Team + AI |
| 2026-07-28 | 界線修訂 R2:acting claim/API 由 delegates 收斂為方向正確的 grantors,所有 framework identity 與轉場稽核鍵改以 stable subject 對齊 AuthPrincipal.subject();username 映射留在 application reference boundary。同步修正 ActingWebConfig 必須消費 Feature core bean、不再自行補建,以及 DELEGATION_APPLIED 不得在 read-only transaction 發布,確保持久 sink 真正落表 | Development Team + AI |
| 2026-08-04 | 補齊 application identity resolver 契約:參考實作以 structural AuthenticationIdentityResolver 區分 stable Account ID 與 username/顯示名稱;currentUserId 不查 DB,wire projection 才經 PrincipalDirectory。同步明定 JPA entity audit 記有效主體 stable ID,真實 actor/grantors 由 acting audit 軌承載 | Development Team + AI |
| 2026-08-04 | 界線修訂 R3:新增獨立 optional Entity changeset history。框架提供 engine-neutral context 與 optional Envers adapter;應用擁有 revision schema,Entity 逐一 @Audited opt in。revision metadata 記 stable subject/actor/grantors,不修改 AuditableBase,request-level acting audit 仍負責未變更資料的存取軌跡 | Development Team + AI |
| 2026-08-14 | 界線修訂 R4:Refresh Session 新增 policy binding/eligibility revalidation 與依 policy reference 主動撤銷;acting audit 補 session/policy。參考實作把 consent 與 MFA break-glass 分流、禁止 ordinary bypass 與 nested impersonation,採 pure impersonation;delegation grant 補期限、狀態、撤銷 metadata 與唯一配對 | Development Team + AI Assistant |
文檔維護者: Development Team + AI Assistant 最後審閱: 2026-08-14