easyai-ai-gateway/apps/api/internal/securityevents/kubernetes_secret_store_test.go
chengcheng 9efeb16fd1 feat(ssf): 实现安全事件一键连接与凭据托管
新增数据库驱动的 SecurityEventConnectionManager,复用 RFC 7662 机器 Client,自动完成 Discovery、Push Bearer 生成、Stream 创建、Verification、首次启用、零停机轮换和安全退役。

增加文件与 Kubernetes SecretStore、最小权限 RBAC、动态 Receiver/撤销水位/内省降级,以及系统设置管理页面。已通过全量 Go 测试、go vet、关键包竞态测试、27 个 Web 测试、类型检查、生产构建、Compose 和 Kubernetes dry-run。
2026-07-15 17:25:00 +08:00

96 lines
3.1 KiB
Go

package securityevents
import (
"context"
"encoding/base64"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sync"
"testing"
)
func TestKubernetesSecretStoreUsesOnePreprovisionedSecret(t *testing.T) {
var mutex sync.Mutex
data := map[string]string{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/namespaces/easyai/secrets/easyai-gateway-security-events" || r.Header.Get("Authorization") != "Bearer service-account-token" {
t.Fatalf("unexpected Kubernetes request: %s authorization=%t", r.URL.Path, r.Header.Get("Authorization") != "")
}
mutex.Lock()
defer mutex.Unlock()
switch r.Method {
case http.MethodGet:
_ = json.NewEncoder(w).Encode(map[string]any{"data": data})
case http.MethodPatch:
var patch struct {
Data map[string]*string `json:"data"`
}
if err := json.NewDecoder(r.Body).Decode(&patch); err != nil {
t.Fatal(err)
}
for key, value := range patch.Data {
if value == nil {
delete(data, key)
} else {
data[key] = *value
}
}
_ = json.NewEncoder(w).Encode(map[string]any{"data": data})
default:
http.Error(w, "unsupported", http.StatusMethodNotAllowed)
}
}))
defer server.Close()
tokenFile := filepath.Join(t.TempDir(), "token")
if err := os.WriteFile(tokenFile, []byte("service-account-token\n"), 0o600); err != nil {
t.Fatal(err)
}
secrets, err := NewKubernetesSecretStore(KubernetesSecretStoreConfig{
Namespace: "easyai", SecretName: "easyai-gateway-security-events", APIServer: server.URL,
TokenFile: tokenFile, HTTPClient: server.Client(),
})
if err != nil {
t.Fatal(err)
}
value := []byte("0123456789abcdef0123456789abcdef")
if err := secrets.Put(context.Background(), "ssf-push-connection", value); err != nil {
t.Fatal(err)
}
if data["ssf-push-connection"] != base64.StdEncoding.EncodeToString(value) {
t.Fatal("Push Bearer was not stored in Kubernetes Secret data")
}
loaded, err := secrets.Get(context.Background(), "ssf-push-connection")
if err != nil || string(loaded) != string(value) {
t.Fatalf("loaded secret mismatch: err=%v", err)
}
clear(loaded)
if err := secrets.Delete(context.Background(), "ssf-push-connection"); err != nil {
t.Fatal(err)
}
if _, ok := data["ssf-push-connection"]; ok {
t.Fatal("Push Bearer key was not removed")
}
}
func TestKubernetesSecretStoreRejectsUnprovisionedSecret(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { http.NotFound(w, nil) }))
defer server.Close()
tokenFile := filepath.Join(t.TempDir(), "token")
if err := os.WriteFile(tokenFile, []byte("token"), 0o600); err != nil {
t.Fatal(err)
}
secrets, err := NewKubernetesSecretStore(KubernetesSecretStoreConfig{
Namespace: "easyai", SecretName: "easyai-gateway-security-events", APIServer: server.URL,
TokenFile: tokenFile, HTTPClient: server.Client(),
})
if err != nil {
t.Fatal(err)
}
if err := secrets.Put(context.Background(), "ssf-push-connection", []byte("0123456789abcdef0123456789abcdef")); err == nil {
t.Fatal("missing pre-provisioned Kubernetes Secret was accepted")
}
}