“切到客户租户看看”是高风险运维动作。它至少需要回答:谁获准切入、目标是谁、能做什么、何时失效、能否主动撤销,以及数据与审计分别归谁。当前源码同时存在轻量 Header 覆盖和正式 Grant/Token 模型,两者不能混称。
1. 三条路径对比
| 路径 | 入口 | 授权证据 | CurrentTenant | 状态 Guard | 会话校验 |
|---|---|---|---|---|---|
| Host operate-as | X-Operate-As-Tenant | 仅已认证 CallerType.Host | 普通目标快照 | 跳过 | 不触发 |
| Development debug | X-Tenant-Debug | 仅环境为 Development | 普通目标快照 | 跳过 | 不触发 |
| 正式模拟模型 | TenantImpersonationGrant + Token Descriptor | 操作人、目标、有效期、Scope | 应含完整四元组 | 应检查目标 | Middleware 检查到期和撤销 |
前两条是解析覆盖;第三条才是可审计会话协议。
2. 正式模型
TenantImpersonationGrant 以操作人租户作为持久化 TenantId,唯一键为操作人租户 + 操作人 + 目标租户。它保存:
ValidFrom/ValidTo;Scope,例如 Support、Diagnostics、Read、Full;MaxSessions;RequiresStepUp;- 软删除与租户隔离。
TenantImpersonationTokenService.IssueAsync 当前强制:
- 禁止 CurrentUser 用户级嵌套模拟;
- 目标非空且不能等于操作人租户;
- 当前操作人 + 目标存在有效 Grant;
- 当前时间处于 Grant 窗口;
- Token 到期取
min(2h, Grant.ValidTo - now)。
3. Token Descriptor 如何进入 CurrentTenant
正确快照必须同时保存数据面与审计面:
public static class TenantImpersonationErrors{ public static readonly Error OperatorInvalid = Error.Validation("TenantImpersonation.OperatorInvalid", "Operator id is invalid.");
public static readonly Error ClaimsInvalid = Error.Validation("TenantImpersonation.ClaimsInvalid", "Tenant impersonation claims are incomplete.");}
public static Result<CurrentTenant> ToCurrentTenant( TenantImpersonationTokenDescriptor descriptor){ if (!long.TryParse(descriptor.OperatorUserId, out var operatorUserId)) { return Result.Failure<CurrentTenant>(TenantImpersonationErrors.OperatorInvalid); }
var tenant = new CurrentTenant( TenantId: descriptor.TargetTenantId, ImpersonatorUserId: operatorUserId, OriginalTenantId: descriptor.OperatorTenantId, TenantImpersonationGrantId: descriptor.GrantId, TenantImpersonationExpiresAt: descriptor.ExpiresAt);
// Effective Tenant 来自 Target,Actor Tenant 则由 OriginalTenantId 保留。 // 构造函数不抛错,适配边界必须主动执行四元组校验。 return tenant.ValidateInvariants() ? Result.Success(tenant) : Result.Failure<CurrentTenant>(TenantImpersonationErrors.ClaimsInvalid);}这段适配器当前不在 Host 源码中。没有它,TenantImpersonationTokenMiddleware 看到的 CurrentTenant.IsImpersonating 始终可能为 false,过期与撤销检查不会执行。
4. Middleware 的失败语义
当 CurrentTenant 被标记为模拟:
| 条件 | HTTP | 错误码 |
|---|---|---|
| 独立到期为空或已到 | 401 | TenantImpersonation.Expired |
| Token Service 未注册 | 401 | TenantImpersonation.Unavailable |
| Grant Store Failure、Grant 删除或过期 | 401 | TenantImpersonation.Revoked |
| 仍有效 | 继续 | 无 |
所有错误使用统一 Problem Details,不用反射式匿名 JSON。
5. 当前服务没有强制的 Grant 字段
Grant 模型已有 Scope、MaxSessions 和 RequiresStepUp,但当前 IssueAsync:
- 只把 Scope 放进 Descriptor,没有把它映射到 Authorization 权限;
- 不检查 RequiresStepUp;
- 不统计或限制 MaxSessions;
- 不检查目标租户是否存在、Active 或 GracePeriod;
- 不检查操作人账号是否仍启用。
IsStillAuthorizedAsync(grantId, operatorTenantId) 只检查两个参数非空,然后按 GrantId 加载并检查时间。operatorTenantId 没有与 Grant 的 OperatorTenantId 比较,也没有校验 OperatorUserId、TargetTenantId 或 Scope。
6. Header 覆盖的风险边界
X-Operate-As-Tenant 只检查 CallerType.Host,随后:
- 构造普通 CurrentTenant,不含原租户、操作人、Grant 和到期;
- 跳过 TenantStatusGuard;
- 不触发 TenantImpersonationTokenMiddleware;
- ORM 仍因 Host 身份绕过 Tenant Filter;
- 没有 Scope、StepUp、并发会话或主动撤销。
因此它更接近“Host 控制台显示上下文”,不能作为客户数据面隔离保证。若保留该 Header,应限制在显式只读运维端口,并记录目标租户与 Actor;普通 owner Store 应采用正式模拟会话或显式 target predicate。
7. 正式端到端流程应是什么
当前已交付的是 Grant 模型、Token Service 部分规则、CurrentTenant 模型和校验 Middleware;StepUp、Session 计数、HTTP 签发与 Claim 还原、目标 Guard、Scope 授权和 Host 目标过滤仍需闭环。
8. 安全审计字段
正式模拟至少记录:
- Effective TenantId(客户数据面);
- ActorTenantId / OriginalTenantId(运维归属);
- ImpersonatorUserId;
- GrantId 与 Scope;
- 签发、使用、退出、到期、撤销时间;
- CorrelationId、TraceId、资源和动作;
- StepUp 证据与客户端信息。
日志不应记录 Raw Token 或完整 Grant 内容。
9. 测试矩阵
仓库现有 Token Service 测试覆盖嵌套、同租户、空目标、无 Grant、过期和两小时上限,以及 Grant 删除/过期。还必须增加:
[Fact]public async Task Authorization_Check_Should_Reject_Wrong_Operator_Tenant(){ // Grant 属于 operations-a,攻击者不能只知道 GrantId 就从 operations-b 复用。 repository.Return(Grant( id: "grant-1", operatorTenant: "operations-a", operatorUser: "1001", targetTenant: "customer-a"));
var result = await service.IsStillAuthorizedAsync( grantId: "grant-1", operatorTenantId: "operations-b", CancellationToken.None);
// 服务应返回正常的“未授权”,而不是因所有权不匹配抛出基础设施异常。 result.IsSuccess.Should().BeTrue(); result.Value.Should().BeFalse();}当前实现不会满足最后一个断言。另需覆盖目标租户状态、Scope 到权限、RequiresStepUp、MaxSessions、HTTP Claim 篡改、Host operate-as 双 ORM 隔离和审计四元组。
10. 源码扫尾
# 查找签发 Endpoint、Claim 写入和 CurrentTenant 还原;当前只应命中契约、服务和校验 Middleware。rg -n "TenantImpersonationTokenDescriptor|TenantImpersonationGrantId|TenantImpersonationExpiresAt" \ src/Hosts src/Platform/Identity src/Framework -g '*.cs'
# Scope、StepUp 和 MaxSessions 当前只有模型定义;出现消费点后必须更新本页与测试。rg -n "RequiresStepUp|MaxSessions|\.Scope" \ src/Platform/Identity/BitzOrcas.Identity.Application/Tenancy \ src/Hosts/BitzOrcas.Api -g '*.cs'