fix(identity): 完善统一认证配对恢复与安全退役
修复 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。
This commit is contained in:
@@ -39,7 +39,7 @@ func TestOnboardingClientKeepsCodeAndExchangeTokenOutOfURLs(t *testing.T) {
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewOnboardingClient(server.URL, server.Client())
|
||||
client, err := NewOnboardingClient(server.URL, server.Client(), "production")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -63,17 +63,17 @@ func TestManifestV1ValidationRequiresStableFieldsAndCapabilityDependencies(t *te
|
||||
Clients: ManifestClients{BrowserLogin: &ManifestClient{ClientID: "browser"}, MachineToMachine: &ManifestClient{ClientID: "service"}},
|
||||
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(); err != nil {
|
||||
if err := valid.Validate("production"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
invalid := valid
|
||||
invalid.Capabilities = []string{"session_revocation"}
|
||||
if err := invalid.Validate(); err == nil {
|
||||
if err := invalid.Validate("production"); err == nil {
|
||||
t.Fatal("manifest with missing capability dependencies was accepted")
|
||||
}
|
||||
invalid = valid
|
||||
invalid.SchemaVersion = 2
|
||||
if err := invalid.Validate(); err == nil {
|
||||
if err := invalid.Validate("production"); err == nil {
|
||||
t.Fatal("unsupported manifest schema was accepted")
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ func TestOnboardingClientRejectsRedirects(t *testing.T) {
|
||||
http.Redirect(w, r, target.URL, http.StatusTemporaryRedirect)
|
||||
}))
|
||||
defer redirect.Close()
|
||||
client, err := NewOnboardingClient(redirect.URL, redirect.Client())
|
||||
client, err := NewOnboardingClient(redirect.URL, redirect.Client(), "production")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -95,3 +95,61 @@ func TestOnboardingClientRejectsRedirects(t *testing.T) {
|
||||
t.Fatal("redirecting onboarding endpoint was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManifestAllowsLoopbackHTTPOnlyInLocalEnvironments(t *testing.T) {
|
||||
manifest := ManifestV1{
|
||||
SchemaVersion: 1, Issuer: "https://auth.example.com/issuer/easyai",
|
||||
TenantID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", ApplicationID: "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
|
||||
Capabilities: []string{"machine_to_machine", "token_introspection", "session_revocation"}, Scopes: []string{"gateway.access"},
|
||||
Clients: ManifestClients{MachineToMachine: &ManifestClient{ClientID: "service"}},
|
||||
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",
|
||||
},
|
||||
}
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
mutate func(*ManifestV1)
|
||||
}{
|
||||
{name: "OIDC issuer", mutate: func(value *ManifestV1) { value.Issuer = "http://localhost:18003/issuer/easyai" }},
|
||||
{name: "SSF issuer", mutate: func(value *ManifestV1) { value.SecurityEvents.TransmitterIssuer = "http://127.0.0.1:18004/ssf" }},
|
||||
{name: "SSF configuration", mutate: func(value *ManifestV1) {
|
||||
value.SecurityEvents.ConfigurationEndpoint = "http://127.0.0.1:18004/.well-known/ssf-configuration/ssf"
|
||||
}},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
candidate := manifest
|
||||
securityEvents := *manifest.SecurityEvents
|
||||
candidate.SecurityEvents = &securityEvents
|
||||
test.mutate(&candidate)
|
||||
if err := candidate.Validate("production"); err == nil {
|
||||
t.Fatalf("production accepted loopback HTTP %s URL", test.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
localManifest := manifest
|
||||
localSecurityEvents := *manifest.SecurityEvents
|
||||
localManifest.SecurityEvents = &localSecurityEvents
|
||||
localManifest.Issuer = "http://localhost:18003/issuer/easyai"
|
||||
localManifest.SecurityEvents.TransmitterIssuer = "http://127.0.0.1:18004/ssf"
|
||||
localManifest.SecurityEvents.ConfigurationEndpoint = "http://127.0.0.1:18004/.well-known/ssf-configuration/ssf"
|
||||
if err := localManifest.Validate("test"); err != nil {
|
||||
t.Fatalf("test environment rejected loopback HTTP manifest URLs: %v", err)
|
||||
}
|
||||
localManifest.Capabilities = []string{"machine_to_machine"}
|
||||
localManifest.Issuer = manifest.Issuer
|
||||
if err := localManifest.Validate("production"); err == nil {
|
||||
t.Fatal("production accepted optional loopback HTTP security event metadata")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnboardingClientAllowsLoopbackHTTPOnlyInLocalEnvironments(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
|
||||
defer server.Close()
|
||||
if _, err := NewOnboardingClient(server.URL, server.Client(), "production"); err == nil {
|
||||
t.Fatal("production accepted loopback HTTP Auth Center URL")
|
||||
}
|
||||
if _, err := NewOnboardingClient(server.URL, server.Client(), "development"); err != nil {
|
||||
t.Fatalf("development rejected loopback HTTP Auth Center URL: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user