diff --git a/README.md b/README.md index 94d5574..7a50cfb 100644 --- a/README.md +++ b/README.md @@ -115,26 +115,28 @@ go build ./cc-metric-store ``` -And finally, use the API to fetch some data: +And finally, use the API to fetch some data. The API is protected by JWT based authentication if `jwt-public-key` is set in `config.json`. You can use this JWT for testing: `eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlcyI6WyJST0xFX0FETUlOIiwiUk9MRV9BTkFMWVNUIiwiUk9MRV9VU0VSIl19.d-3_3FZTsadPjDEdsWrrQ7nS0edMAR4zjl-eK7rJU3HziNBfI9PDHDIpJVHTNN5E5SlLGLFXctWyKAkwhXL-Dw` ```sh +JWT="eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlcyI6WyJST0xFX0FETUlOIiwiUk9MRV9BTkFMWVNUIiwiUk9MRV9VU0VSIl19.d-3_3FZTsadPjDEdsWrrQ7nS0edMAR4zjl-eK7rJU3HziNBfI9PDHDIpJVHTNN5E5SlLGLFXctWyKAkwhXL-Dw" + # If the collector and store and nats-server have been running for at least 60 seconds on the same host, you may run: -curl -D - "http://localhost:8080/api/$(expr $(date +%s) - 60)/$(date +%s)/timeseries" -d "{ \"selectors\": [[\"testcluster\", \"$(hostname)\"]], \"metrics\": [\"load_one\"] }" +curl -H "Authorization: Bearer $JWT" -D - "http://localhost:8080/api/$(expr $(date +%s) - 60)/$(date +%s)/timeseries" -d "{ \"selectors\": [[\"testcluster\", \"$(hostname)\"]], \"metrics\": [\"load_one\"] }" # Get flops_any for all CPUs: -curl -D - "http://localhost:8080/api/$(expr $(date +%s) - 60)/$(date +%s)/timeseries" -d "{ \"selectors\": [[\"testcluster\", \"$(hostname)\"]], \"metrics\": [\"flops_any\"] }" +curl -H "Authorization: Bearer $JWT" -D - "http://localhost:8080/api/$(expr $(date +%s) - 60)/$(date +%s)/timeseries" -d "{ \"selectors\": [[\"testcluster\", \"$(hostname)\"]], \"metrics\": [\"flops_any\"] }" # Get flops_any for CPU 0: -curl -D - "http://localhost:8080/api/$(expr $(date +%s) - 60)/$(date +%s)/timeseries" -d "{ \"selectors\": [[\"testcluster\", \"$(hostname)\", \"cpu0\"]], \"metrics\": [\"flops_any\"] }" +curl -H "Authorization: Bearer $JWT" -D - "http://localhost:8080/api/$(expr $(date +%s) - 60)/$(date +%s)/timeseries" -d "{ \"selectors\": [[\"testcluster\", \"$(hostname)\", \"cpu0\"]], \"metrics\": [\"flops_any\"] }" # Get flops_any for CPU 0, 1, 2 and 3: -curl -D - "http://localhost:8080/api/$(expr $(date +%s) - 60)/$(date +%s)/timeseries" -d "{ \"selectors\": [[\"testcluster\", \"$(hostname)\", [\"cpu0\", \"cpu1\", \"cpu2\", \"cpu3\"]]], \"metrics\": [\"flops_any\"] }" +curl -H "Authorization: Bearer $JWT" -D - "http://localhost:8080/api/$(expr $(date +%s) - 60)/$(date +%s)/timeseries" -d "{ \"selectors\": [[\"testcluster\", \"$(hostname)\", [\"cpu0\", \"cpu1\", \"cpu2\", \"cpu3\"]]], \"metrics\": [\"flops_any\"] }" # Stats for load_one and proc_run: -curl -D - "http://localhost:8080/api/$(expr $(date +%s) - 60)/$(date +%s)/stats" -d "{ \"selectors\": [[\"testcluster\", \"$(hostname)\"]], \"metrics\": [\"load_one\", \"proc_run\"] }" +curl -H "Authorization: Bearer $JWT" -D - "http://localhost:8080/api/$(expr $(date +%s) - 60)/$(date +%s)/stats" -d "{ \"selectors\": [[\"testcluster\", \"$(hostname)\"]], \"metrics\": [\"load_one\", \"proc_run\"] }" # Stats for *all* CPUs aggregated both from CPU to node and over time: -curl -D - "http://localhost:8080/api/$(expr $(date +%s) - 60)/$(date +%s)/stats" -d "{ \"selectors\": [[\"testcluster\", \"$(hostname)\"]], \"metrics\": [\"flops_sp\", \"flops_dp\"] }" +curl -H "Authorization: Bearer $JWT" -D - "http://localhost:8080/api/$(expr $(date +%s) - 60)/$(date +%s)/stats" -d "{ \"selectors\": [[\"testcluster\", \"$(hostname)\"]], \"metrics\": [\"flops_sp\", \"flops_dp\"] }" # ... diff --git a/api.go b/api.go index 69267d6..ba6cf70 100644 --- a/api.go +++ b/api.go @@ -2,13 +2,18 @@ package main import ( "context" + "crypto/ed25519" + "encoding/base64" "encoding/json" + "errors" "fmt" "log" "net/http" "strconv" + "strings" "time" + "github.com/golang-jwt/jwt/v4" "github.com/gorilla/mux" ) @@ -203,6 +208,35 @@ func handlePeek(rw http.ResponseWriter, r *http.Request) { } } +func authentication(next http.Handler, publicKey ed25519.PublicKey) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + authheader := r.Header.Get("Authorization") + if authheader == "" || !strings.HasPrefix(authheader, "Bearer ") { + http.Error(rw, "Use JWT Authentication", http.StatusUnauthorized) + return + } + + // The actual token is ignored for now. + // In case expiration and so on are specified, the Parse function + // already returns an error for expired tokens. + _, err := jwt.Parse(authheader[len("Bearer "):], func(t *jwt.Token) (interface{}, error) { + if t.Method != jwt.SigningMethodEdDSA { + return nil, errors.New("only Ed25519/EdDSA supported") + } + + return publicKey, nil + }) + + if err != nil { + http.Error(rw, err.Error(), http.StatusUnauthorized) + return + } + + // Let request through... + next.ServeHTTP(rw, r) + }) +} + func StartApiServer(address string, ctx context.Context) error { r := mux.NewRouter() @@ -218,6 +252,15 @@ func StartApiServer(address string, ctx context.Context) error { ReadTimeout: 15 * time.Second, } + if len(conf.JwtPublicKey) > 0 { + buf, err := base64.StdEncoding.DecodeString(conf.JwtPublicKey) + if err != nil { + return err + } + publicKey := ed25519.PublicKey(buf) + server.Handler = authentication(server.Handler, publicKey) + } + go func() { log.Printf("API http endpoint listening on '%s'\n", address) err := server.ListenAndServe() @@ -227,7 +270,7 @@ func StartApiServer(address string, ctx context.Context) error { }() for { - _ = <-ctx.Done() + <-ctx.Done() err := server.Shutdown(context.Background()) log.Println("API server shut down") return err diff --git a/config.json b/config.json index db3f01f..d9cab72 100644 --- a/config.json +++ b/config.json @@ -23,5 +23,6 @@ "directory": "./var/archive" }, "retention-in-memory": 86400, - "nats": "nats://localhost:4222" + "nats": "nats://localhost:4222", + "jwt-public-key": "kzfYrYy+TzpanWZHJ5qSdMj5uKUWgq74BWhQG6copP0=" } diff --git a/go.mod b/go.mod index 9339fad..0c62c1d 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/ClusterCockpit/cc-metric-store go 1.16 require ( + github.com/golang-jwt/jwt/v4 v4.0.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/gorilla/mux v1.8.0 github.com/nats-io/nats-server/v2 v2.2.6 // indirect diff --git a/go.sum b/go.sum index 18f2209..bcfc0ce 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v4 v4.0.0 h1:RAqyYixv1p7uEnocuy8P1nru5wprCh/MH2BIlW5z5/o= +github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= diff --git a/metric-store.go b/metric-store.go index f06538e..5c7f806 100644 --- a/metric-store.go +++ b/metric-store.go @@ -22,6 +22,7 @@ type Config struct { Metrics map[string]MetricConfig `json:"metrics"` RetentionInMemory int `json:"retention-in-memory"` Nats string `json:"nats"` + JwtPublicKey string `json:"jwt-public-key"` Checkpoints struct { Interval int `json:"interval"` RootDir string `json:"directory"` @@ -40,10 +41,10 @@ var lastCheckpoint time.Time func loadConfiguration(file string) Config { var config Config configFile, err := os.Open(file) - defer configFile.Close() if err != nil { fmt.Println(err.Error()) } + defer configFile.Close() jsonParser := json.NewDecoder(configFile) jsonParser.Decode(&config) return config @@ -172,7 +173,7 @@ func main() { sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) go func() { - _ = <-sigs + <-sigs log.Println("Shuting down...") shutdown() }()