Use JWT authentication for the API

This commit takes care of the API part of issue #6.
This commit is contained in:
Lou Knauer 2021-09-20 09:27:31 +02:00
parent 22de7da5e4
commit 27a5c0b561
6 changed files with 63 additions and 11 deletions

View File

@ -115,26 +115,28 @@ go build
./cc-metric-store ./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 ```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: # 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: # 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: # 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: # 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: # 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: # 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\"] }"
# ... # ...

45
api.go
View File

@ -2,13 +2,18 @@ package main
import ( import (
"context" "context"
"crypto/ed25519"
"encoding/base64"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/golang-jwt/jwt/v4"
"github.com/gorilla/mux" "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 { func StartApiServer(address string, ctx context.Context) error {
r := mux.NewRouter() r := mux.NewRouter()
@ -218,6 +252,15 @@ func StartApiServer(address string, ctx context.Context) error {
ReadTimeout: 15 * time.Second, 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() { go func() {
log.Printf("API http endpoint listening on '%s'\n", address) log.Printf("API http endpoint listening on '%s'\n", address)
err := server.ListenAndServe() err := server.ListenAndServe()
@ -227,7 +270,7 @@ func StartApiServer(address string, ctx context.Context) error {
}() }()
for { for {
_ = <-ctx.Done() <-ctx.Done()
err := server.Shutdown(context.Background()) err := server.Shutdown(context.Background())
log.Println("API server shut down") log.Println("API server shut down")
return err return err

View File

@ -23,5 +23,6 @@
"directory": "./var/archive" "directory": "./var/archive"
}, },
"retention-in-memory": 86400, "retention-in-memory": 86400,
"nats": "nats://localhost:4222" "nats": "nats://localhost:4222",
"jwt-public-key": "kzfYrYy+TzpanWZHJ5qSdMj5uKUWgq74BWhQG6copP0="
} }

1
go.mod
View File

@ -3,6 +3,7 @@ module github.com/ClusterCockpit/cc-metric-store
go 1.16 go 1.16
require ( require (
github.com/golang-jwt/jwt/v4 v4.0.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.0
github.com/nats-io/nats-server/v2 v2.2.6 // indirect github.com/nats-io/nats-server/v2 v2.2.6 // indirect

4
go.sum
View File

@ -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/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.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=

View File

@ -22,6 +22,7 @@ type Config struct {
Metrics map[string]MetricConfig `json:"metrics"` Metrics map[string]MetricConfig `json:"metrics"`
RetentionInMemory int `json:"retention-in-memory"` RetentionInMemory int `json:"retention-in-memory"`
Nats string `json:"nats"` Nats string `json:"nats"`
JwtPublicKey string `json:"jwt-public-key"`
Checkpoints struct { Checkpoints struct {
Interval int `json:"interval"` Interval int `json:"interval"`
RootDir string `json:"directory"` RootDir string `json:"directory"`
@ -40,10 +41,10 @@ var lastCheckpoint time.Time
func loadConfiguration(file string) Config { func loadConfiguration(file string) Config {
var config Config var config Config
configFile, err := os.Open(file) configFile, err := os.Open(file)
defer configFile.Close()
if err != nil { if err != nil {
fmt.Println(err.Error()) fmt.Println(err.Error())
} }
defer configFile.Close()
jsonParser := json.NewDecoder(configFile) jsonParser := json.NewDecoder(configFile)
jsonParser.Decode(&config) jsonParser.Decode(&config)
return config return config
@ -172,7 +173,7 @@ func main() {
sigs := make(chan os.Signal, 1) sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() { go func() {
_ = <-sigs <-sigs
log.Println("Shuting down...") log.Println("Shuting down...")
shutdown() shutdown()
}() }()