mirror of
https://github.com/ClusterCockpit/cc-metric-store.git
synced 2025-07-23 21:31:41 +02:00
Change JWT Auth to Middleware Handler
Caveat: Cache is per endpoint
This commit is contained in:
@@ -137,10 +137,6 @@ func (data *ApiMetricData) PadDataWithNull(ms *memorystore.MemoryStore, from, to
|
||||
// @security ApiKeyAuth
|
||||
// @router /free/ [get]
|
||||
func handleFree(rw http.ResponseWriter, r *http.Request) {
|
||||
if err := isAuthenticated(r); err != nil {
|
||||
handleError(err, http.StatusUnauthorized, rw)
|
||||
return
|
||||
}
|
||||
rawTo := r.URL.Query().Get("to")
|
||||
if rawTo == "" {
|
||||
handleError(errors.New("'to' is a required query parameter"), http.StatusBadRequest, rw)
|
||||
@@ -200,10 +196,6 @@ func handleFree(rw http.ResponseWriter, r *http.Request) {
|
||||
// @security ApiKeyAuth
|
||||
// @router /write/ [post]
|
||||
func handleWrite(rw http.ResponseWriter, r *http.Request) {
|
||||
if err := isAuthenticated(r); err != nil {
|
||||
handleError(err, http.StatusUnauthorized, rw)
|
||||
return
|
||||
}
|
||||
bytes, err := io.ReadAll(r.Body)
|
||||
rw.Header().Add("Content-Type", "application/json")
|
||||
if err != nil {
|
||||
@@ -263,10 +255,6 @@ type ApiQuery struct {
|
||||
// @security ApiKeyAuth
|
||||
// @router /query/ [get]
|
||||
func handleQuery(rw http.ResponseWriter, r *http.Request) {
|
||||
if err := isAuthenticated(r); err != nil {
|
||||
handleError(err, http.StatusUnauthorized, rw)
|
||||
return
|
||||
}
|
||||
var err error
|
||||
req := ApiQueryRequest{WithStats: true, WithData: true, WithPadding: true}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
@@ -385,10 +373,10 @@ func handleQuery(rw http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// handleDebug godoc
|
||||
// @summary Debug endpoint
|
||||
// @tags write
|
||||
// @tags debug
|
||||
// @description Write metrics to store
|
||||
// @produce json
|
||||
// @param selector query string false "Job Cluster"
|
||||
// @param selector query string false "Selector"
|
||||
// @success 200 {string} string "Debug dump"
|
||||
// @failure 400 {object} api.ErrorResponse "Bad Request"
|
||||
// @failure 401 {object} api.ErrorResponse "Unauthorized"
|
||||
@@ -397,10 +385,6 @@ func handleQuery(rw http.ResponseWriter, r *http.Request) {
|
||||
// @security ApiKeyAuth
|
||||
// @router /debug/ [post]
|
||||
func handleDebug(rw http.ResponseWriter, r *http.Request) {
|
||||
if err := isAuthenticated(r); err != nil {
|
||||
handleError(err, http.StatusUnauthorized, rw)
|
||||
return
|
||||
}
|
||||
raw := r.URL.Query().Get("selector")
|
||||
rw.Header().Add("Content-Type", "application/json")
|
||||
selector := []string{}
|
||||
|
@@ -10,43 +10,47 @@ import (
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
var publicKey ed25519.PublicKey
|
||||
|
||||
func isAuthenticated(r *http.Request) error {
|
||||
func authHandler(next http.Handler, publicKey ed25519.PublicKey) http.Handler {
|
||||
cacheLock := sync.RWMutex{}
|
||||
cache := map[string]*jwt.Token{}
|
||||
|
||||
authheader := r.Header.Get("Authorization")
|
||||
if authheader == "" || !strings.HasPrefix(authheader, "Bearer ") {
|
||||
return errors.New("Use JWT Authentication")
|
||||
}
|
||||
|
||||
rawtoken := authheader[len("Bearer "):]
|
||||
cacheLock.RLock()
|
||||
token, ok := cache[rawtoken]
|
||||
cacheLock.RUnlock()
|
||||
if ok && token.Claims.Valid() == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
var err error
|
||||
token, err = jwt.Parse(rawtoken, func(t *jwt.Token) (interface{}, error) {
|
||||
if t.Method != jwt.SigningMethodEdDSA {
|
||||
return nil, errors.New("only Ed25519/EdDSA supported")
|
||||
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
|
||||
}
|
||||
|
||||
return publicKey, nil
|
||||
rawtoken := authheader[len("Bearer "):]
|
||||
cacheLock.RLock()
|
||||
token, ok := cache[rawtoken]
|
||||
cacheLock.RUnlock()
|
||||
if ok && token.Claims.Valid() == nil {
|
||||
next.ServeHTTP(rw, r)
|
||||
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.
|
||||
var err error
|
||||
token, err = jwt.Parse(rawtoken, 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
|
||||
}
|
||||
|
||||
cacheLock.Lock()
|
||||
cache[rawtoken] = token
|
||||
cacheLock.Unlock()
|
||||
|
||||
// Let request through...
|
||||
next.ServeHTTP(rw, r)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cacheLock.Lock()
|
||||
cache[rawtoken] = token
|
||||
cacheLock.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@@ -23,30 +23,32 @@ const docTemplate = `{
|
||||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"paths": {
|
||||
"/clusters/": {
|
||||
"get": {
|
||||
"/debug/": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Write metrics to store",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"free"
|
||||
"debug"
|
||||
],
|
||||
"summary": "Debug endpoint",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "up to timestamp",
|
||||
"name": "to",
|
||||
"description": "Selector",
|
||||
"name": "selector",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "ok",
|
||||
"description": "Debug dump",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -78,32 +80,30 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/debug/": {
|
||||
"post": {
|
||||
"/free/": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Write metrics to store",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"write"
|
||||
"free"
|
||||
],
|
||||
"summary": "Debug endpoint",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Job Cluster",
|
||||
"name": "selector",
|
||||
"description": "up to timestamp",
|
||||
"name": "to",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Debug dump",
|
||||
"description": "ok",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
|
@@ -19,11 +19,15 @@ func MountRoutes(r *http.ServeMux) {
|
||||
if err != nil {
|
||||
log.Fatalf("starting server failed: %v", err)
|
||||
}
|
||||
publicKey = ed25519.PublicKey(buf)
|
||||
publicKey := ed25519.PublicKey(buf)
|
||||
r.Handle("POST /api/free/", authHandler(http.HandlerFunc(handleFree), publicKey))
|
||||
r.Handle("POST /api/write/", authHandler(http.HandlerFunc(handleWrite), publicKey))
|
||||
r.Handle("GET /api/query/", authHandler(http.HandlerFunc(handleQuery), publicKey))
|
||||
r.Handle("GET /api/debug/", authHandler(http.HandlerFunc(handleDebug), publicKey))
|
||||
} else {
|
||||
r.HandleFunc("POST /api/free/", handleFree)
|
||||
r.HandleFunc("POST /api/write/", handleWrite)
|
||||
r.HandleFunc("GET /api/query/", handleQuery)
|
||||
r.HandleFunc("GET /api/debug/", handleDebug)
|
||||
}
|
||||
|
||||
r.HandleFunc("POST /api/free/", handleFree)
|
||||
r.HandleFunc("POST /api/write/", handleWrite)
|
||||
r.HandleFunc("GET /api/query/", handleQuery)
|
||||
r.HandleFunc("GET /api/debug/", handleDebug)
|
||||
}
|
||||
|
Reference in New Issue
Block a user