refactor(identity): 统一网页认证配置来源
移除旧 OIDC_* 与 VITE_OIDC_* 业务配置读取,统一使用数据库 Revision 和 SecretStore 引用构建运行时。同步更新 Compose、示例配置、接入文档及集成测试,并保留数据库、SecretStore 和安全事件健康窗口等部署级参数。\n\n验证:go test ./...;go test -race ./...;go vet ./...;pnpm test;pnpm lint;pnpm build;docker compose config -q
This commit is contained in:
@@ -1,182 +1,59 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadOIDCJITProvisioningDefaultsToDisabled(t *testing.T) {
|
||||
t.Setenv("OIDC_JIT_PROVISIONING_ENABLED", "")
|
||||
t.Setenv("OIDC_GATEWAY_TENANT_KEY", "")
|
||||
func TestLoadIdentitySecretStoreUsesNewEnvironmentNamesAndIgnoresLegacyBusinessValues(t *testing.T) {
|
||||
t.Setenv("IDENTITY_SECRET_STORE", "file")
|
||||
t.Setenv("IDENTITY_SECRET_DIR", ".test-secrets/identity")
|
||||
t.Setenv("OIDC_ISSUER", "https://legacy-sensitive.example/issuer")
|
||||
t.Setenv("OIDC_INTROSPECTION_CLIENT_SECRET", "legacy-sensitive-secret")
|
||||
t.Setenv("OIDC_SESSION_ENCRYPTION_KEY", "legacy-sensitive-session-key")
|
||||
|
||||
cfg := Load()
|
||||
if cfg.OIDCJITProvisioningEnabled {
|
||||
t.Fatal("OIDC JIT provisioning must be disabled by default")
|
||||
if cfg.IdentitySecretStore != "file" || cfg.IdentitySecretDir != ".test-secrets/identity" {
|
||||
t.Fatalf("identity SecretStore = %q %q", cfg.IdentitySecretStore, cfg.IdentitySecretDir)
|
||||
}
|
||||
if cfg.OIDCGatewayTenantKey != "" {
|
||||
t.Fatalf("unexpected gateway tenant key: %q", cfg.OIDCGatewayTenantKey)
|
||||
printed := fmt.Sprintf("%+v", cfg)
|
||||
for _, legacyValue := range []string{"legacy-sensitive.example", "legacy-sensitive-secret", "legacy-sensitive-session-key"} {
|
||||
if strings.Contains(printed, legacyValue) {
|
||||
t.Fatalf("legacy identity business setting was loaded into Config: %q", legacyValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRequiresGatewayTenantKeyWhenOIDCJITIsEnabled(t *testing.T) {
|
||||
cfg := Config{
|
||||
OIDCEnabled: true,
|
||||
OIDCJITProvisioningEnabled: true,
|
||||
func TestValidateIdentityFileSecretStoreRequiresDirectory(t *testing.T) {
|
||||
cfg := Config{IdentitySecretStore: "file"}
|
||||
if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "IDENTITY_SECRET_DIR") {
|
||||
t.Fatalf("Validate() error = %v, want missing identity secret directory", err)
|
||||
}
|
||||
|
||||
err := cfg.Validate()
|
||||
if err == nil || !strings.Contains(err.Error(), "OIDC_GATEWAY_TENANT_KEY") {
|
||||
t.Fatalf("Validate() error = %v, want missing OIDC_GATEWAY_TENANT_KEY", err)
|
||||
}
|
||||
|
||||
cfg.OIDCGatewayTenantKey = "default"
|
||||
cfg.IdentitySecretDir = ".test-secrets/identity"
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("Validate() with tenant key: %v", err)
|
||||
t.Fatalf("valid file SecretStore was rejected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadOIDCBrowserSessionUsesSafeEnvironmentDefaults(t *testing.T) {
|
||||
t.Setenv("APP_ENV", "development")
|
||||
t.Setenv("OIDC_BROWSER_SESSION_ENABLED", "")
|
||||
t.Setenv("OIDC_SESSION_COOKIE_SECURE", "")
|
||||
|
||||
cfg := Load()
|
||||
if !cfg.OIDCBrowserSessionEnabled {
|
||||
t.Fatal("OIDC browser session should be enabled by default")
|
||||
func TestValidateIdentityKubernetesSecretStore(t *testing.T) {
|
||||
cfg := Config{IdentitySecretStore: "kubernetes", IdentityKubernetesSecretName: "easyai-gateway-identity"}
|
||||
if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "namespace") {
|
||||
t.Fatalf("Validate() error = %v, want missing namespace", err)
|
||||
}
|
||||
if cfg.OIDCSessionCookieSecure {
|
||||
t.Fatal("development cookie should allow localhost HTTP by default")
|
||||
}
|
||||
|
||||
t.Setenv("APP_ENV", "production")
|
||||
cfg = Load()
|
||||
if !cfg.OIDCSessionCookieSecure {
|
||||
t.Fatal("production OIDC session cookie must default to Secure")
|
||||
}
|
||||
|
||||
t.Setenv("APP_ENV", "staging")
|
||||
cfg = Load()
|
||||
if !cfg.OIDCSessionCookieSecure {
|
||||
t.Fatal("staging OIDC session cookie must default to Secure")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRejectsInsecureNonLocalOIDCBrowserSession(t *testing.T) {
|
||||
cfg := Config{
|
||||
AppEnv: "staging",
|
||||
OIDCEnabled: true,
|
||||
OIDCBrowserSessionEnabled: true,
|
||||
OIDCSessionCookieSecure: false,
|
||||
CORSAllowedOrigin: "https://gateway.example.com",
|
||||
OIDCClientID: "gateway-public",
|
||||
OIDCRedirectURI: "https://gateway.example.com/gateway-api/api/v1/auth/oidc/callback",
|
||||
OIDCPostLogoutRedirectURI: "https://gateway.example.com/",
|
||||
OIDCSessionEncryptionKey: base64.RawStdEncoding.EncodeToString(bytes.Repeat([]byte{1}, 32)),
|
||||
OIDCSessionIdleTTLSeconds: 1800,
|
||||
OIDCSessionAbsoluteTTLSeconds: 28800,
|
||||
OIDCSessionRefreshBeforeSeconds: 60,
|
||||
}
|
||||
if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "OIDC_SESSION_COOKIE_SECURE") {
|
||||
t.Fatalf("Validate() error = %v, want insecure non-local cookie rejection", err)
|
||||
}
|
||||
|
||||
cfg.OIDCSessionCookieSecure = true
|
||||
cfg.CORSAllowedOrigin = "*"
|
||||
if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "CORS_ALLOWED_ORIGIN") {
|
||||
t.Fatalf("Validate() error = %v, want wildcard credentialed CORS rejection", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRequiresCompletePublicClientSessionConfiguration(t *testing.T) {
|
||||
cfg := Config{
|
||||
AppEnv: "test", OIDCEnabled: true, OIDCBrowserSessionEnabled: true,
|
||||
OIDCSessionCookieSecure: false, CORSAllowedOrigin: "http://localhost:5178",
|
||||
}
|
||||
if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "OIDC_CLIENT_ID") {
|
||||
t.Fatalf("Validate() error = %v, want incomplete public client rejection", err)
|
||||
}
|
||||
|
||||
cfg.OIDCClientID = "gateway-public"
|
||||
cfg.OIDCRedirectURI = "http://localhost:8088/api/v1/auth/oidc/callback"
|
||||
cfg.OIDCPostLogoutRedirectURI = "http://localhost:5178/"
|
||||
cfg.OIDCSessionEncryptionKey = base64.RawStdEncoding.EncodeToString(bytes.Repeat([]byte{2}, 32))
|
||||
cfg.OIDCSessionIdleTTLSeconds = 1800
|
||||
cfg.OIDCSessionAbsoluteTTLSeconds = 28800
|
||||
cfg.OIDCSessionRefreshBeforeSeconds = 60
|
||||
cfg.IdentityKubernetesNamespace = "easyai"
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("Validate() valid public session config: %v", err)
|
||||
}
|
||||
key, err := cfg.OIDCSessionEncryptionKeyBytes()
|
||||
if err != nil || len(key) != 32 {
|
||||
t.Fatalf("decoded encryption key len=%d err=%v", len(key), err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRejectsPlaintextOrWrongLengthSessionKey(t *testing.T) {
|
||||
cfg := Config{
|
||||
AppEnv: "test", OIDCEnabled: true, OIDCBrowserSessionEnabled: true,
|
||||
OIDCClientID: "gateway-public", OIDCRedirectURI: "http://localhost:8088/callback",
|
||||
OIDCPostLogoutRedirectURI: "http://localhost:5178/", OIDCSessionEncryptionKey: strings.Repeat("x", 32),
|
||||
OIDCSessionIdleTTLSeconds: 1800, OIDCSessionAbsoluteTTLSeconds: 28800, OIDCSessionRefreshBeforeSeconds: 60,
|
||||
CORSAllowedOrigin: "http://localhost:5178",
|
||||
}
|
||||
if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "OIDC_SESSION_ENCRYPTION_KEY") {
|
||||
t.Fatalf("Validate() error = %v, want encoded AES-256 key rejection", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRejectsOfflineAccessForBrowserSession(t *testing.T) {
|
||||
cfg := Config{
|
||||
AppEnv: "test", OIDCEnabled: true, OIDCBrowserSessionEnabled: true,
|
||||
OIDCRequiredScopes: []string{"gateway.access", "offline_access"},
|
||||
}
|
||||
if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "offline_access") {
|
||||
t.Fatalf("Validate() error = %v, want offline_access rejection", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecurityEventsUseDurableConnectionInsteadOfAnEnableFlag(t *testing.T) {
|
||||
t.Setenv("OIDC_SECURITY_EVENTS_ENABLED", "true")
|
||||
t.Setenv("OIDC_SECURITY_EVENTS_TRANSMITTER_ISSUER", "https://untrusted.example/ssf")
|
||||
t.Setenv("OIDC_SECURITY_EVENTS_BEARER_SECRET", "must-not-be-loaded")
|
||||
t.Setenv("OIDC_SECURITY_EVENTS_SECRET_STORE", "file")
|
||||
t.Setenv("OIDC_SECURITY_EVENTS_SECRET_DIR", ".test-secrets/ssf")
|
||||
|
||||
cfg := Load()
|
||||
if cfg.OIDCSecurityEventsSecretStore != "file" || cfg.OIDCSecurityEventsSecretDir != ".test-secrets/ssf" {
|
||||
t.Fatalf("secret directory=%q", cfg.OIDCSecurityEventsSecretDir)
|
||||
}
|
||||
if err := (Config{}).Validate(); err != nil {
|
||||
t.Fatalf("an absent security event connection changed existing config: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateKubernetesSecurityEventSecretStore(t *testing.T) {
|
||||
config := Config{OIDCSecurityEventsSecretStore: "kubernetes", OIDCSecurityEventsKubernetesSecretName: "easyai-gateway-security-events"}
|
||||
if err := config.Validate(); err == nil || !strings.Contains(err.Error(), "namespace") {
|
||||
t.Fatalf("Validate() error=%v, want missing namespace", err)
|
||||
}
|
||||
config.OIDCSecurityEventsKubernetesNamespace = "easyai"
|
||||
if err := config.Validate(); err != nil {
|
||||
t.Fatalf("valid Kubernetes SecretStore was rejected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSecurityEventTiming(t *testing.T) {
|
||||
config := Config{OIDCSecurityEventsHeartbeatIntervalSeconds: 60, OIDCSecurityEventsStaleAfterSeconds: 60, OIDCSecurityEventsClockSkewSeconds: 60}
|
||||
if err := config.Validate(); err == nil || !strings.Contains(err.Error(), "heartbeat") {
|
||||
t.Fatalf("Validate() error=%v, want invalid stale threshold", err)
|
||||
func TestValidateIdentitySecurityEventTiming(t *testing.T) {
|
||||
cfg := Config{
|
||||
IdentitySecurityEventHeartbeatIntervalSeconds: 60,
|
||||
IdentitySecurityEventStaleAfterSeconds: 60,
|
||||
IdentitySecurityEventClockSkewSeconds: 60,
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadSecurityEventsReusesOnlyTheRFC7662MachineClient(t *testing.T) {
|
||||
t.Setenv("OIDC_INTROSPECTION_CLIENT_ID", "gateway-service")
|
||||
t.Setenv("OIDC_INTROSPECTION_CLIENT_SECRET", "service-secret")
|
||||
|
||||
cfg := Load()
|
||||
if cfg.OIDCIntrospectionClientID != "gateway-service" || cfg.OIDCIntrospectionClientSecret != "service-secret" {
|
||||
t.Fatal("RFC 7662 machine credentials were not preserved")
|
||||
if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "heartbeat") {
|
||||
t.Fatalf("Validate() error = %v, want invalid stale threshold", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user