跳至主要内容

代理與模擬(Acting)使用指南

Packageio.leandev.appfuse.security.auth.* 核心類別ActingContextActingClaimsJwtTokenProvider 決策背景ADR-013: 代理與模擬——ActingContext 框架能力

框架以最薄的機制面支援「以他人身份/權限操作系統」的兩種能力——代理(Delegation)模擬(Impersonation)。框架交付 token 簽發擴充、JWT claim wire 契約、ActingContext 讀取工具與 request-level 稽核事件能力;啟用時機、閘門、授權與啟用記錄、資料表、instance 級過濾一律留給應用層(由 app-tenant-server / app-office 出參考實作)。

本指南分兩半:前半是框架設計(你在任何 appfuse 專案都能用的機制),後半是參考實作走查(花店 app-office 如何在框架機制上落地一套完整政策)。


一、兩種能力的語意差異

代理與模擬常被混為一談,但語意根本不同——這組差異是整個設計的核心:

面向代理 Delegation模擬 Impersonation
一句話登入者借用他人權限操作登入者取得他人身份操作
實際執行者(對系統而言)仍是登入者 A是被模擬者 B
身份(JWT sub維持登入者 A切成被模擬者 B
權限(JWT authA ∪ 授權人的權限= B 的權限
稽核 @LastModifiedBy記 A記 B
對象數多個(0..n)單一
跨租戶不跨(同租戶)可跨(管理/客服超能力)
額外 claimgrantors(授權人 subject 集合)actor(真實操作者 subject)
前端提示低調指示器(身份未變)醒目橫幅(身份被換掉)

判斷句:identity 被換掉 → 模擬(橫幅);只是多借到權限、身份沒變 → 代理(指示器)。

兩者正交、可同時發生:代理與模擬不是二選一——它們是兩個獨立維度,可共存於同一 token(如「A 模擬 B,而 B 本身被 C 指派代理」)。框架不設 act_mode 這類模式判別器,模擬性/代理性由 actorgrantors claim 的存在性各自獨立判定(見 §3、§4 的正交模型)。

兩者不互相取代。模擬能跨租戶、換身份,看似「更強」,但它抹掉了「真正是誰在操作」——代理保留登入者身份、只補權限,稽核直指真人。用途不同,並存。


二、設計哲學:框架只交付四件

appfuse-server 對「代理/模擬」刻意保持薄機制面,理由見 ADR-013:授權模型(誰可對誰、時限、scope)與持久化因應用而異,框架若硬塞資料表或端點會限制適應性,也與「框架只擁有 tenant 隔離、resource 級留應用層」的既有邊界矛盾。

因此框架提供:

#交付物類別職責
1Token 簽發擴充JwtTokenProvider正確地簽出代理/模擬 token(claim 對稱)
2Claim wire 契約ActingClaims簽發端 ↔ 讀取端唯一約定的 claim 名與組裝
3讀取工具ActingContext從當前 JWT 算出 {subject, grantors[], actor}
4稽核事件能力ActingAuditInterceptor代行請求完成後發布 ACTING_ACCESS

框架不提供:授權表、啟用記錄表、enter/exit 端點、閘門 authority、URL 掛載範圍、持久稽核 sink。這些一律由應用層設計。

2.1 Feature 導入、預設與邊界

acting 是 optional Feature,本身不宣告 prerequisite;delegationimpersonation 需要它時,由 feature dependency 自動帶入。其組成刻意分為:

  • Feature coreActingConfig 提供可覆寫的 ActingAuditInterceptor 預設 bean; 使用者宣告同型 bean 時,@ConditionalOnMissingBean 讓位。
  • Reference domainActingController 把 stable subject 經 PrincipalDirectory 即時映射成 username,並以 /api/v1/auth/acting 提供 UI 顯示契約; ActingWebConfig 選擇把 core interceptor 掛在 /api/**
  • Catalog seamacting 沒有 catalog seam,也沒有 property、schema 或 migration。 可覆寫 bean 是 Spring extension point;URL、response 與掛載範圍則是 app-owned reference policy,不是 framework seam。

若產品要客製,可替換 ActingAuditInterceptor bean,或直接 retarget reference controller/config;不要在 reference config 重新建一份 framework bean。若要移除,須先移除 依賴它的 delegationimpersonation,再移除 acting reference domain 與 core。

tenant 與 tenantless variant 的 acting core、reference source 與 catalog 宣告皆為 零差異;租戶差異由 token 的 tenantId 與上層 delegation/impersonation 政策承擔。 另外,interceptor 只會發布事件;需要合規等級的可追溯性時,應另 provision audit 及持久 AuditEventRepository,否則事件不保證跨程序生命週期留存。


三、框架 API

3.1 Token 簽發擴充(JwtTokenProvider

框架在既有的 generateToken(...) 之上加兩個代行專用簽發方法。載入被代理/被模擬者、選哪些 ROLE、決定是否允許皆由應用層負責——框架只負責正確地簽。

/// 產生代理 token:sub 維持登入者、auth 取 subject 的權限
/// (呼叫端應先把「借用 ROLE 展開的權限」併入 subject.getAuthorities())、帶 grantors claim。
public String generateDelegationToken(UserDetails subject, List<String> grantors,
Map<String, Object> extraClaims);

/// 產生模擬 token:sub/auth 換成被模擬者、帶 actor claim。
/// 被模擬者的 tenantId 由呼叫端於 extraClaims 帶入(模擬可跨租戶)。
/// 要同時具代理維(被模擬者本身被指派代理),於 extraClaims 併入 ActingClaims.delegation(...) 即可。
public String generateImpersonationToken(UserDetails impersonatedSubject, String actor,
Map<String, Object> extraClaims);
  • 代理subject 傳登入者,但其 getAuthorities() 應已是「自身 ∪ 借用權限」的合併結果(合併在應用層做)。grantors 至少一筆。
  • 模擬impersonatedSubject 傳被模擬者,其 stable subject/authorities 成為 token 的 sub/authactor 是真實操作者的 stable subject(供結束模擬與稽核)。
  • 兩維可組合grantorsactor 是獨立 claim、無 key 衝突——要簽「模擬 B 且帶 B 的代理維」,把 ActingClaims.delegation([C]) merge 進模擬簽發的 extraClaims 即可。
  • 兩者的 sessionIdextraClaims 帶入,沿用既有黑名單撤銷機制。

3.2 Claim wire 契約(ActingClaims

代理授權人、模擬者與有效身份一律以不可變 stable subject 識別,對齊 JWT subAuthPrincipal.subject() 是簽發來源;username、email、顯示名等可變欄位不得進入 framework claim contract。需要顯示名稱時,由應用層透過 PrincipalDirectory 映射。

兩個正交 claim,無模式判別器:模擬性由 actor 存在表達、代理性由 grantors 非空表達,兩者獨立、可並存。

claim常數代理模擬說明
sub登入者 A被模擬者 B既有;auditor 讀此
authA ∪ 借用權限B 的權限既有;converter 還原 authorities
tenantIdA 的 tenantB 的 tenant既有;TenantContext 解析
sessionIdCLAIM_SESSION_ID既有;黑名單撤銷
actingPolicyCLAIM_ACTING_POLICY選用Refresh Session 綁定的應用政策識別
grantorsCLAIM_GRANTORS[stable subject, …]—(除非共存)代理維,非空即代表代理性(0..n)
actorCLAIM_ACTOR—(除非共存)stable subject模擬維,存在即代表模擬性

為何沒有 act_mode:模式判別交由 grantors/actor存在性承擔——act_mode 承載零額外資訊,且單一值天生表達不了「同時模擬又代理」。安全性不靠它把關:token 完整性由 JWT 簽章保證,無法在不重簽(必經 JwtTokenProvider)下注入假 claim。

ActingClaims 提供組裝工具,供簽發端組出代行 claim(generateDelegationToken / generateImpersonationToken 內部即呼叫):

ActingClaims.delegation(List.of("account-a", "account-b")); // {grantors=[account-a, account-b]}
ActingClaims.impersonation("account-c"); // {actor=account-c}
// 無 act_mode key → 兩者可 merge:{actor=account-c, grantors=[account-a]}

Refresh Credential 是不透明 secret,不承載 claim。Refresh Session family 保存 subjectactorSubjectSessionPolicyBinding;每次 refresh 重載 subject、actor 與 delegation,重驗 policy 絕對效期、actor 必要 authority 及應用提供的 SessionEligibilityPolicy,再重鑄 grantorsactoractingPolicy。政策拒絕會撤銷整個 family。

3.3 讀取工具(ActingContext

ActingContext 是讀取端唯一入口——把當前 JWT 的代行 claim 在框架邏輯下算成結構化脈絡,供應用層寫啟用/稽核記錄、做 instance 級過濾。

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 的 session id
String getActingPolicy(); // 目前 family 綁定的應用政策識別
boolean isImpersonating(); // 帶 actor claim
boolean isDelegating(); // 帶非空 grantors;可與 isImpersonating() 同時為 true
}

取得脈絡(三個靜態工廠):

// 最常用:從當前 SecurityContext 讀
ActingContext acting = ActingContext.current();

// 從 resource-server 的 Jwt principal
ActingContext acting = ActingContext.of(jwt);

// 從原始 claim map(測試或非 resource-server 情境)
ActingContext acting = ActingContext.ofClaims(claims);

語意細節(實作於 DefaultActingContext):

  • isImpersonating() / isDelegating()兩個獨立述詞(各由 actor / grantors 的存在性判定),可同時為 true(共存情境);無單一 getMode()
  • getGrantor() 是給「只支援單一授權人」的應用的便利存取:恰好一筆回該筆、零筆null多筆卻以單數存取則丟 IllegalStateException(不允許把多筆當一筆讀)。
  • getActor() 在模擬時回 actor claim;其餘情況退化回 subject 自身——因為「真實操作者恆為當前 principal」,代理/一般 session 下操作者就是登入者本人。

目前狀態欄為何自動正確AuditorAware 只讀 authentication.getName()(即 sub)。代理維持 sub=登入者、模擬切 sub=被模擬者,於是 @LastModifiedBy 免改即正確。ActingContext旁路資訊、不改既有 auditor 預設;需要 actor/grantors 與 Entity 版本時,另選配 Entity history。


四、稽核與資料隔離的互動

稽核:三層獨立互補

JPA 目前狀態欄(@LastModifiedBy)只記有效身份(sub)。要看見 acting 維度,可依查詢目的獨立選配 Entity history 與 request-level audit event:

① 資料列級 — Spring Data JPA Auditing(AuditableBase

  • @CreatedBy/@LastModifiedBy/@CreatedDate/@LastModifiedDate + AuditorAware(讀 SecurityContextHolder…getName() = sub)。
  • 每次 persist/update 把「誰(=sub)、何時」蓋到資料列。單一 @LastModifiedBy 不承載 actor/grantors;既有 Entity 與 AuditableBase 不需修改。

② Entity changeset 級 — optional Entity history

  • 應用安裝 entity-audit feature,並只對需要歷程的 Entity 加 @Audited
  • Envers _AUD 表保存 Entity 版本;application-owned audit_revision 保存同一 transaction 的 subject/actor/grantors/principalKind/requestId/sessionId/source,tenant variant 另存 tenant。
  • impersonation/delegation 都不是必要相依;沒有對應 claim 時自然退化為 subject = actorgrantors = []。詳見 Entity 歷程稽核

③ 存取級 — request-level acting 稽核 hook + 持久 AuditEventRepository

  • 關鍵洞察:JWT 於每次認證請求都自帶完整代行脈絡(sub/actor/grantors),Spring Security 每請求處理它 → ActingContext.current() 隨時可還原。故 acting 稽核的正確落點是存取層,不需 session 表。
  • jar 的 ActingAuditInterceptor capability 在代行脈絡下完成的每個請求,發一筆 ACTING_ACCESS AuditEvent(principal=真實操作者,data 帶 subject/actor/grantors/actingModes/sessionId/actingPolicy/method/uri/status);acting Feature 提供可覆寫的預設 bean, app-tenant-server reference domain 才選擇掛在 /api/**。轉場另發 IMPERSONATION_STARTED/ENDEDDELEGATION_APPLIED。這些事件的 principal 與 data 身份欄一律使用 stable subject;DELEGATION_APPLIED 的發布方法必須位於可寫交易, 才能與持久 sink 在同一交易內真正落表。
  • 參考實作把這些 AuditEventPersistentAuditEventRepository(實作 Spring Boot Actuator 的 AuditEventRepository)持久落 audit_event 表,/actuator/auditevents 可查詢;Spring Security 的登入/授權稽核事件也共用此 sink。
    • 持久 sink 是框架 capability(io.leandev.appfuse.audit,opt-in,ADR-013 R1)audit Feature 本身以 in-memory sink 作零資料表預設。需要持久稽核的應用才由 reference domain 宣告 entity、repository 與 AuditPersistenceConfig,後者透過 repository provider 讓 Feature 改選持久 sink,過程只產生一個 AuditEventRepository bean。acting 的事件解算同樣是 capability;哪些 URL 納入稽核仍是應用 reference policy
  • 不再有 acting 專屬 session 表——ImpersonationSession/DelegationSession 已移除(JWT 每請求自帶脈絡,session 表對「軌跡」是冗餘)。

如何選:只需要目前狀態就用 AuditableBase;要查某 Entity 每一版與 changeset actor 就加 Entity history;要查未改 Entity 的存取、登入與 HTTP 結果則用 AuditEvent。三者可單獨或一起啟用,不必把 acting 欄位塞進所有業務 Entity。

資料隔離:跨租戶不對稱

隔離模式代理模擬
tenant同租戶;資料可見性本即整租戶,代理是「輕版」純權限合併tenantId claim → TenantContext + Hibernate @Filter 自動跟隨
ownership應用層讀 getGrantors() 把授權人 subject 集合折入擁有權過濾隨 B 身份自動成立
  • 模擬幾乎免費:既有 tenant/ownership 機制都 key 在 principal 身份,模擬換掉 sub/auth/tenantId 即自動跟隨,框架不改邏輯。
  • 代理不跨租戶:技術上 TenantContext/Hibernate filter 為單租戶,且實務罕見;代理 token 維持登入者 tenantId

五、參考實作走查(花店 app-office

以下為 app-tenant-server / app-office 的參考實作,屬 @reference-surface(見 m-reference-code.md),下游 retarget 時替換為自己的政策。框架碼不動,只換應用層政策。

5.1 端點總覽

方法路徑閘門職責歸屬
POST/api/v1/auth/impersonationhasAuthority('impersonation:enter')進入一般模擬Impersonation
POST/api/v1/auth/impersonation/break-glasshasAuthority('impersonation:break-glass')MFA step-up 緊急模擬Impersonation
POST/api/v1/auth/impersonation/break-glass/cases/{caseNumber}/closehasAuthority('impersonation:break-glass')結案並撤銷該案件全部 familyImpersonation
POST/api/v1/auth/impersonation/exitisAuthenticated()結束模擬Impersonation
GET/api/v1/auth/impersonation/candidateshasAuthority('impersonation:enter')可模擬對象名單Impersonation
GET/api/v1/auth/delegation/grants?activeOnly=true&page=0&size=50isAuthenticated()有界的代理人清單;每頁最多 100Delegation
POST/api/v1/auth/delegation/grantsisAuthenticated()指派代理人(同租戶)Delegation
DELETE/api/v1/auth/delegation/grants/{id}isAuthenticated()撤銷代理授權Delegation
GET/api/v1/auth/delegation/candidatesisAuthenticated()可指派代理人候選Delegation
GET/api/v1/auth/actingisAuthenticated()當前代行脈絡(渲染橫幅/指示器)Acting(中性共用)

兩功能獨立、可各自 cherry-pick:Impersonation 與 Delegation 各有獨立的 controller / service,彼此零依賴ImpersonationService 不碰任何 delegation 元件、反之亦然)——下游只想要其一時,整組拿走即可編譯運行。GET /acting(當前代行脈絡)是兩者共用的中性讀取面,由獨立的 ActingController 直接讀框架 ActingContext,對兩功能皆中性(只用模擬 → delegating 恆 false;只用代理 → impersonating 恆 false)。候選名單也依功能拆成兩個端點,不再用單一 ?mode= 混流。

閘門是政策:一般模擬使用 impersonation:enter,跨租戶另需 impersonation:cross-tenant;緊急路徑只接受 impersonation:break-glass,且不得由一般端點 自動繞過 consent。指派自己的代理人是個人行為,只驗 isAuthenticated()

5.2 代理:授權(grant)模型

代理只有一種啟用方式——授權(grant)。 「代理」即「權限持有者把權限授給他人代為行使」,權限由授權人往下流出;不存在「受益人自行挑選要借誰的權限」(那是自我提權、不是代理)。所以代理一律是授權人指派代理人 → 代理人登入時自動生效,直到絕對期限或撤銷。grant 保存 ACTIVE/REVOKED 狀態、到期時間與撤銷資訊;同一 grantor/grantee 配對具有唯一限制,續授權會重新啟用既有資料列。適用休假代理、職務代理人等現實情境。

流程:授權人 A 在「我的代理人」頁指派代理人 B(DelegationGrant 持久化)→ B 登入時 /api/v1/auth/login 查其收到的授權、合併 A 的 ROLE 權限、以 generateDelegationToken(B, [A], …) 簽 token。B 身份不變(sub=B)、多了 A 的權限,grantors=[A]。框架只負責簽 token;授權關係的持久化與撤銷政策是應用層職責。

邊界分成三層:jar capability 提供 token/LoginActingContributordelegation Feature 提供 DelegationApplication 窄契約;reference domain 才擁有 DelegationGrant entity/repository、REST URL 與帳號目錄政策。登入整合由 reference DelegationService 同時實作 DelegationApplication 與其父介面 LoginActingContributor,再由框架 LoginService(ADR-023)於登入時呼叫:

// 起點:代理人自身權限。逐一併入每位授權人的 ROLE 與其下 authority
for (DelegationGrant grant : grants) {
PrincipalView grantor = principalDirectory
.findById(grant.getGrantorSubject()).orElse(null);
if (grantor == null) continue;
grantor.roleNames().forEach(role ->
mergedAuthorities.add(new SimpleGrantedAuthority(toRoleAuthority(role))));
grantor.authorityNames().forEach(authority ->
mergedAuthorities.add(new SimpleGrantedAuthority(authority)));
grantorSubjects.add(grant.getGrantorSubject());
}
AuthPrincipal mergedActor = DefaultAuthPrincipal.builder(grantee.username())
.subject(grantee.id())
.authorities(mergedAuthorities)
.build(); // sub 仍是 B 的 stable subject
// …發 DELEGATION_APPLIED AuditEvent(持久落 audit_event 表,見 §四稽核)…

app-tenantless-server 不建立 tenant context;reference grant 本身也沒有 tenant 欄位。 真正的 instance ownership 可見性由各產品把 ActingContext.getGrantors() 折入 repository/service 過濾。app-tenant-server 則在自己的模組內維護 tenant-native 政策。

LoginService 拿到合併結果(ActingLogin)後:

var acting = actingContributor.flatMap(c -> c.contribute(account.subject()));
if (acting.isPresent()) {
var login = acting.get();
accessToken = jwtTokenProvider.generateDelegationToken(
login.effectivePrincipal(), login.grantors(), claims);
}
// Refresh Session 只保存 stable subject;refresh 時重新呼叫 contributor,反映最新 grant。

清單邊界GET /delegation/grants 預設 activeOnly=true,只回 ACTIVE 且未到期資料; page 從 0 起、size 預設 50 且最大 100,總數與頁資訊在 X-Total-CountX-PageX-Page-Size。管理歷史時才明示 activeOnly=false,避免「我的代理人」頁隨軟刪歷史無界膨脹。

關閉代理:grant 模型下代理人不能自行卸下被授予的權限(那是授權人的決定)——沒有 delegate-initiated exit 端點。要停止代理,授權人 DELETE /delegation/grants/{id} 撤銷授權(下次登入即不再套用),或代理人登出。這與模擬的 enter/exit 對稱不同:代理人從未「enter」(登入自動套用),故無對稱的「exit」。

既有 delegation_grant 升級

有既有資料的環境不得直接讓升級後 binary 以 ddl-auto=update 首次啟動:新增的 expires_atstatus 是 NOT NULL,且 pair unique constraint 可能被歷史重複資料阻擋。 請先備份並在停機窗口以既有 Flyway/Liquibase/DBA 流程完成以下順序:

  1. 先以 nullable 方式加入 expires_atstatusrevoked_atrevoked_by_subjectrevoke_reason
  2. 查出 grantor_subject × grantee_subject 重複;依業務決定保留一筆並移除其餘,不得讓腳本任意猜 winner
  3. 將舊 grant 回填為 ACTIVE,並設定明確期限。Reference 建議以 migration 時刻加 7 天,讓舊的無限期授權有受控換約窗口。
  4. expires_atstatus 改為 NOT NULL,再建立 uk_delegation_grant_pair
  5. ddl-auto=validate 啟動一次確認 mapping,之後才恢復該環境既定策略。

重複資料預檢(各支援資料庫通用 SQL):

SELECT grantor_subject, grantee_subject, COUNT(*) AS duplicate_count
FROM delegation_grant
GROUP BY grantor_subject, grantee_subject
HAVING COUNT(*) > 1;

MySQL 8 參考回填:

ALTER TABLE delegation_grant
ADD COLUMN expires_at DATETIME(6) NULL,
ADD COLUMN status VARCHAR(20) NULL,
ADD COLUMN revoked_at DATETIME(6) NULL,
ADD COLUMN revoked_by_subject VARCHAR(36) NULL,
ADD COLUMN revoke_reason VARCHAR(100) NULL;

UPDATE delegation_grant
SET expires_at = DATE_ADD(UTC_TIMESTAMP(6), INTERVAL 7 DAY),
status = 'ACTIVE'
WHERE expires_at IS NULL OR status IS NULL;

-- 先人工處理上述 duplicate query 的結果,再執行 constraint。
ALTER TABLE delegation_grant
MODIFY expires_at DATETIME(6) NOT NULL,
MODIFY status VARCHAR(20) NOT NULL,
ADD CONSTRAINT uk_delegation_grant_pair UNIQUE (grantor_subject, grantee_subject);

PostgreSQL 參考回填:

ALTER TABLE delegation_grant
ADD COLUMN expires_at TIMESTAMP(6) WITH TIME ZONE,
ADD COLUMN status VARCHAR(20),
ADD COLUMN revoked_at TIMESTAMP(6) WITH TIME ZONE,
ADD COLUMN revoked_by_subject VARCHAR(36),
ADD COLUMN revoke_reason VARCHAR(100);

UPDATE delegation_grant
SET expires_at = CURRENT_TIMESTAMP + INTERVAL '7 days',
status = 'ACTIVE'
WHERE expires_at IS NULL OR status IS NULL;

-- 先人工處理上述 duplicate query 的結果,再執行 constraint。
ALTER TABLE delegation_grant
ALTER COLUMN expires_at SET NOT NULL,
ALTER COLUMN status SET NOT NULL,
ADD CONSTRAINT uk_delegation_grant_pair UNIQUE (grantor_subject, grantee_subject);

SQL Server/Oracle 使用相同五步驟,但型別與 interval 語法須由該專案 migration 工具依 dialect 產生;reference 不提供一支假裝跨方言、實際可能毀損資料的通用 DDL。

5.3 模擬:enter / exit + 跨租戶閘門

POST /impersonation 進入模擬(ImpersonationService.enter):

  1. 閘門 impersonation:enter 由 controller @PreAuthorize 把關。
  2. Service 補 runtime 政策:目標與自己不同租戶時須另具 impersonation:cross-tenant (一般 USER 因此被限縮在同租戶);拒絕模擬服務帳號、拒絕模擬自己。
  3. 呼叫 LoginService.switchToImpersonation(...) 建立新 Refresh Session family;其 subject 為被模擬者、 actorSubject 為真實操作者,並綁定 policy reference/絕對期限/必要 actor authority;access token 的 tenantId/authorities 來自被模擬者(純模擬,不繼承目標 delegation)。
  4. 原 actor family 以 identity-switch 撤銷;refresh 由 family 的 actorSubject 重鑄 actor claim,避免刷新後降級成被模擬者的直接登入。
  5. IMPERSONATION_STARTED AuditApplicationEvent(principal=actor,data 帶 subject/crossTenant/sessionId)→ 持久落 audit_event 表。Refresh Session 表只保存 authentication state,不取代 audit 軌。
  6. 每次 refresh 重驗 actor/subject 狀態、actor authority、綁定 policy 與 consent;consent 撤銷時應用亦以 policy reference 主動撤銷所有既有 family。

為何純模擬:框架層雖容許「模擬 B 且帶 B 的代理維」兩維共存(把 ActingClaims.delegation(...) merge 進 extraClaims 即可),但參考實作刻意讓兩功能獨立——ImpersonationService 不依賴 DelegationService,下游可只取模擬。需要該組合的下游自行在 enter wire 即可。

POST /impersonation/exitImpersonationService.exit):ActingContext.current().isImpersonating() 確認確在模擬中,取 getActor() 拿回真實操作者,建立 actor 的新 family、撤銷模擬 family 並短效 blacklist 舊 access family,發 IMPERSONATION_ENDED AuditEvent,再以正常 access token 換回身份。

一般 consent 與緊急 break-glass 是兩條不同產品路徑:一般端點只接受結構政策或有效 consent; break-glass 端點要求 IdP amrauth_time 證明近期 MFA step-up、理由、案件編號,並簽發短效、 non-remembered、具絕對有效期的 family。首次進入會登錄 OPEN case;結案端點持久標記 CLOSED、 立即依 case number 撤銷所有 family,之後 refresh 與重新進入都 fail closed。

Reference seed 不把 impersonation:break-glass 配給預設 ADMIN/SUPER_ADMIN。普通 local login 也沒有 amrauth_time,所以即使手動配權仍會 fail closed;只有接上企業 IdP MFA step-up 的 專用 operations 角色才應取得該 authority。

app-office/MSW 刻意不提供 break-glass UI。它只示範普通 impersonation 警示;正式產品 必須先完成 IdP step-up journey,再以 reason/caseNumber 呼叫 server API,並在緊急 session 全程 顯示更強警示。沒有 IdP 的 mock 表單不得被當成 MFA UAT 證據。

5.4 候選名單(政策在後端)

候選名單依功能拆成兩個獨立端點(不再用 ?mode= 混流),各由自己的 service 依政策過濾,前端只呈現、不自行過濾

  • GET /impersonation/candidatesImpersonationService):先要求 impersonation:enter,再依 consent、保護角色/帳號與 anti-escalation 政策逐筆過濾。
  • GET /delegation/candidatesDelegationService):同租戶、且尚未指派過(避免重複授權)。
  • 兩者皆排除自己與服務帳號。

5.5 前端兩級提示(app-office

兩級各綁一個維度、由 GET /acting 回的兩正交述詞獨立觸發、彼此獨立顯示。框架容許兩維共存,故前端天生支援同時顯示(橫幅+指示器);但花店參考實作的模擬保持純粹(不併入被模擬者的代理權)、兩功能又各自獨立,故 florist 不會由模擬觸發共存——同時顯示是框架+前端就緒的能力,留給需要該組合的下游:

觸發述詞UI元件
模擬impersonating(帶 actor)醒目橫幅「模擬中:B」+ 結束鍵acting-banner.tsx
代理delegating(grantors 非空)低調指示器(Header chip「代理中:{授權人}」,純顯示無停止鍵;關閉由授權人撤銷 grant)delegation-indicator.tsx

ActingResponse{ impersonating, delegating, subject, actor, grantors } 兩布林 + username 顯示欄,無單一 mode;controller 在 reference boundary 將 framework stable subject 即時映射為 username。兩元件各自依述詞顯示,天然支援共存。進入模擬經「切換身份」 對話框(switch-identity-dialog.tsx,候選 Select);代理由授權人在「我的代理人」設定頁 (my-delegates-page.tsx)指派、代理人登入即生效。


六、導入你自己的專案

在框架四件套之上,你需要作者化下列應用層政策(可直接參考 app-tenant-server):

  • 端點:模擬 enter/exit、代理 grants CRUD、acting 脈絡查詢、候選名單。
  • 閘門 authority:模擬用特權 authority(+跨租戶額外 authority);代理指派依你的政策(參考實作用 isAuthenticated())。
  • 持久化:代理授權關係 DelegationGrant(grantor→grantee,這是活的授權、非稽核)。
  • 稽核:依需求選擇 Entity history(版本+changeset actor)及/或 request-level acting hook(HandlerInterceptorActingContext)發 AuditEvent + 持久 AuditEventRepositoryaudit_event 表);不建 acting 專屬 session 表(JWT 每請求自帶脈絡,見 §四)。
  • 權限合併(代理):登入時把授權人的 ROLE 權限併入,sub 維持代理人。
  • instance 級過濾ownership 應用):讀 getGrantors() 折入擁有權過濾。
  • 前端兩級 UI:模擬橫幅 vs 代理指示器,靠 GET /acting 驅動。

框架不做的事,別在應用層繞過框架自己簽 token——一律經 JwtTokenProvider 的兩個代行方法,確保 claim 對稱、ActingContext 讀得到。


七、相關文檔

外部參考

  • RFC 8693(OAuth 2.0 Token Exchange,act/may_act claim 的既有先例,供 claim 命名參考)
  • Spring Security SwitchUserFilter(switch-user 既有模式;本設計採 token-based 而非 filter-based)