跳至主要内容

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 出參考實作),框架不提供業務表格與端點。

關鍵設計(無模式判別器):代理性與模擬性是兩個正交維度——模擬性由 actor claim 是否存在表達、代理性由 grantors claim 是否非空表達,兩者彼此獨立、可同時成立於同一 token(如「A 模擬 B,而 B 本身被 C 指派代理」→ actorgrantors 並存)。框架不設 act_mode 這類單一模式判別器(它承載零額外資訊、且天生表達不了兩維共存);是否共存由應用層簽發政策決定。


背景 (Context)

問題陳述

多數建於 appfuse 上的應用都需要兩種「以他人身份/權限操作系統」的能力,且語意根本不同

  • 代理(Delegation):登入者透過取得授權人的權限操作系統。對系統而言,實際執行者仍是登入者——權限解析須合併授權人的權限,稽核執行者仍是登入者,代理的事實由「代理本身的記錄」分辨。授權人可以多個
  • 模擬(Impersonation):登入者透過取得模擬對象的身份操作系統。對系統而言,實際執行者是被模擬對象——沒有權限合併問題,稽核執行者是被模擬對象,模擬的事實由「模擬本身的記錄」分辨。一次只能是一個身份。

首個消費者為 ict(ownership 隔離),次個為 florist-flora(tenant 隔離)。這是跨應用共通、且必須同時適用兩種資料隔離模式的機制——屬框架先行範疇,不應在各 app 各自重造。

框架現況(見 ADR-009):

  • JWT 為簽發當下的快照、per-request 無狀態、不回查 DB;權限放在 auth claim,每次請求由 converter 還原成 authorities。
  • 稽核 auditor(AuditorAware)只讀 authentication.getName(),即 JWT 的 sub
  • tenant/ownership 的資料可見性都 key 在 principal 身份上:tenanttenantId claim 餵 TenantContext + Hibernate @Filterownership 由 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 政策差異大:何時可代理/模擬、誰可對誰、如何記錄,因應用而異——框架若硬塞資料表或端點會限制適應性。
  • 撤銷機制既有sessionId claim + token 黑名單可即時撤銷整組 token,代理/模擬 session 應可沿用。

假設前提

  • 代理與模擬的啟用是一個特權動作,其閘門(誰能做)由應用層以既有 @PreAuthorize authority 控制。
  • 「代理/模擬的事實」只要被應用層記錄下來且可查,即足以在稽核時推算,不需在每筆資料列額外標記真實操作者。
  • 權限高低是應用層主觀決定的,框架無客觀依據判斷「最高權限者」。

考量的方案 (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(採用方案)

說明: 框架交付三件事、且僅此三件:

  1. Token 簽發擴充JwtTokenProvider 可產生
    • 代理 tokensub 維持登入者、auth 併入指定 ROLE 展開的權限、帶 grantors claim(授權人 stable subject 集合);
    • 模擬 tokensub/auth/tenantId 換成被模擬者、帶 actor claim(真實操作者);
    • 兩者沿用 sessionId。兩維 claim 無 key 衝突、可 merge 進同一 token載入被代理/被模擬者、選哪些 ROLE 由應用層解析後傳入,框架只負責正確地簽。
  2. Claim schema 契約:制定兩個正交 claim 名(grantorsactor),是「簽發端 ↔ converter ↔ ActingContext」round-trip 的唯一約定;無模式判別 claim
  3. ActingContext interface + 讀取工具:讀當前 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)

核心理由:

  1. 薄框架、最大適應性:授權模型與持久化因應用而異,框架不塞表格與端點,才能同時服務 ict(ownership)與 florist-flora(tenant)及未來各種設計。
  2. 對齊既有邊界:框架只擁有 tenant 隔離、resource/instance 級留應用層——代理的資料 scope 合併因此是應用層工作,框架只需把授權人 subject 帶進 token。
  3. 模擬幾乎免費:既有 tenant/ownership 機制都 key 在 principal 身份,模擬換掉 sub/auth/tenantId 即自動跟隨,框架不改邏輯。
  4. 稽核自動正確sub 語意(代理=登入者、模擬=被模擬者)讓 AuditorAware 免改即正確;ActingContext 供應用層補記「事實」。

兩維正交(設計核心)

代理與模擬是兩個彼此獨立、可自由組合的維度,不是一個要三選一的模式。各維度自身的 claim 對 sub/auth/tenantId 的效果不對稱:

代理 Delegation(代理維)模擬 Impersonation(模擬維)
由哪個 claim 表達grantors 非空actor 存在
sub(稽核身份)維持登入者 A切成被模擬者 B
auth(權限)A ∪ 借用 ROLE 的權限= B 的權限
tenantId維持 A(不跨租戶換成 B(可跨租戶
額外 claimgrantors(多筆,0..n)actor(單筆,真實操作者 A)
對象數
資料可見性合併應用層grantors 折進 ownership 過濾隨 B 身份自動成立
  • 兩維可共存於同一 tokengrantorsactor 是獨立 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 讀此
authA ∪ 借用 ROLE 權限B 的權限既有;converter 還原 authorities
tenantIdA 的 tenantB 的 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 以 subjectactorSubject 保存身份維度;refresh 時重載目前 principal 與 delegation, 並重驗 SessionPolicyBinding 的期限、必要 actor authority 與應用 SessionEligibilityPolicy, 再重鑄 grantorsactoractingPolicy claim。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() 在模擬時回 actor claim,其餘情況回 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)

必須遵守的規則

  1. acting 核心只交付四件:token 簽發擴充、claim schema、ActingContext 讀取工具、ActingAuditInterceptor 事件解算。中立 Entity history context/adapter 是另一個獨立 optional capability;兩者都不得在框架加入授權表、啟用記錄表、enter/exit 端點、URL policy 或閘門 authority。
  2. 授權人/被模擬者的解析在應用層:框架簽發 API 接受「已解析的授權人 stable subject/借用 ROLE」或「已解析的被模擬者身份」為輸入;載入帳號、選 ROLE、決定是否允許為應用層職責。
  3. claim schema 為契約、兩維正交grantorsactor 為唯一約定、act_mode,簽發端與 ActingContext 讀取端一致;兩 claim 無 key 衝突、可 merge 進同一 access token;Refresh Session 保存 actor context 並於 refresh 重鑄。
  4. 代理維持 sub、模擬切 sub:確保 AuditorAware 自動正確;框架不為此新增 auditor 分支。
  5. 代理不跨租戶、模擬可跨租戶:代理 token 維持登入者 tenantId;模擬 token 換成被模擬者 tenantId
  6. 通用生命週期管控:框架禁止 nested impersonation、於 refresh 重驗 subject/actor 狀態與 policy binding,並提供 SessionEligibilityPolicy 與依 policy reference 主動撤銷 family 的接點; 角色、保護帳號、consent 與 break-glass step-up 仍由應用層政策決定。
  7. 撤銷沿用 sessionId:代理/模擬 token 帶 sessionId,可經既有黑名單即時撤銷。

建議的最佳實踐

  1. acting 存取稽核軌不可省:Entity history 只在 transaction 實際變更 audited Entity 時產生 revision;未修改資料的讀取、拒絕與 HTTP 結果仍須由 request-level hook(ActingAuditInterceptor)+ 持久 AuditEventRepository 記錄——不需 session 表(JWT 每請求自帶脈絡)。
  2. instance 級過濾讀 getGrantors():ownership 應用把授權人 subject 集合折進自己的擁有權過濾;tenant 應用同租戶多半免處理。
  3. 前端明示(兩級、各綁一個維度,可同時顯示)模擬覆蓋 identity(sub=被模擬者),由 isImpersonating() 觸發醒目橫幅「模擬中:B」+ 結束鍵,結束以 actor 換回登入者 token;代理身份未變(sub=行為者自身、只是多了權限),由 isDelegating() 觸發低調指示器(Header chip「代理中:{grantors}」,純顯示、無停止鍵——grant 模型下代理人不能自行卸下,關閉由授權人撤銷 grant)即可,不用全幅橫幅。兩述詞獨立判定 → 前端天生支援同時顯示橫幅+指示器(框架容許兩維共存);惟參考實作為維持兩功能獨立、模擬保持純粹,florist 不會由模擬觸發共存(見「參考實作」節)。判斷句:帶 actor → 橫幅;grantors 非空 → 指示器;兩者各自獨立判定。
  4. 短 TTL 降低快照過時與洩漏風險。

檢查清單

  • JwtTokenProvider 具代理/模擬簽發能力;兩維 claim 可 merge 進同一 access token,Refresh Session family 可保存並重鑄 actor context
  • ActingContextisImpersonating()/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-level AuditEvent;未修改資料的 acting 存取仍由 request audit 軌記錄

參考實作 (Reference Implementation)

框架只出機制;下列政策與持久化由 app-server / app-office 出參考實作,屬 @reference-surface(見 m-reference-code.md),下游 retarget:

兩功能獨立、可各自 cherry-pick:Impersonation 與 Delegation 各有獨立的 controller / serviceImpersonationController/ServiceDelegationController/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/grantsPOST/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},兩功能共用;subjectactorgrantors 是為 UI 投影的 username(查無時退回 stable subject)。JWT 與 ActingContext 本身始終只承載 stable subject。
  • 應用身分解析器service/auth/AuthenticationIdentityResolver,structural):集中區分 stable subject 與可變顯示資料。currentUserId(authentication) 直接回 Authentication#getName()(JWT sub/Account ID,不查 DB);findBySubjectusernameOrSubjectdisplayNameOrSubject 才經框架 PrincipalDirectory 查目錄。它依賴應用帳號模型、屬可 retarget 的 application structural code,不是 framework capability、Feature core 或 local seam;下游若有以 username 關聯的既有資料,可在自家 resolver 增加明確的相容方法,參考實作不預設這個 legacy 契約。
  • 候選名單依功能拆兩端點GET /impersonation/candidates(同租戶 / 跨租戶能力)與 GET /delegation/candidates(同租戶 / 已指派),各由自己的 service 過濾——不再用單一 ?mode= 混流。
  • 閘門 authorityimpersonation:enter(+跨租戶 impersonation:cross-tenant)守模擬;代理指派為個人動作,參考實作以 isAuthenticated() 守(名稱與粒度由參考實作定,非框架)。
  • 稽核(三層獨立互補、無 session 表)① 目前狀態 JPA Auditing(AuditableBaseAuditorAwaresub@LastModifiedBy,不動)。② Entity changeset optional Entity history(只對 @Audited Entity 保存版本;revision metadata 記 stable subject/actor/grantors)。③ 存取級 request-level acting hook(ActingAuditInterceptorActingContext:代行脈絡下的每個請求發 ACTING_ACCESS,轉場發 IMPERSONATION_STARTED/ENDEDDELEGATION_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 機制只負責把「合併後的權限 + grantors claim」簽進 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框架(已在)ActingContextActingClaimsJwtTokenProvider 代行簽發/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 Actuator AuditEventRepository)。具體 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 路徑仍由 reference service/acting/ActingWebConfig 決定。模擬/代理 service/端點/閘門 authority、DelegationGrant、前端留應用層。模擬(token 流程) 可作後續獨立評估的框架 starter(對齊 Spring Security SwitchUserFilter);代理 grant 因深度耦合授權模型,維持參考實作。

影響

  • 新增 io.leandev.appfuse.audit.* 公開 API → 觸發 appfuse-server CHANGELOG 條目、版本 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 的 delegatesactorgrantorssub 一律承載 stable subject。
  • 公開 API 改為 CLAIM_GRANTORSgetGrantors()getGrantor();解析時 trim、 去除空值與重複 subject。
  • application reference 以 structural AuthenticationIdentityResolver 包裝 PrincipalDirectoryActingController 只在 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 或既有業務契約明確需要可變欄位時才走目錄解析。
  • JpaAuditingConfigAuditorAware 繼續讀 Authentication#getName(),因此 createdBylastModifiedBy 記錄的是有效主體的 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_act claim 的既有先例,供 claim 命名參考)
  • Spring Security SwitchUserFilter(switch-user 既有模式參考,本 ADR 採 token-based 而非 filter-based)

變更歷史 (Change Log)

日期變更內容變更者
2026-07-08初版(提議中)Development Team
2026-07-08代理參考實作改採 grant 模型(授權人指派代理人、代理人登入時自動生效、持久授權);框架 token/claim 機制不變(中立於啟用模型),更新「參考實作」段(新增代理啟用模型節、候選名單端點)與前端明示 bulletDevelopment Team + AI
2026-07-08移除 act_mode 判別器,改兩維正交模型(狀態 → Accepted):模擬性/代理性由 actorgrantors 的存在性獨立表達、可共存於同一 tokenActingContext 移除 getMode()/ActingMode、改 isImpersonating()/isDelegating() 兩述詞;ActingClaims 移除 act_mode 常數(兩 claim 產物無 key 衝突、可 merge)。參考實作:ActingResponseimpersonating/delegating 兩布林、前端橫幅與指示器改各綁一維可同時顯示。框架 + 參考實作 + 前端皆已實作並通過測試Development Team + AI
2026-07-08參考實作拆分 Delegation/Impersonation 為獨立 controller/service:候選名單拆 GET /impersonation/candidates + GET /delegation/candidates(去 ?mode=);GET /acting 抽到中性 ActingControllerImpersonationService 去除對 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 政策」二分精煉為三分——多出「領域無關基礎設施」一格;決定把通用持久稽核 sinkAuditEventRepository 實作 + 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_ACCESSenter/exit/applyAtLoginIMPERSONATION_STARTED/ENDEDDELEGATION_APPLIED。JPA Auditing(AuditableBase,列級=sub)維持不變、與存取級兩層互補。清掉殘留 denorm:DelegationGrant.granteeName(改即時解析)/.tenantIdDelegationSession 全移除。全 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