新增数据库驱动的 SecurityEventConnectionManager,复用 RFC 7662 机器 Client,自动完成 Discovery、Push Bearer 生成、Stream 创建、Verification、首次启用、零停机轮换和安全退役。 增加文件与 Kubernetes SecretStore、最小权限 RBAC、动态 Receiver/撤销水位/内省降级,以及系统设置管理页面。已通过全量 Go 测试、go vet、关键包竞态测试、27 个 Web 测试、类型检查、生产构建、Compose 和 Kubernetes dry-run。
96 lines
3.1 KiB
Go
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")
|
|
}
|
|
}
|