From 81d11e01bb3c9cce696e45f00edb67958cf881d6 Mon Sep 17 00:00:00 2001 From: Ivan Godwin Date: Thu, 30 Oct 2025 23:29:25 -0700 Subject: [PATCH] Further auth and authz configuration --- api/rest/keys.go | 33 +++-- api/rest/router.go | 79 +++-------- cmd/server/main.go | 88 +++++++++--- config.yaml | 20 +++ go.mod | 32 ++++- go.sum | 84 +++++++++++- internal/auth/bootstrap.go | 229 ++++++++++++++++++++++++++++++- internal/auth/keystore_db.go | 41 ++++-- internal/auth/keystore_hybrid.go | 31 +---- internal/config/config.go | 50 ++++++- k8s/configmap.yaml | 20 +++ k8s/deployment.yaml | 105 ++++++++++++++ k8s/rbac.yaml | 31 +++++ 13 files changed, 695 insertions(+), 148 deletions(-) create mode 100644 k8s/rbac.yaml diff --git a/api/rest/keys.go b/api/rest/keys.go index 4312b2e..f1eb04d 100644 --- a/api/rest/keys.go +++ b/api/rest/keys.go @@ -27,21 +27,21 @@ func NewKeyManagementHandler(keyStore *auth.HybridKeyStore, logger *logging.Logg // CreateKeyRequest is the request body for creating a new API key type CreateKeyRequest struct { - ClientID string `json:"client_id"` - Roles []string `json:"roles"` - RateLimit int `json:"rate_limit,omitempty"` - ExpiresIn *time.Duration `json:"expires_in,omitempty"` + ClientID string `json:"client_id"` + Roles []string `json:"roles"` + RateLimit int `json:"rate_limit,omitempty"` + ExpiresIn string `json:"expires_in,omitempty"` // Duration string like "8760h", "30d", "1h", etc. } // CreateKeyResponse is the response body when creating an API key type CreateKeyResponse struct { - Key string `json:"key"` - Name string `json:"name"` - ClientID string `json:"client_id"` - Roles []string `json:"roles"` - CreatedAt time.Time `json:"created_at"` + Key string `json:"key"` + Name string `json:"name"` + ClientID string `json:"client_id"` + Roles []string `json:"roles"` + CreatedAt time.Time `json:"created_at"` ExpiresAt *time.Time `json:"expires_at,omitempty"` - RateLimit int `json:"rate_limit"` + RateLimit int `json:"rate_limit"` } // ListKeysResponse is the response body for listing API keys @@ -103,8 +103,19 @@ func (h *KeyManagementHandler) CreateKey(w http.ResponseWriter, r *http.Request) req.RateLimit = 100 } + // Parse expires_in duration string if provided + var expiresInDuration *time.Duration + if req.ExpiresIn != "" { + duration, err := time.ParseDuration(req.ExpiresIn) + if err != nil { + h.respondError(w, http.StatusBadRequest, "Invalid expires_in format", fmt.Sprintf("expected duration format like '8760h' or '30d': %v", err)) + return + } + expiresInDuration = &duration + } + // Create the key - apiKey, err := h.keyStore.CreateKey(ctx, req.ClientID, req.Roles, req.RateLimit, req.ExpiresIn, authCtx.ClientID) + apiKey, err := h.keyStore.CreateKey(ctx, req.ClientID, req.Roles, req.RateLimit, expiresInDuration, authCtx.ClientID) if err != nil { h.logger.Errorf("Failed to create API key: %v", err) h.respondError(w, http.StatusInternalServerError, "Failed to create API key", err.Error()) diff --git a/api/rest/router.go b/api/rest/router.go index 5cac423..140321e 100644 --- a/api/rest/router.go +++ b/api/rest/router.go @@ -2,8 +2,6 @@ package rest import ( "net/http" - "strconv" - "strings" "github.com/gorilla/mux" "github.com/igodwin/notifier/internal/auth" @@ -45,11 +43,16 @@ func DefaultCORSConfig() *CORSConfig { // NewRouter creates a new HTTP router with all routes configured func NewRouter(service domain.NotificationService, logger *logging.Logger) *mux.Router { - return NewRouterWithAuth(service, logger, nil, DefaultCORSConfig()) + return NewRouterWithAuth(service, logger, nil) } // NewRouterWithAuth creates a new HTTP router with optional authentication and CORS configuration -func NewRouterWithAuth(service domain.NotificationService, logger *logging.Logger, authStore *auth.APIKeyStore, corsConfig *CORSConfig) *mux.Router { +func NewRouterWithAuth(service domain.NotificationService, logger *logging.Logger, authStore *auth.APIKeyStore) *mux.Router { + return NewRouterWithAuthAndKeyStore(service, logger, authStore, nil) +} + +// NewRouterWithAuthAndKeyStore creates a new HTTP router with authentication and key management +func NewRouterWithAuthAndKeyStore(service domain.NotificationService, logger *logging.Logger, authStore *auth.APIKeyStore, keyStore *auth.HybridKeyStore) *mux.Router { handler := NewHandler(service, logger) router := mux.NewRouter() @@ -76,14 +79,21 @@ func NewRouterWithAuth(service domain.NotificationService, logger *logging.Logge // Notifiers route v1.HandleFunc("/notifiers", handler.GetNotifiers).Methods(http.MethodGet) + // Key management routes (requires auth and keystore) + if authStore != nil && keyStore != nil { + keyHandler := NewKeyManagementHandler(keyStore, logger) + v1.HandleFunc("/admin/keys", keyHandler.CreateKey).Methods(http.MethodPost) + v1.HandleFunc("/admin/keys", keyHandler.ListKeys).Methods(http.MethodGet) + v1.HandleFunc("/admin/keys/{key}", keyHandler.RevokeKey).Methods(http.MethodDelete) + v1.HandleFunc("/admin/keys/{key}/rotate", keyHandler.RotateKey).Methods(http.MethodPost) + v1.HandleFunc("/admin/keys/{key}/audit", keyHandler.GetAuditLog).Methods(http.MethodGet) + } + // Health check route (no auth required) router.HandleFunc("/health", handler.HealthCheck).Methods(http.MethodGet) - // Middleware - CORS must be applied before auth to handle preflight requests + // Middleware - logging and CORS router.Use(loggingMiddleware) - if corsConfig != nil { - router.Use(newCORSMiddleware(corsConfig)) - } return router } @@ -95,56 +105,3 @@ func loggingMiddleware(next http.Handler) http.Handler { next.ServeHTTP(w, r) }) } - -// newCORSMiddleware creates a CORS middleware with origin whitelist validation -func newCORSMiddleware(config *CORSConfig) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - origin := r.Header.Get("Origin") - - // Check if the origin is in the allowed list - allowed := false - for _, allowedOrigin := range config.AllowedOrigins { - if origin == allowedOrigin { - allowed = true - break - } - } - - // Only set CORS headers if the origin is allowed - if allowed { - // Set the exact origin (never use wildcard) - w.Header().Set("Access-Control-Allow-Origin", origin) - - // Set allowed methods - if len(config.AllowedMethods) > 0 { - w.Header().Set("Access-Control-Allow-Methods", strings.Join(config.AllowedMethods, ", ")) - } - - // Set allowed headers - if len(config.AllowedHeaders) > 0 { - w.Header().Set("Access-Control-Allow-Headers", strings.Join(config.AllowedHeaders, ", ")) - } - - // Set credentials header if enabled - if config.AllowCredentials { - w.Header().Set("Access-Control-Allow-Credentials", "true") - } - - // Set max age for preflight caching - if config.MaxAge > 0 { - w.Header().Set("Access-Control-Max-Age", strconv.FormatInt(int64(config.MaxAge), 10)) - } - } - - // Handle preflight OPTIONS requests - if r.Method == http.MethodOptions { - // Return 200 OK for preflight requests - w.WriteHeader(http.StatusOK) - return - } - - next.ServeHTTP(w, r) - }) - } -} diff --git a/cmd/server/main.go b/cmd/server/main.go index 4e3c1fa..acfe84b 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -88,11 +88,73 @@ func main() { // Initialize authentication if enabled (must be before service creation for RBAC) var authStore *auth.APIKeyStore + var hybridKeyStore *auth.HybridKeyStore var authz *auth.NotifierAuthz if cfg.Auth.Enabled { authStore = auth.NewAPIKeyStore() authz = auth.NewNotifierAuthz() logger.Info("API authentication enabled") + + // Create database backend if configured + var dbStore *auth.KeyStoreDB + if cfg.Auth.Database.URL != "" { + dbStore, err = auth.NewKeyStoreDB(cfg.Auth.Database.URL) + if err != nil { + logger.Fatalf("Failed to create database key store: %v", err) + } + logger.Infof("Connected to authentication database: %s", cfg.Auth.Database.URL) + } else { + logger.Warn("No database configured for authentication - API keys will only be stored in memory") + } + + // Create hybrid key store for key management (in-memory cache + database backend) + hybridKeyStore = auth.NewHybridKeyStore(authStore, dbStore) + logger.Debugf("Initialized hybrid key store for API key management") + + // Bootstrap admin key if configured + if cfg.Auth.Bootstrap.Enabled { + bootstrapCfg := &auth.BootstrapConfig{ + Enabled: cfg.Auth.Bootstrap.Enabled, + AdminKeyFileName: cfg.Auth.Bootstrap.AdminKeyFileName, + PrintToStdout: cfg.Auth.Bootstrap.PrintToStdout, + } + + // Try to load existing key from Kubernetes secret first + existingKey, err := auth.LoadAdminKeyFromKubernetesSecret( + ctx, + cfg.Auth.Bootstrap.KubernetesSecretName, + cfg.Auth.Bootstrap.KubernetesSecretKey, + logger, + ) + if err != nil { + logger.Warnf("Error loading from Kubernetes secret: %v", err) + } + + // If we have an existing key, use it + if existingKey != "" { + if _, err := auth.RegisterAdminKeyInMemory(authStore, existingKey, logger); err != nil { + logger.Warnf("Failed to register existing admin key: %v", err) + } + } else { + // Generate new key + if apiKey, err := auth.BootstrapAdminKeyInMemory(authStore, bootstrapCfg, logger); err != nil { + logger.Warnf("Bootstrap admin key creation failed: %v", err) + } else if apiKey != nil { + // Store in Kubernetes secret if configured + if cfg.Auth.Bootstrap.KubernetesSecretName != "" { + if err := auth.CreateKubernetesSecret( + ctx, + cfg.Auth.Bootstrap.KubernetesSecretName, + cfg.Auth.Bootstrap.KubernetesSecretKey, + apiKey.Key, + logger, + ); err != nil { + logger.Warnf("Failed to create Kubernetes secret: %v", err) + } + } + } + } + } } // Initialize notifier factory and register notifiers @@ -144,7 +206,7 @@ func main() { var restServer *http.Server if cfg.Server.Mode == "both" || cfg.Server.Mode == "rest" { wg.Add(1) - restServer = startRESTServer(ctx, &wg, cfg, svc, logger, authStore) + restServer = startRESTServer(ctx, &wg, cfg, svc, logger, authStore, hybridKeyStore) } // Wait for interrupt signal @@ -284,26 +346,16 @@ func startGRPCServer(ctx context.Context, wg *sync.WaitGroup, cfg *config.Config return grpcServer } -func startRESTServer(ctx context.Context, wg *sync.WaitGroup, cfg *config.Config, svc domain.NotificationService, logger *logging.Logger, authStore *auth.APIKeyStore) *http.Server { - // Convert config CORS to rest.CORSConfig - corsConfig := &rest.CORSConfig{ - AllowedOrigins: cfg.CORS.AllowedOrigins, - AllowedMethods: cfg.CORS.AllowedMethods, - AllowedHeaders: cfg.CORS.AllowedHeaders, - AllowCredentials: cfg.CORS.AllowCredentials, - MaxAge: cfg.CORS.MaxAge, - } - - // Log CORS configuration - if len(cfg.CORS.AllowedOrigins) > 0 { - logger.Infof("CORS enabled for origins: %v", cfg.CORS.AllowedOrigins) +func startRESTServer(ctx context.Context, wg *sync.WaitGroup, cfg *config.Config, svc domain.NotificationService, logger *logging.Logger, authStore *auth.APIKeyStore, hybridKeyStore *auth.HybridKeyStore) *http.Server { + var router *mux.Router + if authStore != nil && hybridKeyStore != nil { + router = rest.NewRouterWithAuthAndKeyStore(svc, logger, authStore, hybridKeyStore) + } else if authStore != nil { + router = rest.NewRouterWithAuth(svc, logger, authStore) } else { - logger.Warn("CORS has no allowed origins configured - all cross-origin requests will be blocked") + router = rest.NewRouter(svc, logger) } - // Create router with auth and CORS config - router := rest.NewRouterWithAuth(svc, logger, authStore, corsConfig) - addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.RESTPort) server := &http.Server{ Addr: addr, diff --git a/config.yaml b/config.yaml index 0a51b90..1212afc 100644 --- a/config.yaml +++ b/config.yaml @@ -92,6 +92,26 @@ notifiers: # default_topic: "company-notifications" # insecure_skip_verify: false # Set to true for self-signed certs +# Authentication and authorization configuration +# auth: +# enabled: false # Enable API key authentication +# default_rate_limit: 100 # Default requests per minute (0 = unlimited) +# bootstrap: +# enabled: false # Enable bootstrap admin key creation on startup +# admin_key_file: "/tmp/notifier-admin-key" # Save generated key to this file +# print_to_stdout: false # Print key to stdout (DANGER: only for setup) +# +# Example with bootstrap enabled for testing: +# auth: +# enabled: true +# default_rate_limit: 100 +# database: +# url: "postgresql://user:password@localhost:5432/notifier" +# bootstrap: +# enabled: true +# admin_key_file: "/tmp/notifier-admin-key" +# print_to_stdout: true + logging: level: "info" # Options: debug, info, warn, error format: "json" # Options: json, text diff --git a/go.mod b/go.mod index 9f9d411..195f962 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,9 @@ require ( github.com/testcontainers/testcontainers-go v0.39.0 google.golang.org/grpc v1.76.0 google.golang.org/protobuf v1.36.10 + k8s.io/api v0.34.1 + k8s.io/apimachinery v0.34.1 + k8s.io/client-go v0.34.1 ) require ( @@ -30,16 +33,25 @@ require ( github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/ebitengine/purego v0.8.4 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gnostic-models v0.7.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.10 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/go-archive v0.1.0 // indirect @@ -48,7 +60,10 @@ require ( github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/morikuni/aec v1.0.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect @@ -62,11 +77,12 @@ require ( github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/pflag v1.0.6 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect + github.com/x448/float16 v0.8.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect @@ -78,12 +94,26 @@ require ( go.opentelemetry.io/proto/otlp v1.8.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.43.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/net v0.46.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sys v0.37.0 // indirect + golang.org/x/term v0.36.0 // indirect golang.org/x/text v0.30.0 // indirect + golang.org/x/time v0.9.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index 5c6de83..e764c47 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,7 @@ github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpS github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -34,12 +35,16 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -47,13 +52,28 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= @@ -62,12 +82,19 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnV github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= @@ -76,6 +103,8 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -94,8 +123,20 @@ github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -125,8 +166,8 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -138,6 +179,7 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= @@ -150,6 +192,8 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= @@ -178,6 +222,10 @@ go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -193,6 +241,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -213,12 +263,14 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -237,6 +289,10 @@ google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -244,3 +300,23 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= +k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= +k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/auth/bootstrap.go b/internal/auth/bootstrap.go index 72cd306..89755f9 100644 --- a/internal/auth/bootstrap.go +++ b/internal/auth/bootstrap.go @@ -8,6 +8,10 @@ import ( "time" "github.com/igodwin/notifier/internal/logging" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" ) // BootstrapConfig holds configuration for bootstrap operations @@ -20,8 +24,110 @@ type BootstrapConfig struct { PrintToStdout bool } -// BootstrapAdminKey creates an initial admin API key on first startup -// This should be called once per deployment +// BootstrapAdminKeyInMemory creates an initial admin API key on first startup (in-memory store) +// This is a simpler version for in-memory APIKeyStore (without database persistence) +func BootstrapAdminKeyInMemory(keyStore *APIKeyStore, cfg *BootstrapConfig, logger *logging.Logger) (*APIKey, error) { + if !cfg.Enabled { + return nil, fmt.Errorf("bootstrap is disabled") + } + + // Check if bootstrap has already been done + if cfg.AdminKeyFileName != "" { + if _, err := os.Stat(cfg.AdminKeyFileName); err == nil { + // File exists, bootstrap already done + logger.Infof("Bootstrap key file exists at %s, skipping bootstrap", cfg.AdminKeyFileName) + return nil, fmt.Errorf("bootstrap already completed") + } + } + + // Create admin key with all roles + adminRoles := []string{"admin", "notify-email", "notify-slack", "notify-ntfy"} + apiKey, err := keyStore.CreateKey( + "admin-bootstrap", + adminRoles, + 0, // Unlimited rate limit + nil, // No expiration + ) + if err != nil { + return nil, fmt.Errorf("failed to create bootstrap admin key: %w", err) + } + + // Save key to file if configured + if cfg.AdminKeyFileName != "" { + keyContent := fmt.Sprintf(`# Notifier Admin Key +# Created: %s +# This key has full admin permissions +# KEEP THIS SECRET! + +%s +`, time.Now().Format(time.RFC3339), apiKey.Key) + + if err := os.WriteFile(cfg.AdminKeyFileName, []byte(keyContent), 0600); err != nil { + logger.Warnf("Failed to save admin key to file: %v", err) + } else { + logger.Infof("Admin key saved to %s", cfg.AdminKeyFileName) + } + } + + // Print to stdout if configured (DANGEROUS - only for interactive setup) + if cfg.PrintToStdout { + separator := strings.Repeat("=", 60) + fmt.Println("\n" + separator) + fmt.Println("NOTIFIER BOOTSTRAP: ADMIN KEY CREATED") + fmt.Println(separator) + fmt.Printf("Key: %s\n", apiKey.Key) + fmt.Println("\nSave this key in a secure location. You will not be able to see it again.") + fmt.Println("Use this key to create additional API keys via the key management API.") + fmt.Println(separator + "\n") + } + + logger.Infof("Bootstrap admin key created successfully") + return apiKey, nil +} + +// RegisterAdminKeyInMemory registers a pre-existing admin API key in the keystore +// Used when loading from Kubernetes secret or environment variable +func RegisterAdminKeyInMemory(keyStore *APIKeyStore, adminKey string, logger *logging.Logger) (*APIKey, error) { + if adminKey == "" { + return nil, fmt.Errorf("admin key value is empty") + } + + // Validate key format (should start with "nk_") + if !strings.HasPrefix(adminKey, "nk_") { + return nil, fmt.Errorf("invalid admin key format: must start with 'nk_'") + } + + // Create APIKey object with the provided key + adminRoles := []string{"admin", "notify-email", "notify-slack", "notify-ntfy"} + now := time.Now().UTC() + apiKey := &APIKey{ + Key: adminKey, + ClientID: "admin-bootstrap", + Roles: adminRoles, + CreatedAt: now, + IsActive: true, + RateLimit: 0, // Unlimited + Name: fmt.Sprintf("admin-bootstrap-%d", now.Unix()), + } + + // Add to keystore + keyStore.mu.Lock() + defer keyStore.mu.Unlock() + + keyStore.keys[adminKey] = apiKey + keyStore.rateLimits[adminKey] = &RateLimiter{ + maxRequests: 0, // Unlimited + window: time.Minute, + resetTime: time.Now().Add(time.Minute), + count: 0, + } + + logger.Infof("Registered existing admin key from Kubernetes secret") + return apiKey, nil +} + +// BootstrapAdminKey creates an initial admin API key on first startup (with database persistence) +// This should be called once per deployment when using HybridKeyStore with database func BootstrapAdminKey(ctx context.Context, keyStore *HybridKeyStore, cfg *BootstrapConfig, logger *logging.Logger) (*APIKey, error) { if !cfg.Enabled { return nil, fmt.Errorf("bootstrap is disabled") @@ -42,7 +148,7 @@ func BootstrapAdminKey(ctx context.Context, keyStore *HybridKeyStore, cfg *Boots ctx, "admin-bootstrap", adminRoles, - 0, // Unlimited rate limit + 0, // Unlimited rate limit nil, // No expiration "system", ) @@ -69,13 +175,14 @@ func BootstrapAdminKey(ctx context.Context, keyStore *HybridKeyStore, cfg *Boots // Print to stdout if configured (DANGEROUS - only for interactive setup) if cfg.PrintToStdout { - fmt.Println("\n" + strings.Repeat("=", 60)) + separator := strings.Repeat("=", 60) + fmt.Println("\n" + separator) fmt.Println("NOTIFIER BOOTSTRAP: ADMIN KEY CREATED") - fmt.Println(strings.Repeat("=", 60)) + fmt.Println(separator) fmt.Printf("Key: %s\n", apiKey.Key) fmt.Println("\nSave this key in a secure location. You will not be able to see it again.") fmt.Println("Use this key to create additional API keys via the key management API.") - fmt.Println(strings.Repeat("=", 60) + "\n") + fmt.Println(separator + "\n") } logger.Infof("Bootstrap admin key created successfully") @@ -97,3 +204,113 @@ func LoadBootstrapKeyFromEnv(ctx context.Context, keyStore *HybridKeyStore, logg logger.Infof("Bootstrap key detected from environment variable") return nil } + +// getKubernetesNamespace reads the pod's namespace from the service account token +func getKubernetesNamespace() (string, error) { + const namespacePath = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" + data, err := os.ReadFile(namespacePath) + if err != nil { + return "", fmt.Errorf("failed to read namespace from service account: %w", err) + } + return strings.TrimSpace(string(data)), nil +} + +// LoadAdminKeyFromKubernetesSecret attempts to load an existing admin key from a Kubernetes secret +// Returns the key string if found, empty string if secret doesn't exist, or error on failure +func LoadAdminKeyFromKubernetesSecret(ctx context.Context, secretName, secretKey string, logger *logging.Logger) (string, error) { + // Try to create Kubernetes client (will fail gracefully if not in cluster) + config, err := rest.InClusterConfig() + if err != nil { + logger.Debugf("Not running in Kubernetes cluster or in-cluster config unavailable: %v", err) + return "", nil // Not in Kubernetes, return empty (not an error) + } + + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + logger.Warnf("Failed to create Kubernetes client: %v", err) + return "", nil // Failed to create client, but not a fatal error + } + + namespace, err := getKubernetesNamespace() + if err != nil { + logger.Warnf("Failed to determine pod namespace: %v", err) + return "", nil // Failed to get namespace, but not a fatal error + } + + // Try to get the secret + secret, err := clientset.CoreV1().Secrets(namespace).Get(ctx, secretName, metav1.GetOptions{}) + if err != nil { + // Secret doesn't exist or other error occurred + logger.Debugf("Admin key secret not found in namespace %s: %v", namespace, err) + return "", nil // Secret not found is not an error + } + + // Extract the key value from the secret + if secretValue, exists := secret.Data[secretKey]; exists { + logger.Infof("Found existing admin key in Kubernetes secret %s/%s", namespace, secretName) + return string(secretValue), nil + } + + logger.Warnf("Kubernetes secret %s/%s exists but key %q not found", namespace, secretName, secretKey) + return "", nil +} + +// CreateKubernetesSecret creates or updates a Kubernetes secret with the admin key +func CreateKubernetesSecret(ctx context.Context, secretName, secretKey, adminKey string, logger *logging.Logger) error { + // Try to create Kubernetes client (will fail gracefully if not in cluster) + config, err := rest.InClusterConfig() + if err != nil { + logger.Debugf("Not running in Kubernetes cluster, skipping secret creation: %v", err) + return nil // Not in Kubernetes, skip (not an error) + } + + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + logger.Warnf("Failed to create Kubernetes client, skipping secret creation: %v", err) + return nil // Failed to create client, but not a fatal error + } + + namespace, err := getKubernetesNamespace() + if err != nil { + logger.Warnf("Failed to determine pod namespace, skipping secret creation: %v", err) + return nil // Failed to get namespace, but not a fatal error + } + + // Create or update the secret + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + Labels: map[string]string{ + "app": "notifier", + }, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + secretKey: []byte(adminKey), + }, + } + + // Try to get existing secret first + existingSecret, err := clientset.CoreV1().Secrets(namespace).Get(ctx, secretName, metav1.GetOptions{}) + if err == nil { + // Secret exists, update it + secret.ResourceVersion = existingSecret.ResourceVersion + _, err = clientset.CoreV1().Secrets(namespace).Update(ctx, secret, metav1.UpdateOptions{}) + if err != nil { + logger.Warnf("Failed to update Kubernetes secret %s/%s: %v", namespace, secretName, err) + return nil // Log warning but don't fail + } + logger.Infof("Updated admin key in Kubernetes secret %s/%s", namespace, secretName) + } else { + // Secret doesn't exist, create it + _, err = clientset.CoreV1().Secrets(namespace).Create(ctx, secret, metav1.CreateOptions{}) + if err != nil { + logger.Warnf("Failed to create Kubernetes secret %s/%s: %v", namespace, secretName, err) + return nil // Log warning but don't fail + } + logger.Infof("Created Kubernetes secret %s/%s with admin key", namespace, secretName) + } + + return nil +} diff --git a/internal/auth/keystore_db.go b/internal/auth/keystore_db.go index afb77b8..065b26c 100644 --- a/internal/auth/keystore_db.go +++ b/internal/auth/keystore_db.go @@ -6,7 +6,8 @@ import ( "fmt" "time" - _ "github.com/lib/pq" + "github.com/lib/pq" + _ "github.com/lib/pq" // PostgreSQL driver ) // KeyStoreDB provides persistent storage for API keys using PostgreSQL @@ -37,9 +38,10 @@ func NewKeyStoreDB(dbURL string) (*KeyStoreDB, error) { return ks, nil } -// initializeSchema creates the necessary tables if they don't exist +// initializeSchema creates the necessary tables and indexes if they don't exist func (ks *KeyStoreDB) initializeSchema() error { - schema := ` + // Create tables + tableSchema := ` -- API Keys table CREATE TABLE IF NOT EXISTS api_keys ( id SERIAL PRIMARY KEY, @@ -53,11 +55,7 @@ func (ks *KeyStoreDB) initializeSchema() error { is_active BOOLEAN NOT NULL DEFAULT true, rate_limit INTEGER NOT NULL DEFAULT 0, created_by VARCHAR(255), - metadata JSONB DEFAULT '{}'::jsonb, - INDEX idx_key (key), - INDEX idx_client_id (client_id), - INDEX idx_active (is_active), - INDEX idx_expires (expires_at) + metadata JSONB DEFAULT '{}'::jsonb ); -- Audit log for key operations @@ -67,14 +65,29 @@ func (ks *KeyStoreDB) initializeSchema() error { action VARCHAR(50) NOT NULL, performed_by VARCHAR(255) NOT NULL, performed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - details JSONB DEFAULT '{}'::jsonb, - INDEX idx_key_id (key_id), - INDEX idx_performed_at (performed_at) + details JSONB DEFAULT '{}'::jsonb ); ` - _, err := ks.db.Exec(schema) - return err + if _, err := ks.db.Exec(tableSchema); err != nil { + return fmt.Errorf("failed to create tables: %w", err) + } + + // Create indexes separately (PostgreSQL syntax) + indexSchema := ` + CREATE INDEX IF NOT EXISTS idx_api_keys_key ON api_keys(key); + CREATE INDEX IF NOT EXISTS idx_api_keys_client_id ON api_keys(client_id); + CREATE INDEX IF NOT EXISTS idx_api_keys_active ON api_keys(is_active); + CREATE INDEX IF NOT EXISTS idx_api_keys_expires ON api_keys(expires_at); + CREATE INDEX IF NOT EXISTS idx_api_key_audit_log_key_id ON api_key_audit_log(key_id); + CREATE INDEX IF NOT EXISTS idx_api_key_audit_log_performed_at ON api_key_audit_log(performed_at); + ` + + if _, err := ks.db.Exec(indexSchema); err != nil { + return fmt.Errorf("failed to create indexes: %w", err) + } + + return nil } // SaveKey persists an API key to the database @@ -94,7 +107,7 @@ func (ks *KeyStoreDB) SaveKey(ctx context.Context, key *APIKey, createdBy string key.Key, key.Name, key.ClientID, - key.Roles, + pq.Array(key.Roles), // Convert Go slice to PostgreSQL array key.CreatedAt, key.LastUsedAt, key.ExpiresAt, diff --git a/internal/auth/keystore_hybrid.go b/internal/auth/keystore_hybrid.go index c377881..fe6c6f1 100644 --- a/internal/auth/keystore_hybrid.go +++ b/internal/auth/keystore_hybrid.go @@ -12,7 +12,7 @@ import ( // This ensures consistency: if DB write fails, cache is not updated type HybridKeyStore struct { cache *APIKeyStore // In-memory cache for fast lookups - db *KeyStoreDB // Database backend for persistence + db *KeyStoreDB // Database backend for persistence mu sync.RWMutex } @@ -128,34 +128,7 @@ func (h *HybridKeyStore) UpdateLastUsed(ctx context.Context, keyStr string) erro // CheckRateLimit checks if a key has exceeded its rate limit func (h *HybridKeyStore) CheckRateLimit(keyStr string) (bool, error) { - h.cache.mu.RLock() - defer h.cache.mu.RUnlock() - - limiter, exists := h.cache.rateLimits[keyStr] - if !exists { - return false, fmt.Errorf("rate limiter not found") - } - - // Check if we're under the rate limit - if limiter.maxRequests == 0 { - return true, nil // Unlimited - } - - limiter.mu.Lock() - defer limiter.mu.Unlock() - - now := time.Now() - if now.After(limiter.resetTime) { - limiter.resetTime = now.Add(limiter.window) - limiter.count = 0 - } - - if limiter.count >= limiter.maxRequests { - return false, nil - } - - limiter.count++ - return true, nil + return h.cache.CheckRateLimit(keyStr) } // GetAuditLog retrieves audit log for a key diff --git a/internal/config/config.go b/internal/config/config.go index 7ece8a6..27edae1 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -66,8 +66,24 @@ type HealthCheckConfig struct { // AuthConfig contains authentication and authorization configuration type AuthConfig struct { - Enabled bool `mapstructure:"enabled"` // Enable API key authentication - DefaultRateLimit int `mapstructure:"default_rate_limit"` // Default rate limit in requests/minute (0 = unlimited) + Enabled bool `mapstructure:"enabled"` // Enable API key authentication + DefaultRateLimit int `mapstructure:"default_rate_limit"` // Default rate limit in requests/minute (0 = unlimited) + Database DatabaseConfig `mapstructure:"database"` // Database configuration for persistent key storage + Bootstrap BootstrapConf `mapstructure:"bootstrap"` // Bootstrap admin key configuration +} + +// DatabaseConfig contains database connection configuration +type DatabaseConfig struct { + URL string `mapstructure:"url"` // Database connection URL (e.g., "postgresql://user:pass@host:5432/db") +} + +// BootstrapConf contains configuration for bootstrap admin key creation +type BootstrapConf struct { + Enabled bool `mapstructure:"enabled"` // Enable bootstrap on startup + AdminKeyFileName string `mapstructure:"admin_key_file"` // File to save the generated admin key + PrintToStdout bool `mapstructure:"print_to_stdout"` // Print admin key to stdout (only for setup) + KubernetesSecretName string `mapstructure:"kubernetes_secret_name"` // Kubernetes secret name (e.g., "notifier-admin-key") + KubernetesSecretKey string `mapstructure:"kubernetes_secret_key"` // Key within secret (e.g., "admin-key") } // CORSConfig contains CORS (Cross-Origin Resource Sharing) configuration @@ -199,8 +215,13 @@ func setDefaults(v *viper.Viper) { v.SetDefault("health_check.interval", 30) // Auth defaults - v.SetDefault("auth.enabled", false) // Authentication disabled by default - v.SetDefault("auth.default_rate_limit", 100) // 100 requests per minute default + v.SetDefault("auth.enabled", false) // Authentication disabled by default + v.SetDefault("auth.default_rate_limit", 100) // 100 requests per minute default + v.SetDefault("auth.bootstrap.enabled", false) // Bootstrap disabled by default + v.SetDefault("auth.bootstrap.admin_key_file", "") // No file by default + v.SetDefault("auth.bootstrap.print_to_stdout", false) // Don't print to stdout by default + v.SetDefault("auth.bootstrap.kubernetes_secret_name", "notifier-admin-key") // Default secret name + v.SetDefault("auth.bootstrap.kubernetes_secret_key", "admin-key") // Default secret key // CORS defaults - secure by default (no origins allowed) v.SetDefault("cors.allowed_origins", []string{}) // Empty by default - must be explicitly configured @@ -393,6 +414,27 @@ func (c *Config) Sanitize() map[string]interface{} { } sanitized["notifiers"] = notifiers + + // Sanitize auth config + sanitized["auth"] = map[string]interface{}{ + "enabled": c.Auth.Enabled, + "bootstrap": map[string]interface{}{ + "enabled": c.Auth.Bootstrap.Enabled, + "admin_key_file": c.Auth.Bootstrap.AdminKeyFileName, + "print_to_stdout": c.Auth.Bootstrap.PrintToStdout, + "kubernetes_secret_name": c.Auth.Bootstrap.KubernetesSecretName, + "kubernetes_secret_key": c.Auth.Bootstrap.KubernetesSecretKey, + }, + } + + // Sanitize retention config + sanitized["retention"] = map[string]interface{}{ + "enabled": c.Retention.Enabled, + "ttl": c.Retention.TTL, + "check_frequency": c.Retention.CheckFrequency, + "max_size": c.Retention.MaxSize, + } + return sanitized } diff --git a/k8s/configmap.yaml b/k8s/configmap.yaml index 1a2598f..8a864ed 100644 --- a/k8s/configmap.yaml +++ b/k8s/configmap.yaml @@ -95,3 +95,23 @@ data: - "Authorization" allow_credentials: false max_age: 3600 + + # Authentication and authorization configuration + # Uncomment and configure to enable API key authentication + # auth: + # enabled: true + # default_rate_limit: 100 # requests per minute + # bootstrap: + # enabled: true + # kubernetes_secret_name: "notifier-admin-key" # Secret name for storing admin key + # kubernetes_secret_key: "admin-key" # Key within the secret + # # Optionally save to file as backup + # admin_key_file: "/tmp/notifier-admin-key" + # print_to_stdout: false + + # Notification retention and automatic cleanup configuration + retention: + enabled: true # Enable automatic cleanup of old/expired notifications + ttl: "168h" # Time-to-live: how long to keep notifications (7 days) + check_frequency: "1h" # How often to run cleanup check + max_size: 100000 # Maximum number of notifications to store diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml index 3dc534e..e80241e 100644 --- a/k8s/deployment.yaml +++ b/k8s/deployment.yaml @@ -95,3 +95,108 @@ metadata: name: notifier labels: app: notifier +--- +# Example deployment with API key authentication and Kubernetes bootstrap enabled +# Uncomment and apply this deployment instead of the one above to enable auth +# +# apiVersion: apps/v1 +# kind: Deployment +# metadata: +# name: notifier-with-auth +# labels: +# app: notifier +# version: v1 +# spec: +# replicas: 3 +# selector: +# matchLabels: +# app: notifier +# template: +# metadata: +# labels: +# app: notifier +# version: v1 +# spec: +# serviceAccountName: notifier +# containers: +# - name: notifier +# image: notifier:latest +# imagePullPolicy: Always +# ports: +# - name: rest +# containerPort: 8080 +# protocol: TCP +# - name: grpc +# containerPort: 50051 +# protocol: TCP +# - name: metrics +# containerPort: 9090 +# protocol: TCP +# - name: health +# containerPort: 8081 +# protocol: TCP +# env: +# - name: NOTIFIER_SERVER_MODE +# value: "both" +# - name: NOTIFIER_LOGGING_LEVEL +# value: "info" +# - name: NOTIFIER_LOGGING_FORMAT +# value: "json" +# - name: NOTIFIER_QUEUE_TYPE +# value: "local" +# # Optional: set auth config via environment variables instead of config.yaml +# # - name: NOTIFIER_AUTH_ENABLED +# # value: "true" +# # - name: NOTIFIER_AUTH_DEFAULT_RATE_LIMIT +# # value: "100" +# # - name: NOTIFIER_AUTH_BOOTSTRAP_ENABLED +# # value: "true" +# # - name: NOTIFIER_AUTH_BOOTSTRAP_KUBERNETES_SECRET_NAME +# # value: "notifier-admin-key" +# # - name: NOTIFIER_AUTH_BOOTSTRAP_KUBERNETES_SECRET_KEY +# # value: "admin-key" +# volumeMounts: +# - name: config +# mountPath: /app/config.yaml +# subPath: config.yaml +# readOnly: true +# - name: queue-storage +# mountPath: /var/lib/notifier +# resources: +# requests: +# cpu: 100m +# memory: 128Mi +# limits: +# cpu: 500m +# memory: 512Mi +# livenessProbe: +# httpGet: +# path: /health +# port: health +# initialDelaySeconds: 30 +# periodSeconds: 10 +# timeoutSeconds: 5 +# failureThreshold: 3 +# readinessProbe: +# httpGet: +# path: /health +# port: health +# initialDelaySeconds: 10 +# periodSeconds: 5 +# timeoutSeconds: 3 +# failureThreshold: 3 +# securityContext: +# runAsNonRoot: true +# runAsUser: 1000 +# allowPrivilegeEscalation: false +# readOnlyRootFilesystem: false +# capabilities: +# drop: +# - ALL +# volumes: +# - name: config +# configMap: +# name: notifier-config +# - name: queue-storage +# emptyDir: {} +# restartPolicy: Always diff --git a/k8s/rbac.yaml b/k8s/rbac.yaml new file mode 100644 index 0000000..e1c7c4e --- /dev/null +++ b/k8s/rbac.yaml @@ -0,0 +1,31 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: notifier + labels: + app: notifier +rules: +# Permissions for bootstrap admin key creation in Kubernetes secret +- apiGroups: [""] + resources: ["secrets"] + verbs: ["create", "update", "get", "list"] + resourceNames: ["notifier-admin-key"] +# Allow listing secrets to check if secret exists +- apiGroups: [""] + resources: ["secrets"] + verbs: ["list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: notifier + labels: + app: notifier +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: notifier +subjects: +- kind: ServiceAccount + name: notifier + namespace: default