diff --git a/director/origin_api.go b/director/origin_api.go index e30d1c0ee..321438495 100644 --- a/director/origin_api.go +++ b/director/origin_api.go @@ -20,7 +20,9 @@ package director import ( "context" - "errors" + "crypto/tls" + "encoding/json" + "net/http" "net/url" "path" "strings" @@ -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" ) @@ -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 } @@ -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)). @@ -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 } @@ -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 } @@ -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 } diff --git a/director/redirect.go b/director/redirect.go index 61635caea..db2cf1f50 100644 --- a/director/redirect.go +++ b/director/redirect.go @@ -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"}) diff --git a/namespace-registry/registry-db.go b/namespace-registry/registry-db.go index 2b8f31b55..091e9d780 100644 --- a/namespace-registry/registry-db.go +++ b/namespace-registry/registry-db.go @@ -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) diff --git a/namespace-registry/registry.go b/namespace-registry/registry.go index 37b63b28e..7151ee363 100644 --- a/namespace-registry/registry.go +++ b/namespace-registry/registry.go @@ -30,6 +30,7 @@ import ( "net/http" "net/url" "os" + "path/filepath" "strings" "sync" @@ -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) @@ -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}) @@ -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) } diff --git a/origin_ui/advertise.go b/origin_ui/advertise.go index 25fc4c322..d9d02b7ed 100644 --- a/origin_ui/advertise.go +++ b/origin_ui/advertise.go @@ -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) @@ -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{