diff options
| author | Fuwn <[email protected]> | 2026-01-20 16:15:50 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-01-20 16:15:50 -0800 |
| commit | b147654b8aed7005760017f604aae58637ac7767 (patch) | |
| tree | 77cad60fc8eff8bd6c18a9846307e02bf1d6ba32 | |
| parent | feat: Add all remaining monitor options to group defaults (diff) | |
| download | kaze-b147654b8aed7005760017f604aae58637ac7767.tar.xz kaze-b147654b8aed7005760017f604aae58637ac7767.zip | |
feat: Add API access control (public/private/authenticated)
Add configurable access control for /api/* endpoints:
- public: Anyone can access (default, backwards compatible)
- private: API endpoints return 403 Forbidden
- authenticated: Requires API key via X-API-Key header or ?api_key= param
Config example:
api:
access: authenticated
keys:
- "secret-key-1"
- "secret-key-2"
| -rw-r--r-- | config.example.yaml | 26 | ||||
| -rw-r--r-- | internal/config/config.go | 29 | ||||
| -rw-r--r-- | internal/server/server.go | 51 |
3 files changed, 103 insertions, 3 deletions
diff --git a/config.example.yaml b/config.example.yaml index 93236df..228aecd 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -23,6 +23,18 @@ server: host: "0.0.0.0" port: 8080 +# API settings +api: + # Access control for /api/* endpoints: + # "public" - Anyone can access (default) + # "private" - API endpoints return 403 Forbidden + # "authenticated" - Requires API key via X-API-Key header or ?api_key= query param + access: public + # API keys (only used when access is "authenticated") + # keys: + # - "your-secret-api-key-1" + # - "your-secret-api-key-2" + # Storage settings storage: path: "./kaze.db" # For Docker single-volume: use "./data/kaze.db" to store in mounted volume @@ -340,3 +352,17 @@ incidents: # identified - Root cause found # monitoring - Fix applied, monitoring # resolved - Issue resolved +# +# API access control: +# api: +# access: string - "public" (default), "private", or "authenticated" +# keys: []string - List of valid API keys (for "authenticated" mode) +# +# Endpoints: +# GET /api/status - All monitors status JSON +# GET /api/monitor/{name} - Single monitor status JSON +# GET /api/history/{name} - Monitor history (supports ?mode=ping|minute|hour|day&count=N) +# +# Authentication (when access is "authenticated"): +# - Header: X-API-Key: your-secret-key +# - Query param: ?api_key=your-secret-key diff --git a/internal/config/config.go b/internal/config/config.go index d82fee0..5f05b96 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -15,10 +15,22 @@ type Config struct { Server ServerConfig `yaml:"server"` Storage StorageConfig `yaml:"storage"` Display DisplayConfig `yaml:"display"` + API APIConfig `yaml:"api"` Groups []GroupConfig `yaml:"groups"` Incidents []IncidentConfig `yaml:"incidents"` } +// APIConfig contains settings for the JSON API endpoints +type APIConfig struct { + // Access controls who can access /api/* endpoints: + // "public" - Anyone can access (default) + // "private" - API endpoints are disabled (return 403) + // "authenticated" - Requires API key via X-API-Key header or ?api_key= query param + Access string `yaml:"access"` + // Keys is a list of valid API keys (only used when access is "authenticated") + Keys []string `yaml:"keys"` +} + // DisplayConfig contains display/UI settings type DisplayConfig struct { // TickMode controls how history is aggregated: ping, minute, hour, day @@ -241,6 +253,11 @@ func (c *Config) applyDefaults() { c.Display.Scale = 2.0 } + // Apply API defaults + if c.API.Access == "" { + c.API.Access = "public" + } + // Apply group defaults for i := range c.Groups { grp := &c.Groups[i] @@ -456,6 +473,18 @@ func (c *Config) validate() error { return fmt.Errorf("tick_count must be between 1 and 200, got %d", c.Display.TickCount) } + // Validate API config + switch c.API.Access { + case "", "public", "private", "authenticated": + // Valid modes ("" defaults to "public") + default: + return fmt.Errorf("invalid api.access %q (must be public, private, or authenticated)", c.API.Access) + } + + if c.API.Access == "authenticated" && len(c.API.Keys) == 0 { + return fmt.Errorf("api.access is 'authenticated' but no api.keys provided") + } + return nil } diff --git a/internal/server/server.go b/internal/server/server.go index 92b0f09..b0192a4 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -65,9 +65,11 @@ func New(cfg *config.Config, store *storage.Storage, sched *monitor.Scheduler, l // Pages mux.HandleFunc("GET /", s.handleIndex) - mux.HandleFunc("GET /api/status", s.handleAPIStatus) - mux.HandleFunc("GET /api/monitor/{name}", s.handleAPIMonitor) - mux.HandleFunc("GET /api/history/{name}", s.handleAPIHistory) + + // API endpoints (protected by API access control) + mux.HandleFunc("GET /api/status", s.withAPIAuth(s.handleAPIStatus)) + mux.HandleFunc("GET /api/monitor/{name}", s.withAPIAuth(s.handleAPIMonitor)) + mux.HandleFunc("GET /api/history/{name}", s.withAPIAuth(s.handleAPIHistory)) // Create HTTP server s.server = &http.Server{ @@ -115,6 +117,49 @@ func (s *Server) withMiddleware(next http.Handler) http.Handler { }) } +// withAPIAuth wraps an API handler with access control based on config.API.Access +func (s *Server) withAPIAuth(handler http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + switch s.config.API.Access { + case "private": + // API is disabled + s.jsonError(w, "API access is disabled", http.StatusForbidden) + return + + case "authenticated": + // Check for API key in header or query param + apiKey := r.Header.Get("X-API-Key") + if apiKey == "" { + apiKey = r.URL.Query().Get("api_key") + } + + if apiKey == "" { + w.Header().Set("WWW-Authenticate", "API-Key") + s.jsonError(w, "API key required", http.StatusUnauthorized) + return + } + + // Check if key is valid + valid := false + for _, key := range s.config.API.Keys { + if key == apiKey { + valid = true + break + } + } + + if !valid { + s.jsonError(w, "Invalid API key", http.StatusUnauthorized) + return + } + + // case "public" or default: allow access + } + + handler(w, r) + } +} + // PageData contains data for rendering the status page type PageData struct { Site config.SiteConfig |