feat(identity): 接入认证中心多租户登录

支持 Manifest V2 动态 tid 验证、Tenant Context 同步和租户内 JIT 投影,并保留 Manifest V1 与旧 Session 兼容。\n\n增加 tenantHint、租户切换、普通注册关闭及 application/principal/tenant 两级 SSF 撤销;迁移、定向安全测试和本地双租户跨仓 E2E 已通过。\n\nrelease_required=true;未执行 Release、Staging 或真实链路。
This commit is contained in:
2026-07-28 17:28:35 +08:00
parent 0b02e62c72
commit 5c679ff13f
45 changed files with 2986 additions and 139 deletions
@@ -78,6 +78,47 @@ func TestManifestV1ValidationRequiresStableFieldsAndCapabilityDependencies(t *te
}
}
func TestManifestV2ValidationRequiresExplicitMultiTenantRuntimeContract(t *testing.T) {
valid := ManifestV2{
SchemaVersion: 2, TenantMode: "multi_tenant",
Issuer: "https://auth.example.com/issuer/shared", ApplicationID: "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
Capabilities: []string{"oidc_login", "api_access", "machine_to_machine", "token_introspection", "session_revocation"},
Audience: "urn:easyai:resource:bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", Scopes: []string{"openid", "gateway.access"},
Clients: ManifestClients{
BrowserLogin: &ManifestClient{ClientID: "browser"},
MachineToMachine: &ManifestClient{ClientID: "service"},
},
TenantContext: &ManifestTenantContext{
Endpoint: "https://auth.example.com/api/v1/runtime/tenants/{tenantId}",
Audience: "urn:easyai:auth-center:tenant-context",
Scope: "tenant.context.read",
},
SecurityEvents: &ManifestSecurityEvents{
TransmitterIssuer: "https://auth.example.com/ssf",
ConfigurationEndpoint: "https://auth.example.com/.well-known/ssf-configuration/ssf",
Audience: "urn:easyai:ssf:receiver:bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
},
}
if err := valid.Validate("production"); err != nil {
t.Fatal(err)
}
for _, mutate := range []func(*ManifestV2){
func(value *ManifestV2) { value.TenantMode = "" },
func(value *ManifestV2) { value.TenantID = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" },
func(value *ManifestV2) { value.TenantContext = nil },
func(value *ManifestV2) { value.TenantContext.Scope = "" },
func(value *ManifestV2) { value.TenantContext.Audience = "urn:wrong" },
} {
candidate := valid
contextCopy := *valid.TenantContext
candidate.TenantContext = &contextCopy
mutate(&candidate)
if err := candidate.Validate("production"); err == nil {
t.Fatalf("invalid Manifest V2 was accepted: %#v", candidate)
}
}
}
func TestOnboardingClientRejectsRedirects(t *testing.T) {
target := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "should not be reached", http.StatusTeapot)