Skip to content

Commit

Permalink
Merge pull request PelicanPlatform#121 from jhiemstrawisc/origin-dire…
Browse files Browse the repository at this point in the history
…ctor-autodiscovery

Send token and namespace ad to director
  • Loading branch information
joereuss12 authored Sep 11, 2023
2 parents 8c07563 + 2364c81 commit 3bec835
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 52 deletions.
76 changes: 58 additions & 18 deletions director/origin_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ package director

import (
"context"
"errors"
"crypto/tls"
"encoding/json"
"net/http"
"net/url"
"path"
"strings"
Expand All @@ -32,6 +34,8 @@ import (
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/pelicanplatform/pelican/config"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
)

Expand All @@ -49,11 +53,10 @@ var (
)

func CreateAdvertiseToken(namespace string) (string, error) {
key, err := config.GetOriginJWK()
if err != nil {
return "", err
}
issuer_url, err := GetIssuerURL(namespace)
// TODO: Need to come back and carefully consider a few naming practices.
// Here, issuerUrl is actually the registry database url, and not
// the token issuer url for this namespace
issuerUrl, err := GetIssuerURL(namespace)
if err != nil {
return "", err
}
Expand All @@ -64,7 +67,7 @@ func CreateAdvertiseToken(namespace string) (string, error) {

tok, err := jwt.NewBuilder().
Claim("scope", "pelican.advertise").
Issuer(issuer_url).
Issuer(issuerUrl).
Audience([]string{director}).
Subject("origin").
Expiration(time.Now().Add(time.Minute)).
Expand All @@ -73,7 +76,20 @@ func CreateAdvertiseToken(namespace string) (string, error) {
return "", err
}

signed, err := jwt.Sign(tok, jwt.WithKey(jwa.ES512, key))
key, err := config.GetOriginJWK()
if err != nil {
return "", errors.Wrap(err, "failed to load the origin's JWK")
}

// Get/assign the kid, needed for verification of the token by the director
// TODO: Create more generic "tokenCreate" functions so we don't have to do
// this by hand all the time
err = jwk.AssignKeyID(*key)
if err != nil {
return "", errors.Wrap(err, "Failed to assign kid to the token")
}

signed, err := jwt.Sign(tok, jwt.WithKey(jwa.ES512, *key))
if err != nil {
return "", err
}
Expand All @@ -84,30 +100,54 @@ func CreateAdvertiseToken(namespace string) (string, error) {
// see if the entity is authorized to advertise an origin for the
// namespace
func VerifyAdvertiseToken(token, namespace string) (bool, error) {
issuer_url, err := GetIssuerURL(namespace)
issuerUrl, err := GetIssuerURL(namespace)
if err != nil {
return false, err
}
var ar *jwk.Cache
{

// defer statements are scoped to function, not lexical enclosure,
// which is why we wrap these defer statements in anon funcs
func() {
namespaceKeysMutex.RLock()
defer namespaceKeysMutex.Unlock()
defer namespaceKeysMutex.RUnlock()
item := namespaceKeys.Get(namespace)
if !item.IsExpired() {
ar = item.Value()
if item != nil {
if !item.IsExpired() {
ar = item.Value()
}
}
}
}()
ctx := context.Background()
if ar == nil {
ar := jwk.NewCache(ctx)
if err = ar.Register(issuer_url, jwk.WithMinRefreshInterval(15*time.Minute)); err != nil {
ar = jwk.NewCache(ctx)
// This should be switched to use the common transport, but that must first be exported
client := &http.Client{}
if viper.GetBool("TLSSkipVerify") {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client = &http.Client{Transport: tr}
}
if err = ar.Register(issuerUrl, jwk.WithMinRefreshInterval(15*time.Minute), jwk.WithHTTPClient(client)); err != nil {
return false, err
}
namespaceKeysMutex.Lock()
defer namespaceKeysMutex.Unlock()
namespaceKeys.Set(namespace, ar, ttlcache.DefaultTTL)
}
keyset, err := ar.Get(ctx, issuer_url)
log.Debugln("Attempting to fetch keys from ", issuerUrl)
keyset, err := ar.Get(ctx, issuerUrl)

if log.IsLevelEnabled(log.DebugLevel) {
// Let's check that we can convert to JSON and get the right thing...
jsonbuf, err := json.Marshal(keyset)
if err != nil {
return false, errors.Wrap(err, "failed to marshal the public keyset into JWKS JSON")
}
log.Debugln("Constructed JWKS from fetching jwks:", string(jsonbuf))
}

if err != nil {
return false, err
}
Expand Down Expand Up @@ -145,6 +185,6 @@ func GetIssuerURL(prefix string) (string, error) {
if err != nil {
return "", err
}
namespace_url.Path = path.Join(namespace_url.Path, "namespaces", prefix)
namespace_url.Path = path.Join(namespace_url.Path, "api", "v1.0", "registry", prefix, ".well-known", "issuer.jwks")
return namespace_url.String(), nil
}
4 changes: 3 additions & 1 deletion director/redirect.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,9 @@ func RegisterOrigin(ctx *gin.Context) {
}

for _, namespace := range ad.Namespaces {
ok, err := VerifyAdvertiseToken(tokens[0], namespace.Path)
// We're assuming there's only one token in the slice
token := strings.TrimPrefix(tokens[0], "Bearer ")
ok, err := VerifyAdvertiseToken(token, namespace.Path)
if err != nil {
log.Warningln("Failed to verify token:", err)
ctx.JSON(400, gin.H{"error": "Authorization token verification failed"})
Expand Down
2 changes: 1 addition & 1 deletion namespace-registry/registry-db.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func namespaceExists(prefix string) (bool, error) {
return found, nil
}

func getPrefixJwks(prefix string) (*jwk.Set, error) {
func dbGetPrefixJwks(prefix string) (*jwk.Set, error) {
jwksQuery := `SELECT pubkey FROM namespace WHERE prefix = ?`
var pubkeyStr string
err := db.QueryRow(jwksQuery, prefix).Scan(&pubkeyStr)
Expand Down
74 changes: 43 additions & 31 deletions namespace-registry/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"sync"

Expand Down Expand Up @@ -614,7 +615,7 @@ func dbDeleteNamespace(ctx *gin.Context) {
delTokenStr := strings.TrimPrefix(authHeader, "Bearer ")

// Have the token, now we need to load the JWKS for the prefix
originJwks, err := getPrefixJwks(prefix)
originJwks, err := dbGetPrefixJwks(prefix)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "server encountered an error loading the prefix's stored jwks"})
log.Errorf("Failed to get prefix's stored jwks: %v", err)
Expand Down Expand Up @@ -688,46 +689,57 @@ func dbGetAllNamespaces(ctx *gin.Context) {
nss, err := getAllNamespaces()
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "server encountered an error trying to list all namespaces"})
log.Errorln("Failed to get all namespaces: ", err)
return
}
ctx.JSON(http.StatusOK, nss)
}

// func metadataHandler(ctx *gin.Context) {
// path := ctx.Param("wildcard")

// // A weird feature of gin is that wildcards always
// // add a preceding /. We need to trim it here...
// path = strings.TrimPrefix(path, "/")
// log.Debug("Working with path ", path)
func metadataHandler(ctx *gin.Context) {
// A weird feature of gin is that wildcards always
// add a preceding /. Since the prefix / was trimmed
// out during the url parsing, we can just leave the
// new / here!
path := ctx.Param("wildcard")

// Get the prefix's JWKS
if filepath.Base(path) == "issuer.jwks" {
// do something
prefix := strings.TrimSuffix(path, "/.well-known/issuer.jwks")
jwks, err := dbGetPrefixJwks(prefix)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "server encountered an error trying to get jwks for prefix"})
log.Errorf("Failed to load jwks for prefix %s: %v", prefix, err)
return
}
ctx.JSON(http.StatusOK, jwks)
}

// // Get OpenID config info
// match, err := filepath.Match("*/\\.well-known/openid-configuration", path)
// if err != nil {
// log.Errorf("Failed to check incoming path for match: %v", err)
// return
// }
// if match {
// // do something
// } else {
// log.Errorln("Unknown request")
// return
// }

// // Get JWKS
// if filepath.Base(path) == "issuer.jwks" {
// // do something
// }
}

// // Get OpenID config info
// match, err := filepath.Match("*/\\.well-known/openid-configuration", path)
// func getJwks(prefix string) (*jwk.Set, error) {
// jwks, err := dbGetPrefixJwks(prefix)
// if err != nil {
// log.Errorf("Failed to check incoming path for match: %v", err)
// return
// }
// if match {
// // do something
// } else {
// log.Errorln("Unknown request")
// return
// return nil, errors.Wrapf(err, "Could not load jwks for prefix %s", prefix)
// }

// return jwks, nil
// }

/**
* Commenting out until we're ready to use it. -BB
func getJwks(c *gin.Context) {
prefix := c.Param("prefix")
c.JSON(http.StatusOK, gin.H{"status": "Get JWKS is not implemented", "prefix": prefix})
}
/*
Commenting out until we're ready to use it. -BB
func getOpenIDConfiguration(c *gin.Context) {
prefix := c.Param("prefix")
c.JSON(http.StatusOK, gin.H{"status": "getOpenIDConfiguration is not implemented", "prefix": prefix})
Expand All @@ -740,7 +752,7 @@ func RegisterNamespaceRegistry(router *gin.RouterGroup) {
registry.POST("", cliRegisterNamespace)
registry.GET("", dbGetAllNamespaces)
// Will handle getting jwks, openid config, and listing namespaces
// registry.GET("/*wildcard", metadataHandler)
registry.GET("/*wildcard", metadataHandler)

registry.DELETE("/*wildcard", dbDeleteNamespace)
}
Expand Down
32 changes: 31 additions & 1 deletion origin_ui/advertise.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,31 @@ func AdvertiseOrigin() error {
// TODO: waiting on a different branch to merge origin URL generation
originUrl := "https://localhost:8444"

// Here we instantiate the namespaceAd slice, but we still need to define the namespace
namespaceUrl, err := url.Parse(viper.GetString("NamespaceUrl"))
if err != nil {
return errors.New("Bad namespaceUrl")
}
if namespaceUrl.String() == "" {
return errors.New("No NamespaceUrl is set")
}

prefix := viper.GetString("NamespacePrefix")

// TODO: Need to figure out where to get some of these values
// so that they aren't hardcoded...
nsAd := director.NamespaceAd{
RequireToken: true,
Path: prefix,
Issuer: *namespaceUrl,
MaxScopeDepth: 3,
Strategy: "OAuth2",
BasePath: "/",
}
ad := director.OriginAdvertise{
Name: name,
URL: originUrl,
Namespaces: make([]director.NamespaceAd, 0),
Namespaces: []director.NamespaceAd{nsAd},
}

body, err := json.Marshal(ad)
Expand All @@ -81,13 +102,22 @@ func AdvertiseOrigin() error {
}
directorUrl.Path = "/api/v1.0/director/registerOrigin"

token, err := director.CreateAdvertiseToken(prefix)
if err != nil {
return errors.Wrap(err, "Failed to generate advertise token")
}
log.Debugln("Signed advertise token:", token)

req, err := http.NewRequest("POST", directorUrl.String(), bytes.NewBuffer(body))
if err != nil {
return errors.Wrap(err, "Failed to create POST request for director registration")
}

req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)

// We should switch this over to use the common transport, but for that to happen
// that function needs to be exported from pelican
client := http.Client{}
if viper.GetBool("TLSSkipVerify") {
tr := &http.Transport{
Expand Down

0 comments on commit 3bec835

Please sign in to comment.