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:
@@ -0,0 +1,234 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
ssfreceiver "github.com/easyai/easyai-ai-gateway/apps/api/internal/securityevents"
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
||||
)
|
||||
|
||||
type securityEventConnectionRequest struct {
|
||||
TransmitterIssuer string `json:"transmitter_issuer"`
|
||||
}
|
||||
|
||||
type securityEventConnectionResponse struct {
|
||||
Connected bool `json:"connected"`
|
||||
Connection ssfreceiver.ConnectionView `json:"connection"`
|
||||
}
|
||||
|
||||
func (s *Server) getSecurityEventConnection(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
if s.securityEventManager == nil {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"connected": false, "lifecycleStatus": "disconnected", "prerequisites": s.securityEventPrerequisites()})
|
||||
return
|
||||
}
|
||||
connection, err := s.securityEventManager.Get(r.Context())
|
||||
if errors.Is(err, store.ErrSecurityEventConnectionNotFound) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"connected": false, "lifecycleStatus": "disconnected", "prerequisites": s.securityEventPrerequisites()})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
writeError(w, http.StatusServiceUnavailable, "security event connection state is unavailable", "security_event_state_unavailable")
|
||||
return
|
||||
}
|
||||
w.Header().Set("ETag", fmt.Sprintf(`W/"%d"`, connection.Version))
|
||||
writeJSON(w, http.StatusOK, map[string]any{"connected": true, "connection": connection, "prerequisites": s.securityEventPrerequisites()})
|
||||
}
|
||||
|
||||
func (s *Server) putSecurityEventConnection(w http.ResponseWriter, r *http.Request) {
|
||||
if s.securityEventManager == nil {
|
||||
writeError(w, http.StatusConflict, "OIDC must be configured before connecting security events", "security_event_prerequisite_missing")
|
||||
return
|
||||
}
|
||||
idempotencyKey, ok := requiredConnectionIdempotencyKey(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request securityEventConnectionRequest
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 16*1024)
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&request); err != nil || strings.TrimSpace(request.TransmitterIssuer) == "" {
|
||||
writeError(w, http.StatusBadRequest, "invalid security event connection request", "invalid_request")
|
||||
return
|
||||
}
|
||||
request.TransmitterIssuer = strings.TrimRight(strings.TrimSpace(request.TransmitterIssuer), "/")
|
||||
requestHash := securityEventOperationHash("connect", request.TransmitterIssuer)
|
||||
if s.replaySecurityEventOperation(w, r, "connect", idempotencyKey, requestHash) {
|
||||
return
|
||||
}
|
||||
if current, err := s.securityEventManager.Get(r.Context()); err == nil && !matchConnectionVersion(w, r, current.Version) {
|
||||
return
|
||||
}
|
||||
connection, err := s.securityEventManager.Connect(r.Context(), request.TransmitterIssuer, idempotencyKey)
|
||||
if err != nil {
|
||||
writeSecurityEventConnectionError(w, err)
|
||||
return
|
||||
}
|
||||
s.writeSecurityEventOperation(w, r, "connect", idempotencyKey, requestHash, connection)
|
||||
}
|
||||
|
||||
func (s *Server) verifySecurityEventConnection(w http.ResponseWriter, r *http.Request) {
|
||||
idempotencyKey, requestHash, ok := s.beginSecurityEventOperation(w, r, "verify")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
connection, err := s.securityEventManager.Verify(r.Context())
|
||||
if err != nil {
|
||||
writeSecurityEventConnectionError(w, err)
|
||||
return
|
||||
}
|
||||
s.writeSecurityEventOperation(w, r, "verify", idempotencyKey, requestHash, connection)
|
||||
}
|
||||
|
||||
func (s *Server) rotateSecurityEventConnectionCredential(w http.ResponseWriter, r *http.Request) {
|
||||
idempotencyKey, requestHash, ok := s.beginSecurityEventOperation(w, r, "rotate")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
connection, err := s.securityEventManager.RotateCredential(r.Context())
|
||||
if err != nil {
|
||||
writeSecurityEventConnectionError(w, err)
|
||||
return
|
||||
}
|
||||
s.writeSecurityEventOperation(w, r, "rotate", idempotencyKey, requestHash, connection)
|
||||
}
|
||||
|
||||
func (s *Server) deleteSecurityEventConnection(w http.ResponseWriter, r *http.Request) {
|
||||
idempotencyKey, requestHash, ok := s.beginSecurityEventOperation(w, r, "disconnect")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
connection, err := s.securityEventManager.Disconnect(r.Context())
|
||||
if err != nil {
|
||||
writeSecurityEventConnectionError(w, err)
|
||||
return
|
||||
}
|
||||
s.writeSecurityEventOperation(w, r, "disconnect", idempotencyKey, requestHash, connection)
|
||||
}
|
||||
|
||||
func (s *Server) securityEventPrerequisites() map[string]any {
|
||||
return map[string]any{
|
||||
"oidcConfigured": s.cfg.OIDCEnabled && s.cfg.OIDCIssuer != "" && s.cfg.OIDCTenantID != "",
|
||||
"introspectionClientConfigured": s.cfg.OIDCIntrospectionClientID != "" && s.cfg.OIDCIntrospectionClientSecret != "",
|
||||
"publicBaseUrlConfigured": s.cfg.PublicBaseURL != "",
|
||||
"managementClientId": s.cfg.OIDCIntrospectionClientID,
|
||||
}
|
||||
}
|
||||
|
||||
func requiredConnectionIdempotencyKey(w http.ResponseWriter, r *http.Request) (string, bool) {
|
||||
value := strings.TrimSpace(r.Header.Get("Idempotency-Key"))
|
||||
if value == "" || len(value) > 255 {
|
||||
writeError(w, http.StatusBadRequest, "Idempotency-Key is required", "idempotency_key_required")
|
||||
return "", false
|
||||
}
|
||||
return value, true
|
||||
}
|
||||
|
||||
func (s *Server) beginSecurityEventOperation(w http.ResponseWriter, r *http.Request, operation string) (string, string, bool) {
|
||||
if s.securityEventManager == nil {
|
||||
writeError(w, http.StatusNotFound, "security event connection does not exist", "security_event_connection_not_found")
|
||||
return "", "", false
|
||||
}
|
||||
idempotencyKey, ok := requiredConnectionIdempotencyKey(w, r)
|
||||
if !ok {
|
||||
return "", "", false
|
||||
}
|
||||
requestHash := securityEventOperationHash(operation, normalizedConnectionETag(r.Header.Get("If-Match")))
|
||||
if s.replaySecurityEventOperation(w, r, operation, idempotencyKey, requestHash) {
|
||||
return "", "", false
|
||||
}
|
||||
connection, err := s.securityEventManager.Get(r.Context())
|
||||
if err != nil {
|
||||
writeSecurityEventConnectionError(w, err)
|
||||
return "", "", false
|
||||
}
|
||||
return idempotencyKey, requestHash, matchConnectionVersion(w, r, connection.Version)
|
||||
}
|
||||
|
||||
func (s *Server) replaySecurityEventOperation(w http.ResponseWriter, r *http.Request, operation, idempotencyKey, requestHash string) bool {
|
||||
if s.store == nil {
|
||||
return false
|
||||
}
|
||||
record, err := s.store.SecurityEventConnectionIdempotency(r.Context(), operation, idempotencyKey)
|
||||
if errors.Is(err, store.ErrSecurityEventConnectionNotFound) {
|
||||
return false
|
||||
}
|
||||
if err != nil {
|
||||
writeError(w, http.StatusServiceUnavailable, "security event idempotency state is unavailable", "security_event_state_unavailable")
|
||||
return true
|
||||
}
|
||||
if record.RequestHash != requestHash {
|
||||
writeError(w, http.StatusConflict, "Idempotency-Key was already used for a different request", "idempotency_key_reused")
|
||||
return true
|
||||
}
|
||||
var payload securityEventConnectionResponse
|
||||
if json.Unmarshal(record.Response, &payload) != nil {
|
||||
writeError(w, http.StatusServiceUnavailable, "security event idempotency response is unavailable", "security_event_state_unavailable")
|
||||
return true
|
||||
}
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.Header().Set("Idempotent-Replayed", "true")
|
||||
w.Header().Set("ETag", fmt.Sprintf(`W/"%d"`, payload.Connection.Version))
|
||||
writeJSON(w, http.StatusAccepted, payload)
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Server) writeSecurityEventOperation(w http.ResponseWriter, r *http.Request, operation, idempotencyKey, requestHash string, connection ssfreceiver.ConnectionView) {
|
||||
payload := securityEventConnectionResponse{Connected: true, Connection: connection}
|
||||
encoded, _ := json.Marshal(payload)
|
||||
if s.store != nil {
|
||||
if err := s.store.RecordSecurityEventConnectionIdempotency(r.Context(), operation, idempotencyKey, requestHash, encoded); err != nil && s.logger != nil {
|
||||
s.logger.Error("security event idempotency result could not be recorded", "error_category", "idempotency_store_failed", "operation", operation)
|
||||
}
|
||||
}
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.Header().Set("ETag", fmt.Sprintf(`W/"%d"`, connection.Version))
|
||||
writeJSON(w, http.StatusAccepted, payload)
|
||||
}
|
||||
|
||||
func securityEventOperationHash(operation, canonicalRequest string) string {
|
||||
digest := sha256.Sum256([]byte(operation + "\x00" + canonicalRequest))
|
||||
return fmt.Sprintf("%x", digest[:])
|
||||
}
|
||||
|
||||
func normalizedConnectionETag(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
value = strings.TrimPrefix(value, "W/")
|
||||
return strings.Trim(value, `"`)
|
||||
}
|
||||
|
||||
func matchConnectionVersion(w http.ResponseWriter, r *http.Request, expected int64) bool {
|
||||
value := strings.TrimSpace(r.Header.Get("If-Match"))
|
||||
value = strings.TrimPrefix(value, "W/")
|
||||
value = strings.Trim(value, `"`)
|
||||
version, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusPreconditionRequired, "If-Match is required", "if_match_required")
|
||||
return false
|
||||
}
|
||||
if version != expected {
|
||||
writeError(w, http.StatusPreconditionFailed, "security event connection version changed", "version_conflict")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func writeSecurityEventConnectionError(w http.ResponseWriter, err error) {
|
||||
switch {
|
||||
case errors.Is(err, store.ErrSecurityEventConnectionNotFound):
|
||||
writeError(w, http.StatusNotFound, "security event connection does not exist", "security_event_connection_not_found")
|
||||
case errors.Is(err, store.ErrSecurityEventConnectionConflict):
|
||||
writeError(w, http.StatusConflict, "security event connection conflicts with current state", "security_event_connection_conflict")
|
||||
case strings.Contains(err.Error(), "configured"), strings.Contains(err.Error(), "invalid"), strings.Contains(err.Error(), "HTTPS"):
|
||||
writeError(w, http.StatusConflict, "security event connection prerequisites are incomplete", "security_event_prerequisite_missing")
|
||||
default:
|
||||
writeError(w, http.StatusBadGateway, "authentication center security event service is unavailable", "security_event_transmitter_unavailable")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/config"
|
||||
)
|
||||
|
||||
func TestSecurityEventConnectionWriteHeaders(t *testing.T) {
|
||||
request := httptest.NewRequest(http.MethodPost, "/connection/verify", nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
if _, ok := requiredConnectionIdempotencyKey(recorder, request); ok || recorder.Code != http.StatusBadRequest {
|
||||
t.Fatalf("missing Idempotency-Key status=%d", recorder.Code)
|
||||
}
|
||||
|
||||
request = httptest.NewRequest(http.MethodPost, "/connection/verify", nil)
|
||||
request.Header.Set("If-Match", `W/"7"`)
|
||||
recorder = httptest.NewRecorder()
|
||||
if !matchConnectionVersion(recorder, request, 7) {
|
||||
t.Fatalf("valid weak ETag was rejected: status=%d", recorder.Code)
|
||||
}
|
||||
|
||||
request.Header.Set("If-Match", `"6"`)
|
||||
recorder = httptest.NewRecorder()
|
||||
if matchConnectionVersion(recorder, request, 7) || recorder.Code != http.StatusPreconditionFailed {
|
||||
t.Fatalf("stale ETag status=%d", recorder.Code)
|
||||
}
|
||||
if normalizedConnectionETag(`W/"7"`) != "7" ||
|
||||
securityEventOperationHash("verify", "7") == securityEventOperationHash("verify", "8") {
|
||||
t.Fatal("security event idempotency request fingerprint is not stable")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecurityEventPrerequisitesNeverExposeSecrets(t *testing.T) {
|
||||
server := &Server{cfg: config.Config{
|
||||
OIDCEnabled: true, OIDCIssuer: "https://auth.example/issuer/shared", OIDCTenantID: "stable-tenant",
|
||||
OIDCIntrospectionClientID: "gateway-machine", OIDCIntrospectionClientSecret: "must-not-escape",
|
||||
PublicBaseURL: "https://gateway.example",
|
||||
}}
|
||||
payload, err := json.Marshal(server.securityEventPrerequisites())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(payload), "must-not-escape") || strings.Contains(strings.ToLower(string(payload)), "secret") {
|
||||
t.Fatalf("secret escaped in prerequisites: %s", payload)
|
||||
}
|
||||
}
|
||||
@@ -18,5 +18,9 @@ import "net/http"
|
||||
// @Failure 503 {object} map[string]string
|
||||
// @Router /api/v1/security-events/ssf [post]
|
||||
func (s *Server) receiveSecurityEvent(w http.ResponseWriter, r *http.Request) {
|
||||
if s.securityEventReceiver == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
s.securityEventReceiver.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ type Server struct {
|
||||
logger *slog.Logger
|
||||
geminiUploadSessions sync.Map
|
||||
securityEventReceiver http.Handler
|
||||
securityEventManager *ssfreceiver.ConnectionManager
|
||||
}
|
||||
|
||||
type oidcPublicClient interface {
|
||||
@@ -66,46 +67,29 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor
|
||||
server.auth.ServerMainInternalKey = cfg.ServerMainInternalKey
|
||||
server.auth.ServerMainInternalSecret = cfg.ServerMainInternalSecret
|
||||
securityEventMetrics := &ssfreceiver.Metrics{}
|
||||
var securityEventService *ssfreceiver.Service
|
||||
if cfg.OIDCSecurityEventsEnabled {
|
||||
ssfVerifier, err := ssfreceiver.NewVerifier(ssfreceiver.VerifierConfig{
|
||||
TransmitterIssuer: cfg.OIDCSecurityEventsTransmitterIssuer,
|
||||
Audience: cfg.OIDCSecurityEventsReceiverAudience, SubjectIssuer: cfg.OIDCIssuer,
|
||||
TenantID: cfg.OIDCTenantID, StreamID: cfg.OIDCSecurityEventsStreamID,
|
||||
ClockSkew: time.Duration(cfg.OIDCSecurityEventsClockSkewSeconds) * time.Second,
|
||||
})
|
||||
if cfg.OIDCEnabled {
|
||||
secretStore, err := securityEventSecretStore(cfg)
|
||||
if err != nil {
|
||||
panic("invalid OIDC security event verifier configuration: " + err.Error())
|
||||
panic("invalid OIDC security event secret store: " + err.Error())
|
||||
}
|
||||
ssfVerifier.SetMetrics(securityEventMetrics)
|
||||
securityEventService, err = ssfreceiver.NewService(db, ssfreceiver.ServiceConfig{
|
||||
Issuer: cfg.OIDCSecurityEventsTransmitterIssuer, Audience: cfg.OIDCSecurityEventsReceiverAudience,
|
||||
StreamID: cfg.OIDCSecurityEventsStreamID, ManagementTokenURL: cfg.OIDCSecurityEventsManagementTokenURL,
|
||||
ManagementClientID: cfg.OIDCSecurityEventsManagementClientID, ManagementSecret: cfg.OIDCSecurityEventsManagementClientSecret,
|
||||
manager, err := ssfreceiver.NewConnectionManager(ctx, db, secretStore, ssfreceiver.ConnectionManagerConfig{
|
||||
AppEnv: cfg.AppEnv, OIDCEnabled: cfg.OIDCEnabled, OIDCIssuer: cfg.OIDCIssuer, OIDCTenantID: cfg.OIDCTenantID,
|
||||
ManagementClientID: cfg.OIDCIntrospectionClientID, ManagementClientSecret: cfg.OIDCIntrospectionClientSecret,
|
||||
PublicBaseURL: cfg.PublicBaseURL,
|
||||
HeartbeatInterval: time.Duration(cfg.OIDCSecurityEventsHeartbeatIntervalSeconds) * time.Second,
|
||||
StaleAfter: time.Duration(cfg.OIDCSecurityEventsStaleAfterSeconds) * time.Second,
|
||||
})
|
||||
ClockSkew: time.Duration(cfg.OIDCSecurityEventsClockSkewSeconds) * time.Second,
|
||||
}, securityEventMetrics)
|
||||
if err != nil {
|
||||
panic("invalid OIDC security event heartbeat configuration: " + err.Error())
|
||||
panic("initialize OIDC security event connection manager: " + err.Error())
|
||||
}
|
||||
securityEventService.SetMetrics(securityEventMetrics)
|
||||
if err := securityEventService.Start(ctx); err != nil {
|
||||
panic("OIDC security event state initialization failed")
|
||||
}
|
||||
receiver, err := ssfreceiver.NewHandler(
|
||||
ssfVerifier, db, cfg.OIDCSecurityEventsTransmitterIssuer, cfg.OIDCSecurityEventsReceiverAudience,
|
||||
cfg.OIDCSecurityEventsStreamID, cfg.OIDCSecurityEventsBearerSecret, cfg.OIDCSecurityEventsBearerSecretNext,
|
||||
)
|
||||
if err != nil {
|
||||
panic("invalid OIDC security event receiver configuration: " + err.Error())
|
||||
}
|
||||
receiver.SetMetrics(securityEventMetrics)
|
||||
server.securityEventReceiver = receiver
|
||||
server.securityEventManager = manager
|
||||
server.securityEventReceiver = manager
|
||||
}
|
||||
if cfg.OIDCEnabled {
|
||||
var evaluator func(context.Context, auth.OIDCSecurityEventIdentity) (auth.OIDCSecurityEventEvaluation, error)
|
||||
if securityEventService != nil {
|
||||
evaluator = securityEventService.Evaluate
|
||||
if server.securityEventManager != nil {
|
||||
evaluator = server.securityEventManager.Evaluate
|
||||
}
|
||||
verifier, err := auth.NewOIDCVerifier(auth.OIDCConfig{
|
||||
Issuer: cfg.OIDCIssuer, Audience: cfg.OIDCAudience, TenantID: cfg.OIDCTenantID,
|
||||
@@ -164,7 +148,7 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /healthz", server.health)
|
||||
mux.HandleFunc("GET /readyz", server.ready)
|
||||
mux.Handle("GET /metrics", securityEventMetrics.Handler(db, cfg.OIDCSecurityEventsTransmitterIssuer, cfg.OIDCSecurityEventsReceiverAudience, cfg.OIDCSecurityEventsEnabled))
|
||||
mux.Handle("GET /metrics", securityEventMetrics.DynamicHandler(db))
|
||||
mux.HandleFunc("GET /static/simulation/{asset}", serveSimulationAsset)
|
||||
mux.HandleFunc("GET /static/generated/{asset}", server.serveGeneratedStaticAsset)
|
||||
mux.HandleFunc("GET /static/uploaded/{asset}", server.serveUploadedStaticAsset)
|
||||
@@ -175,9 +159,7 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor
|
||||
mux.HandleFunc("GET /api/v1/auth/oidc/callback", server.completeOIDCLogin)
|
||||
mux.HandleFunc("POST /api/v1/auth/oidc/logout", server.logoutOIDCSession)
|
||||
mux.HandleFunc("DELETE /api/v1/auth/oidc/session", server.deleteOIDCBrowserSession)
|
||||
if server.securityEventReceiver != nil {
|
||||
mux.HandleFunc("POST /api/v1/security-events/ssf", server.receiveSecurityEvent)
|
||||
}
|
||||
mux.HandleFunc("POST /api/v1/security-events/ssf", server.receiveSecurityEvent)
|
||||
mux.Handle("GET /api/v1/me", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.me)))
|
||||
mux.Handle("GET /api/v1/public/catalog/providers", server.auth.Require(auth.PermissionPublic, http.HandlerFunc(server.listCatalogProviders)))
|
||||
mux.Handle("GET /api/v1/public/catalog/base-models", server.auth.Require(auth.PermissionPublic, http.HandlerFunc(server.listBaseModels)))
|
||||
@@ -247,6 +229,11 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor
|
||||
mux.Handle("PATCH /api/admin/system/file-storage/settings", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateFileStorageSettings)))
|
||||
mux.Handle("GET /api/admin/system/client-customization/settings", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.getClientCustomizationSettings)))
|
||||
mux.Handle("PATCH /api/admin/system/client-customization/settings", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateClientCustomizationSettings)))
|
||||
mux.Handle("GET /api/admin/system/identity/security-events/connection", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.getSecurityEventConnection)))
|
||||
mux.Handle("PUT /api/admin/system/identity/security-events/connection", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.putSecurityEventConnection)))
|
||||
mux.Handle("POST /api/admin/system/identity/security-events/connection/verify", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.verifySecurityEventConnection)))
|
||||
mux.Handle("POST /api/admin/system/identity/security-events/connection/rotate-credential", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.rotateSecurityEventConnectionCredential)))
|
||||
mux.Handle("DELETE /api/admin/system/identity/security-events/connection", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deleteSecurityEventConnection)))
|
||||
mux.Handle("GET /api/admin/system/file-storage/channels", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listFileStorageChannels)))
|
||||
mux.Handle("POST /api/admin/system/file-storage/channels", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createFileStorageChannel)))
|
||||
mux.Handle("PATCH /api/admin/system/file-storage/channels/{channelID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateFileStorageChannel)))
|
||||
@@ -324,6 +311,25 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor
|
||||
return server.recover(server.cors(server.protectOIDCSessionCookie(mux)))
|
||||
}
|
||||
|
||||
func securityEventSecretStore(cfg config.Config) (ssfreceiver.SecretStore, error) {
|
||||
switch strings.ToLower(strings.TrimSpace(cfg.OIDCSecurityEventsSecretStore)) {
|
||||
case "", "file":
|
||||
directory := cfg.OIDCSecurityEventsSecretDir
|
||||
if directory == "" {
|
||||
directory = ".local-secrets/ssf"
|
||||
}
|
||||
return ssfreceiver.NewFileSecretStore(directory)
|
||||
case "kubernetes":
|
||||
return ssfreceiver.NewKubernetesSecretStore(ssfreceiver.KubernetesSecretStoreConfig{
|
||||
Namespace: cfg.OIDCSecurityEventsKubernetesNamespace, SecretName: cfg.OIDCSecurityEventsKubernetesSecretName,
|
||||
APIServer: cfg.OIDCSecurityEventsKubernetesAPIServer, TokenFile: cfg.OIDCSecurityEventsKubernetesTokenFile,
|
||||
CAFile: cfg.OIDCSecurityEventsKubernetesCAFile,
|
||||
})
|
||||
default:
|
||||
return nil, errors.New("unsupported OIDC security event SecretStore")
|
||||
}
|
||||
}
|
||||
|
||||
func oidcSessionRequestError(err error) error {
|
||||
switch {
|
||||
case err == nil:
|
||||
@@ -361,7 +367,7 @@ func (s *Server) cors(next http.Handler) http.Handler {
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
w.Header().Set("Vary", "Origin")
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Comfy-Api-Key, X-Goog-Api-Key, X-Goog-Upload-Protocol, X-Goog-Upload-Command, X-Goog-Upload-Header-Content-Length, X-Goog-Upload-Header-Content-Type, X-Goog-Upload-Offset, X-Async, X-EasyAI-Conversation-ID")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, Idempotency-Key, If-Match, X-Comfy-Api-Key, X-Goog-Api-Key, X-Goog-Upload-Protocol, X-Goog-Upload-Command, X-Goog-Upload-Header-Content-Length, X-Goog-Upload-Header-Content-Type, X-Goog-Upload-Offset, X-Async, X-EasyAI-Conversation-ID")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
|
||||
}
|
||||
if r.Method == http.MethodOptions {
|
||||
|
||||
Reference in New Issue
Block a user