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
@@ -61,18 +61,31 @@ type ManifestSecurityEvents struct {
Audience string `json:"audience"`
}
type ManifestTenantContext struct {
Endpoint string `json:"endpoint"`
Audience string `json:"audience"`
Scope string `json:"scope"`
}
type ManifestV1 struct {
SchemaVersion int `json:"schema_version"`
TenantMode string `json:"tenant_mode,omitempty"`
Issuer string `json:"issuer"`
TenantID string `json:"tenant_id"`
TenantID string `json:"tenant_id,omitempty"`
ApplicationID string `json:"application_id"`
Capabilities []string `json:"capabilities"`
Audience string `json:"audience,omitempty"`
Scopes []string `json:"scopes"`
Clients ManifestClients `json:"clients"`
TenantContext *ManifestTenantContext `json:"tenant_context,omitempty"`
SecurityEvents *ManifestSecurityEvents `json:"security_events,omitempty"`
}
// ManifestV2 intentionally shares the wire structure with ManifestV1 so the
// onboarding delivery can be decoded without guessing a union variant. Validate
// always branches on schema_version and rejects fields from the other mode.
type ManifestV2 = ManifestV1
type MachineCredential struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
@@ -86,11 +99,30 @@ type CredentialDelivery struct {
}
func (manifest ManifestV1) Validate(appEnv string) error {
if manifest.SchemaVersion != 1 || validatePublicIdentityURL(manifest.Issuer, appEnv) != nil {
if validatePublicIdentityURL(manifest.Issuer, appEnv) != nil {
return errors.New("application manifest identity metadata is invalid")
}
if _, err := uuid.Parse(manifest.TenantID); err != nil {
return errors.New("application manifest tenant is invalid")
switch manifest.SchemaVersion {
case 1:
if strings.TrimSpace(manifest.TenantMode) != "" || manifest.TenantContext != nil {
return errors.New("application manifest tenant mode is invalid")
}
if _, err := uuid.Parse(manifest.TenantID); err != nil {
return errors.New("application manifest tenant is invalid")
}
case 2:
if manifest.TenantMode != "multi_tenant" || strings.TrimSpace(manifest.TenantID) != "" {
return errors.New("application manifest tenant mode is invalid")
}
if manifest.TenantContext == nil ||
validatePublicIdentityURL(strings.ReplaceAll(manifest.TenantContext.Endpoint, "{tenantId}", uuid.Nil.String()), appEnv) != nil ||
!strings.Contains(manifest.TenantContext.Endpoint, "{tenantId}") ||
manifest.TenantContext.Audience != "urn:easyai:auth-center:tenant-context" ||
manifest.TenantContext.Scope != "tenant.context.read" {
return errors.New("application manifest tenant context is invalid")
}
default:
return errors.New("application manifest identity metadata is invalid")
}
if _, err := uuid.Parse(manifest.ApplicationID); err != nil {
return errors.New("application manifest application is invalid")
@@ -107,6 +139,11 @@ func (manifest ManifestV1) Validate(appEnv string) error {
capabilities["token_introspection"] && !capabilities["machine_to_machine"] {
return errors.New("application manifest capability dependencies are invalid")
}
if manifest.SchemaVersion == 2 &&
(!capabilities["oidc_login"] || !capabilities["machine_to_machine"] ||
!capabilities["token_introspection"] || !capabilities["session_revocation"]) {
return errors.New("application manifest multi-tenant capabilities are invalid")
}
if capabilities["oidc_login"] && (manifest.Clients.BrowserLogin == nil || strings.TrimSpace(manifest.Clients.BrowserLogin.ClientID) == "") {
return errors.New("application manifest browser client is missing")
}