Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Put prometheus metrics behind federation/origin token #176

Merged
merged 3 commits into from
Oct 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ func DiscoverFederation() error {
viper.Set("NamespaceURL", metadata.NamespaceRegistrationEndpoint)
}

viper.Set("FederationURI", metadata.JwksUri)

return nil
}

Expand Down
2 changes: 1 addition & 1 deletion director/redirect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ func TestDirectorRegistration(t *testing.T) {
rInv.POST("/", RegisterOrigin)
cInv.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBuffer([]byte(`{"Namespaces": [{"Path": "/foo/bar", "URL": "https://get-your-tokens.org"}]}`)))

cInv.Request.Header.Set("Authorization", string(signedInv))
cInv.Request.Header.Set("Authorization", "Bearer "+string(signedInv))
cInv.Request.Header.Set("Content-Type", "application/json")

rInv.ServeHTTP(wInv, cInv.Request)
Expand Down
118 changes: 117 additions & 1 deletion web_ui/prometheus.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ package web_ui

import (
"context"
"crypto/ecdsa"
"errors"
"fmt"
"io"
"math"
"net/http"
"net/url"
Expand All @@ -35,6 +37,9 @@ import (
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/grafana/regexp"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/mwitkow/go-conntrack"
"github.com/oklog/run"
pelican_config "github.com/pelicanplatform/pelican/config"
Expand Down Expand Up @@ -138,6 +143,117 @@ func runtimeInfo() (api_v1.RuntimeInfo, error) {
return api_v1.RuntimeInfo{}, nil
}

func checkPromToken(av1 *route.Router) gin.HandlerFunc {
/* A function which wraps around the av1 router to force a jwk token check using
* the origin's private key. It will check the request's URL and Header for a token
* and if found it will then attempt to validate the token. If valid, it will continue
* the routing as normal, otherwise it will return an error"
*/
return func(c *gin.Context) {
req := c.Request

var strToken string
var token jwt.Token
var err error
if authzQuery := req.URL.Query()["authz"]; len(authzQuery) > 0 {
strToken = authzQuery[0]
} else if authzHeader := req.Header["Authorization"]; len(authzHeader) > 0 {
strToken = strings.TrimPrefix(authzHeader[0], "Bearer ")
} else {
c.JSON(403, gin.H{"error": "Permission Denied: Missing token"})
return
}

// Parsing the token (unverified) in order to get its issuer without having the jwks
token, err = jwt.Parse([]byte(strToken), jwt.WithVerify(false))
turetske marked this conversation as resolved.
Show resolved Hide resolved

if err != nil {
c.JSON(400, gin.H{"error": "Failed to parse token"})
}

fedURL := viper.GetString("FederationURL")

var bKey *jwk.Key
if fedURL == token.Issuer() {
err := pelican_config.DiscoverFederation()
if err != nil {
c.JSON(400, gin.H{"error": "Failed to discover the federation information"})
}
fedURIFile := viper.GetString("FederationURI")
response, err := http.Get(fedURIFile)
if err != nil {
c.JSON(400, gin.H{"error": "Failed to get federation key file"})
}
defer response.Body.Close()
contents, err := io.ReadAll(response.Body)
if err != nil {
c.JSON(400, gin.H{"error": "Failed to read federation key file"})
return
}
keys, err := jwk.Parse(contents)
//key, err := jwk.ParseKey(contents, jwk.WithPEM(true))
if err != nil {
c.JSON(400, gin.H{"error": "Failed to parse Federation key file"})
return
}
key, ok := keys.Key(0)
if !ok {
c.JSON(400, gin.H{"error": "No key in keyset"})
}
bKey = &key
} else {
bKey, err = pelican_config.GetOriginJWK()
if err != nil {
c.JSON(400, gin.H{"error": "Failed to retrieve private key"})
return
}
}

var raw ecdsa.PrivateKey
if err = (*bKey).Raw(&raw); err != nil {
c.JSON(400, gin.H{"error": "Failed to extract signing key"})
return
}

parsed, err := jwt.Parse([]byte(strToken), jwt.WithKey(jwa.ES256, raw.PublicKey))

if err != nil {
c.JSON(403, gin.H{"error": "Permission Denied: Invalid token"})
return
}

/*
* The signature is verified, now we need to make sure this token actually gives us
* permission to access prometheus metrics
* NOTE: The validate function also handles checking `iat` and `exp` to make sure the token
* remains valid.
*/
scopeValidator := jwt.ValidatorFunc(func(_ context.Context, tok jwt.Token) jwt.ValidationError {
scope_any, present := tok.Get("scope")
if !present {
return jwt.NewValidationError(errors.New("No scope is present; required for authorization"))
}
scope, ok := scope_any.(string)
if !ok {
return jwt.NewValidationError(errors.New("scope claim in token is not string-valued"))
}

for _, scope := range strings.Split(scope, " ") {
if scope == "prometheus.read" {
return nil
}
}
return jwt.NewValidationError(errors.New("Token does not contain prometheus access authorization"))
})
if err = jwt.Validate(parsed, jwt.WithValidator(scopeValidator)); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "server could not validate the provided access token"})
return
}

av1.ServeHTTP(c.Writer, c.Request)
turetske marked this conversation as resolved.
Show resolved Hide resolved
}
}

func ConfigureEmbeddedPrometheus(engine *gin.Engine) error {

cfg := flagConfig{}
Expand Down Expand Up @@ -341,7 +457,7 @@ func ConfigureEmbeddedPrometheus(engine *gin.Engine) error {
//WithInstrumentation(setPathWithPrefix("/api/v1"))
apiV1.Register(av1)

engine.GET("/api/v1.0/prometheus/*any", gin.WrapH(av1))
engine.GET("/api/v1.0/prometheus/*any", checkPromToken(av1))

reloaders := []reloader{
{
Expand Down
Loading
Loading