Skip to content
bitzorcas
中EN

Concept

Multitenancy 运维租户切换与模拟

区分 Host Header 覆盖、开发调试和正式租户模拟,解释 Grant、Token、CurrentTenant 四元组、会话校验、审计与当前缺口。

Last updated

“切到客户租户看看”是高风险运维动作。它至少需要回答:谁获准切入、目标是谁、能做什么、何时失效、能否主动撤销,以及数据与审计分别归谁。当前源码同时存在轻量 Header 覆盖和正式 Grant/Token 模型,两者不能混称。

1. 三条路径对比

路径入口授权证据CurrentTenant状态 Guard会话校验
Host operate-asX-Operate-As-Tenant仅已认证 CallerType.Host普通目标快照跳过不触发
Development debugX-Tenant-Debug仅环境为 Development普通目标快照跳过不触发
正式模拟模型TenantImpersonationGrant + Token Descriptor操作人、目标、有效期、Scope应含完整四元组应检查目标Middleware 检查到期和撤销

前两条是解析覆盖;第三条才是可审计会话协议。

2. 正式模型

TenantImpersonationGrant 以操作人租户作为持久化 TenantId,唯一键为操作人租户 + 操作人 + 目标租户。它保存:

  • ValidFrom / ValidTo;
  • Scope,例如 Support、Diagnostics、Read、Full;
  • MaxSessions;
  • RequiresStepUp;
  • 软删除与租户隔离。

TenantImpersonationTokenService.IssueAsync 当前强制:

  1. 禁止 CurrentUser 用户级嵌套模拟;
  2. 目标非空且不能等于操作人租户;
  3. 当前操作人 + 目标存在有效 Grant;
  4. 当前时间处于 Grant 窗口;
  5. Token 到期取 min(2h, Grant.ValidTo - now)。
HTTP MiddlewareJWT / Session AdapterGrant RepositoryToken ServiceHost OperatorHTTP MiddlewareJWT / Session AdapterGrant RepositoryToken ServiceHost Operator当前源码缺少 Claim 签发与还原实现Issue(operator, targetTenant)Find operator + target in operator tenantactive GrantToken DescriptorRequest with impersonation claimsIsStillAuthorized(grantId, originalTenant)true / false

3. Token Descriptor 如何进入 CurrentTenant

正确快照必须同时保存数据面与审计面:

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错误码
独立到期为空或已到401TenantImpersonation.Expired
Token Service 未注册401TenantImpersonation.Unavailable
Grant Store Failure、Grant 删除或过期401TenantImpersonation.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. 正式端到端流程应是什么

操作人认证

检查 Host 权限 + StepUp

加载 Grant
所有权 / 目标 / 时间 / Scope

检查目标租户 Active / GracePeriod

检查 MaxSessions

签发短时 Token Claims

每请求还原 CurrentTenant 四元组

校验到期、撤销、操作人状态

目标租户 QueryFilter

Authorization 按 Scope 再决策

双租户 + Grant 安全审计

当前已交付的是 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 删除/过期。还必须增加:

撤销校验必须绑定 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. 源码扫尾

Terminal window
# 查找签发 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'

上一页:上下文与持久化 · 下一篇:后台任务与租户化资源

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%