mirror of
				https://github.com/ClusterCockpit/cc-metric-store.git
				synced 2025-11-04 02:35:08 +01:00 
			
		
		
		
	Use JWT authentication for the API
This commit takes care of the API part of issue #6.
This commit is contained in:
		
							
								
								
									
										16
									
								
								README.md
									
									
									
									
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										16
									
								
								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\"] }"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# ...
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										45
									
								
								api.go
									
									
									
									
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										45
									
								
								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
 | 
			
		||||
 
 | 
			
		||||
@@ -23,5 +23,6 @@
 | 
			
		||||
        "directory": "./var/archive"
 | 
			
		||||
    },
 | 
			
		||||
    "retention-in-memory": 86400,
 | 
			
		||||
    "nats": "nats://localhost:4222"
 | 
			
		||||
    "nats": "nats://localhost:4222",
 | 
			
		||||
    "jwt-public-key": "kzfYrYy+TzpanWZHJ5qSdMj5uKUWgq74BWhQG6copP0="
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								go.mod
									
									
									
									
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										1
									
								
								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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								go.sum
									
									
									
									
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										4
									
								
								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=
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
	}()
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user