feat(ssf): 实现安全事件一键连接与凭据托管
新增数据库驱动的 SecurityEventConnectionManager,复用 RFC 7662 机器 Client,自动完成 Discovery、Push Bearer 生成、Stream 创建、Verification、首次启用、零停机轮换和安全退役。 增加文件与 Kubernetes SecretStore、最小权限 RBAC、动态 Receiver/撤销水位/内省降级,以及系统设置管理页面。已通过全量 Go 测试、go vet、关键包竞态测试、27 个 Web 测试、类型检查、生产构建、Compose 和 Kubernetes dry-run。
This commit is contained in:
@@ -137,33 +137,46 @@ func TestValidateRejectsOfflineAccessForBrowserSession(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecurityEventsAreOptionalButFailFastWhenPartiallyConfigured(t *testing.T) {
|
||||
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("disabled security events changed existing config: %v", err)
|
||||
t.Fatalf("an absent security event connection changed existing config: %v", err)
|
||||
}
|
||||
cfg := Config{AppEnv: "test", OIDCEnabled: true, OIDCSecurityEventsEnabled: true}
|
||||
if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "issuer") {
|
||||
t.Fatalf("partial security event config error = %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)
|
||||
}
|
||||
cfg.OIDCSecurityEventsTransmitterIssuer = "http://localhost:8080/ssf"
|
||||
cfg.OIDCSecurityEventsPublicEndpoint = "http://localhost:8088/api/v1/security-events/ssf"
|
||||
cfg.OIDCSecurityEventsManagementTokenURL = "http://localhost:8080/oauth/token"
|
||||
cfg.OIDCTenantID = "00000000-0000-4000-8000-000000000003"
|
||||
cfg.OIDCSecurityEventsReceiverAudience = "urn:easyai:ssf:receiver:00000000-0000-4000-8000-000000000002"
|
||||
cfg.OIDCSecurityEventsStreamID = "00000000-0000-4000-8000-000000000001"
|
||||
cfg.OIDCSecurityEventsBearerSecret = "receiver-secret-at-least-16"
|
||||
cfg.OIDCSecurityEventsManagementClientID = "gateway-ssf"
|
||||
cfg.OIDCSecurityEventsManagementClientSecret = "management-secret"
|
||||
cfg.OIDCIntrospectionClientID = "gateway-introspection"
|
||||
cfg.OIDCIntrospectionClientSecret = "introspection-secret"
|
||||
cfg.OIDCSecurityEventsHeartbeatIntervalSeconds = 60
|
||||
cfg.OIDCSecurityEventsStaleAfterSeconds = 180
|
||||
cfg.OIDCSecurityEventsClockSkewSeconds = 60
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("valid security event config: %v", err)
|
||||
config.OIDCSecurityEventsKubernetesNamespace = "easyai"
|
||||
if err := config.Validate(); err != nil {
|
||||
t.Fatalf("valid Kubernetes SecretStore was rejected: %v", err)
|
||||
}
|
||||
cfg.OIDCSecurityEventsPublicEndpoint = "http://localhost:8088/wrong-path"
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Fatal("non-canonical security event receiver path was accepted")
|
||||
}
|
||||
|
||||
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 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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user