修复 credentials_saved 状态无法恢复、配对与激活并发冲突,以及 SSF 和身份 Secret 生命周期不完整的问题。新增持久化协调器、取消与清理状态机、事务级并发门禁、受控 SSF 凭据交接、禁用后的延迟 Secret 清理,并对生产环境统一认证及 Discovery 端点强制 HTTPS。 验证:go test ./...;go test -race ./internal/auth ./internal/identity ./internal/identityruntime ./internal/securityevents ./internal/httpapi ./internal/store -count=1;go vet ./...;真实 PostgreSQL 并发及清理成功/冲突回滚测试;pnpm openapi。
82 lines
3.9 KiB
Go
82 lines
3.9 KiB
Go
package identityruntime
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/easyai/easyai-ai-gateway/apps/api/internal/identity"
|
|
)
|
|
|
|
func TestRuntimeBuilderRejectsLoopbackHTTPOutsideLocalEnvironments(t *testing.T) {
|
|
revision := identity.Revision{
|
|
AuthCenterURL: "https://auth.example.com", Issuer: "https://auth.example.com/issuer/easyai",
|
|
PublicBaseURL: "https://api.example.com", WebBaseURL: "https://gateway.example.com",
|
|
SecurityEventIssuer: "https://auth.example.com/ssf", SecurityEventConfigURL: "https://auth.example.com/.well-known/ssf-configuration/ssf",
|
|
SessionRevocation: true,
|
|
}
|
|
production := &RuntimeBuilder{config: RuntimeBuilderConfig{AppEnv: "production"}}
|
|
for _, test := range []struct {
|
|
name string
|
|
mutate func(*identity.Revision)
|
|
}{
|
|
{name: "auth center", mutate: func(value *identity.Revision) { value.AuthCenterURL = "http://localhost:18000" }},
|
|
{name: "OIDC issuer", mutate: func(value *identity.Revision) { value.Issuer = "http://localhost:18003/issuer/easyai" }},
|
|
{name: "public base", mutate: func(value *identity.Revision) { value.PublicBaseURL = "http://127.0.0.1:18089" }},
|
|
{name: "web base", mutate: func(value *identity.Revision) { value.WebBaseURL = "http://localhost:5178" }},
|
|
{name: "SSF issuer", mutate: func(value *identity.Revision) { value.SecurityEventIssuer = "http://localhost:18004/ssf" }},
|
|
{name: "SSF configuration", mutate: func(value *identity.Revision) {
|
|
value.SecurityEventConfigURL = "http://localhost:18004/.well-known/ssf-configuration/ssf"
|
|
}},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
candidate := revision
|
|
test.mutate(&candidate)
|
|
if _, err := production.Build(context.Background(), candidate); err == nil || !strings.Contains(err.Error(), "HTTPS") {
|
|
t.Fatalf("production runtime did not reject loopback HTTP %s URL: %v", test.name, err)
|
|
}
|
|
})
|
|
}
|
|
localRevision := revision
|
|
localRevision.AuthCenterURL = "http://localhost:18000"
|
|
localRevision.Issuer = "http://localhost:18003/issuer/easyai"
|
|
localRevision.PublicBaseURL = "http://127.0.0.1:18089"
|
|
localRevision.WebBaseURL = "http://localhost:5178"
|
|
localRevision.SecurityEventIssuer = "http://localhost:18004/ssf"
|
|
localRevision.SecurityEventConfigURL = "http://localhost:18004/.well-known/ssf-configuration/ssf"
|
|
development := &RuntimeBuilder{config: RuntimeBuilderConfig{AppEnv: "development"}}
|
|
if _, err := development.Build(context.Background(), localRevision); err == nil || strings.Contains(err.Error(), "HTTPS") {
|
|
t.Fatalf("development runtime did not pass URL policy before completeness checks: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSecurityEventManagerConfigBindsRuntimeToTargetRevision(t *testing.T) {
|
|
builder := &RuntimeBuilder{config: RuntimeBuilderConfig{AppEnv: "test"}}
|
|
revision := identity.Revision{
|
|
ID: "revision-target",
|
|
Issuer: "https://auth.example/issuer",
|
|
TenantID: "tenant-public-id",
|
|
MachineClientID: "gateway-machine",
|
|
PublicBaseURL: "https://gateway.example",
|
|
SecurityEventIssuer: "https://auth.example/ssf/",
|
|
SecurityEventAudience: "urn:easyai:ssf:receiver:target",
|
|
}
|
|
|
|
runtimeConfig := builder.securityEventManagerConfig(revision, true)
|
|
if !runtimeConfig.StrictRevisionBinding ||
|
|
runtimeConfig.ExpectedTransmitterIssuer != "https://auth.example/ssf" ||
|
|
runtimeConfig.ExpectedAudience != revision.SecurityEventAudience ||
|
|
runtimeConfig.ManagementClientID != revision.MachineClientID ||
|
|
runtimeConfig.ExpectedOwnerKey != "identity-pairing-ssf-revision-target" {
|
|
t.Fatalf("runtime SSF binding config=%#v", runtimeConfig)
|
|
}
|
|
|
|
recoveryConfig := builder.securityEventManagerConfig(revision, false)
|
|
if recoveryConfig.StrictRevisionBinding {
|
|
t.Fatal("conflict recovery was incorrectly blocked by strict Revision binding")
|
|
}
|
|
if recoveryConfig.ExpectedOwnerKey != runtimeConfig.ExpectedOwnerKey {
|
|
t.Fatal("recovery manager lost the target owner needed for explicit validation")
|
|
}
|
|
}
|