diff --git a/cmd/registry_client.go b/cmd/registry_client.go index 22ebbd57d..22aca6603 100644 --- a/cmd/registry_client.go +++ b/cmd/registry_client.go @@ -76,7 +76,7 @@ func registerANamespace(cmd *cobra.Command, args []string) { } // Parse the namespace URL to make sure it's okay - registrationEndpointURL, err := url.JoinPath(namespaceEndpoint, "api", "v1.0", "registry") + registrationEndpointURL, err := url.JoinPath(namespaceEndpoint, "api", "v2.0", "registry") if err != nil { log.Errorf("Failed to construction registration endpoint URL: %v", err) } @@ -143,7 +143,7 @@ func deleteANamespace(cmd *cobra.Command, args []string) { os.Exit(1) } - deletionEndpointURL, err := url.JoinPath(namespaceEndpoint, "api", "v1.0", "registry", prefix) + deletionEndpointURL, err := url.JoinPath(namespaceEndpoint, "api", "v2.0", "registry", prefix) if err != nil { log.Errorf("Failed to construction deletion endpoint URL: %v", err) } @@ -168,7 +168,7 @@ func listAllNamespaces(cmd *cobra.Command, args []string) { os.Exit(1) } - listEndpoint, err := url.JoinPath(namespaceEndpoint, "api", "v1.0", "registry") + listEndpoint, err := url.JoinPath(namespaceEndpoint, "api", "v2.0", "registry", "metadata") if err != nil { log.Errorf("Failed to construction list endpoint URL: %v", err) } diff --git a/director/origin_api.go b/director/origin_api.go index e961f262c..d90b057e8 100644 --- a/director/origin_api.go +++ b/director/origin_api.go @@ -23,7 +23,6 @@ import ( "encoding/json" "net/http" "net/url" - "path" "strings" "sync" "time" @@ -58,6 +57,8 @@ type NamespaceCache interface { var ( namespaceKeys = ttlcache.New[string, NamespaceCache](ttlcache.WithTTL[string, NamespaceCache](15 * time.Minute)) namespaceKeysMutex = sync.RWMutex{} + + adminApprovalErr error ) func CreateAdvertiseToken(namespace string) (string, error) { @@ -148,6 +149,10 @@ func VerifyAdvertiseToken(token, namespace string) (bool, error) { return false, errors.Wrap(err, "failed to marshal the public keyset into JWKS JSON") } log.Debugln("Constructed JWKS from fetching jwks:", string(jsonbuf)) + if jsonbuf == nil { + adminApprovalErr = errors.New(namespace + " has not been approved by an administrator.") + return false, adminApprovalErr + } } if err != nil { @@ -264,6 +269,9 @@ func GetRegistryIssuerURL(prefix string) (string, error) { if err != nil { return "", err } - namespace_url.Path = path.Join(namespace_url.Path, "api", "v1.0", "registry", prefix, ".well-known", "issuer.jwks") + namespace_url.Path, err = url.JoinPath(namespace_url.Path, "api", "v2.0", "registry", "metadata", prefix, ".well-known", "issuer.jwks") + if err != nil { + return "", err + } return namespace_url.String(), nil } diff --git a/director/origin_api_test.go b/director/origin_api_test.go index 77a774c1b..677123de4 100644 --- a/director/origin_api_test.go +++ b/director/origin_api_test.go @@ -37,8 +37,8 @@ func TestVerifyAdvertiseToken(t *testing.T) { kSet, err := config.GetIssuerPublicJWKS() ar := MockCache{ GetFn: func(key string, keyset *jwk.Set) (jwk.Set, error) { - if key != "https://get-your-tokens.org/api/v1.0/registry/test-namespace/.well-known/issuer.jwks" { - t.Errorf("expecting: https://get-your-tokens.org/api/v1.0/registry/test-namespace/.well-known/issuer.jwks, got %q", key) + if key != "https://get-your-tokens.org/api/v2.0/registry/metadata/test-namespace/.well-known/issuer.jwks" { + t.Errorf("expecting: https://get-your-tokens.org/api/v2.0/registry/metadata/test-namespace/.well-known/issuer.jwks, got %q", key) } return *keyset, nil }, @@ -160,7 +160,7 @@ func TestGetRegistryIssuerURL(t *testing.T) { viper.Set("Federation.NamespaceURL", "test-path") url, err = GetRegistryIssuerURL("test-prefix") assert.Equal(t, nil, err) - assert.Equal(t, "test-path/api/v1.0/registry/test-prefix/.well-known/issuer.jwks", url) + assert.Equal(t, "test-path/api/v2.0/registry/metadata/test-prefix/.well-known/issuer.jwks", url) } diff --git a/director/redirect.go b/director/redirect.go index e183b040e..8de5faf50 100644 --- a/director/redirect.go +++ b/director/redirect.go @@ -363,20 +363,20 @@ func ShortcutMiddleware(defaultResponse string) gin.HandlerFunc { func registerServeAd(ctx *gin.Context, sType ServerType) { tokens, present := ctx.Request.Header["Authorization"] if !present || len(tokens) == 0 { - ctx.JSON(401, gin.H{"error": "Bearer token not present in the 'Authorization' header"}) + ctx.JSON(http.StatusForbidden, gin.H{"error": "Bearer token not present in the 'Authorization' header"}) return } err := versionCompatCheck(ctx) if err != nil { log.Debugf("A version incompatibility was encountered while registering %s and no response was served: %v", sType, err) - ctx.JSON(500, gin.H{"error": "Incompatible versions detected: " + fmt.Sprintf("%v", err)}) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Incompatible versions detected: " + fmt.Sprintf("%v", err)}) return } ad := OriginAdvertise{} if ctx.ShouldBind(&ad) != nil { - ctx.JSON(400, gin.H{"error": "Invalid " + sType + " registration"}) + ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid " + sType + " registration"}) return } @@ -387,13 +387,13 @@ func registerServeAd(ctx *gin.Context, sType ServerType) { 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"}) + ctx.JSON(http.StatusForbidden, gin.H{"error": "Authorization token verification failed"}) return } if !ok { log.Warningf("%s %v advertised to namespace %v without valid registration\n", sType, ad.Name, namespace.Path) - ctx.JSON(400, gin.H{"error": sType + " not authorized to advertise to this namespace"}) + ctx.JSON(http.StatusForbidden, gin.H{"error": sType + " not authorized to advertise to this namespace"}) return } } @@ -402,14 +402,19 @@ func registerServeAd(ctx *gin.Context, sType ServerType) { prefix := path.Join("caches", ad.Name) ok, err := VerifyAdvertiseToken(token, prefix) if err != nil { - log.Warningln("Failed to verify token:", err) - ctx.JSON(400, gin.H{"error": "Authorization token verification failed"}) + if err == adminApprovalErr { + log.Warningln("Failed to verify token:", err) + ctx.JSON(http.StatusForbidden, gin.H{"error": "Cache is not admin approved"}) + } else { + log.Warningln("Failed to verify token:", err) + ctx.JSON(http.StatusForbidden, gin.H{"error": "Authorization token verification failed"}) + } return } if !ok { log.Warningf("%s %v advertised to namespace %v without valid registration\n", sType, ad.Name, prefix) - ctx.JSON(400, gin.H{"error": sType + " not authorized to advertise to this namespace"}) + ctx.JSON(http.StatusForbidden, gin.H{"error": sType + " not authorized to advertise to this namespace"}) return } } @@ -417,14 +422,14 @@ func registerServeAd(ctx *gin.Context, sType ServerType) { ad_url, err := url.Parse(ad.URL) if err != nil { log.Warningf("Failed to parse %s URL %v: %v\n", sType, ad.URL, err) - ctx.JSON(400, gin.H{"error": "Invalid " + sType + " URL"}) + ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid " + sType + " URL"}) return } adWebUrl, err := url.Parse(ad.WebURL) if err != nil && ad.WebURL != "" { // We allow empty WebURL string for backward compatibility log.Warningf("Failed to parse origin Web URL %v: %v\n", ad.WebURL, err) - ctx.JSON(400, gin.H{"error": "Invalid origin Web URL"}) + ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid origin Web URL"}) return } @@ -449,7 +454,7 @@ func registerServeAd(ctx *gin.Context, sType ServerType) { go PeriodicDirectorTest(ctx, sAd) } - ctx.JSON(200, gin.H{"msg": "Successful registration"}) + ctx.JSON(http.StatusOK, gin.H{"msg": "Successful registration"}) } // Return a list of available origins URL in Prometheus HTTP SD format diff --git a/director/redirect_test.go b/director/redirect_test.go index fd5cabceb..3b39908e4 100644 --- a/director/redirect_test.go +++ b/director/redirect_test.go @@ -132,8 +132,8 @@ func TestDirectorRegistration(t *testing.T) { setupMockCache := func(t *testing.T, publicKey jwk.Key) MockCache { return MockCache{ GetFn: func(key string, keyset *jwk.Set) (jwk.Set, error) { - if key != "https://get-your-tokens.org/api/v1.0/registry/foo/bar/.well-known/issuer.jwks" { - t.Errorf("expecting: https://get-your-tokens.org/api/v1.0/registry/foo/bar/.well-known/issuer.jwks, got %q", key) + if key != "https://get-your-tokens.org/api/v2.0/registry/metadata/foo/bar/.well-known/issuer.jwks" { + t.Errorf("expecting: https://get-your-tokens.org/api/v2.0/registry/metadata/foo/bar/.well-known/issuer.jwks, got %q", key) } return *keyset, nil }, @@ -217,7 +217,7 @@ func TestDirectorRegistration(t *testing.T) { r.ServeHTTP(w, c.Request) - assert.Equal(t, 400, w.Result().StatusCode, "Expected failing status code of 400") + assert.Equal(t, http.StatusForbidden, w.Result().StatusCode, "Expected failing status code of 403") body, _ := io.ReadAll(w.Result().Body) assert.Equal(t, `{"error":"Authorization token verification failed"}`, string(body), "Failure wasn't because token verification failed") diff --git a/registry/client_commands_test.go b/registry/client_commands_test.go index 3ac3936fc..711a81086 100644 --- a/registry/client_commands_test.go +++ b/registry/client_commands_test.go @@ -72,7 +72,7 @@ func TestServeNamespaceRegistry(t *testing.T) { require.NoError(t, err) //Test functionality of registering a namespace (without identity) - err = NamespaceRegister(privKey, svr.URL+"/api/v1.0/registry", "", "/foo/bar") + err = NamespaceRegister(privKey, svr.URL+"/api/v2.0/registry", "", "/foo/bar") require.NoError(t, err) //Test we can list the namespace without an error @@ -84,7 +84,7 @@ func TestServeNamespaceRegistry(t *testing.T) { os.Stdout = w //List the namespaces - err = NamespaceList(svr.URL + "/api/v1.0/registry") + err = NamespaceList(svr.URL + "/api/v2.0/registry") require.NoError(t, err) w.Close() os.Stdout = oldStdout @@ -103,7 +103,7 @@ func TestServeNamespaceRegistry(t *testing.T) { r, w, _ := os.Pipe() os.Stdout = w - err = NamespaceGet(svr.URL + "/api/v1.0/registry") + err = NamespaceGet(svr.URL + "/api/v2.0/registry") require.NoError(t, err) w.Close() os.Stdout = oldStdout @@ -116,13 +116,13 @@ func TestServeNamespaceRegistry(t *testing.T) { t.Run("Test namespace delete", func(t *testing.T) { //Test functionality of namespace delete - err = NamespaceDelete(svr.URL+"/api/v1.0/registry/foo/bar", "/foo/bar") + err = NamespaceDelete(svr.URL+"/api/v2.0/registry/foo/bar", "/foo/bar") require.NoError(t, err) var stdoutCapture string oldStdout := os.Stdout r, w, _ := os.Pipe() os.Stdout = w - err = NamespaceGet(svr.URL + "/api/v1.0/registry") + err = NamespaceGet(svr.URL + "/api/v2.0/registry") require.NoError(t, err) w.Close() os.Stdout = oldStdout @@ -161,19 +161,19 @@ func TestRegistryKeyChainingOSDF(t *testing.T) { require.NoError(t, err) // Start by registering /foo/bar with the default key - err = NamespaceRegister(privKey, registrySvr.URL+"/api/v1.0/registry", "", "/foo/bar") + err = NamespaceRegister(privKey, registrySvr.URL+"/api/v2.0/registry", "", "/foo/bar") require.NoError(t, err) // Perform one test with a subspace and the same key -- should succeed - err = NamespaceRegister(privKey, registrySvr.URL+"/api/v1.0/registry", "", "/foo/bar/test") + err = NamespaceRegister(privKey, registrySvr.URL+"/api/v2.0/registry", "", "/foo/bar/test") require.NoError(t, err) // For now, we simply don't allow further super/sub spacing of namespaces from topo, because how // can we validate via a key if there is none? - err = NamespaceRegister(privKey, registrySvr.URL+"/api/v1.0/registry", "", "/topo/foo/bar") + err = NamespaceRegister(privKey, registrySvr.URL+"/api/v2.0/registry", "", "/topo/foo/bar") require.Error(t, err) - err = NamespaceRegister(privKey, registrySvr.URL+"/api/v1.0/registry", "", "/topo") + err = NamespaceRegister(privKey, registrySvr.URL+"/api/v2.0/registry", "", "/topo") require.Error(t, err) // Now we create a new key and try to use it to register a super/sub space. These shouldn't succeed @@ -183,28 +183,28 @@ func TestRegistryKeyChainingOSDF(t *testing.T) { privKey, err = config.GetIssuerPrivateJWK() require.NoError(t, err) - err = NamespaceRegister(privKey, registrySvr.URL+"/api/v1.0/registry", "", "/foo/bar/baz") + err = NamespaceRegister(privKey, registrySvr.URL+"/api/v2.0/registry", "", "/foo/bar/baz") require.ErrorContains(t, err, "Cannot register a namespace that is suffixed or prefixed") - err = NamespaceRegister(privKey, registrySvr.URL+"/api/v1.0/registry", "", "/foo") + err = NamespaceRegister(privKey, registrySvr.URL+"/api/v2.0/registry", "", "/foo") require.ErrorContains(t, err, "Cannot register a namespace that is suffixed or prefixed") // Make sure we can register things similar but distinct in prefix and suffix - err = NamespaceRegister(privKey, registrySvr.URL+"/api/v1.0/registry", "", "/fo") + err = NamespaceRegister(privKey, registrySvr.URL+"/api/v2.0/registry", "", "/fo") require.NoError(t, err) - err = NamespaceRegister(privKey, registrySvr.URL+"/api/v1.0/registry", "", "/foo/barz") + err = NamespaceRegister(privKey, registrySvr.URL+"/api/v2.0/registry", "", "/foo/barz") require.NoError(t, err) // Now turn off token chaining and retry -- no errors should occur viper.Set("Registry.RequireKeyChaining", false) - err = NamespaceRegister(privKey, registrySvr.URL+"/api/v1.0/registry", "", "/foo/bar/baz") + err = NamespaceRegister(privKey, registrySvr.URL+"/api/v2.0/registry", "", "/foo/bar/baz") require.NoError(t, err) - err = NamespaceRegister(privKey, registrySvr.URL+"/api/v1.0/registry", "", "/foo") + err = NamespaceRegister(privKey, registrySvr.URL+"/api/v2.0/registry", "", "/foo") require.NoError(t, err) // Finally, test with one value for topo - err = NamespaceRegister(privKey, registrySvr.URL+"/api/v1.0/registry", "", "/topo") + err = NamespaceRegister(privKey, registrySvr.URL+"/api/v2.0/registry", "", "/topo") require.NoError(t, err) config.SetPreferredPrefix("pelican") @@ -229,11 +229,11 @@ func TestRegistryKeyChaining(t *testing.T) { require.NoError(t, err) // Start by registering /foo/bar with the default key - err = NamespaceRegister(privKey, registrySvr.URL+"/api/v1.0/registry", "", "/foo/bar") + err = NamespaceRegister(privKey, registrySvr.URL+"/api/v2.0/registry", "", "/foo/bar") require.NoError(t, err) // Perform one test with a subspace and the same key -- should succeed - err = NamespaceRegister(privKey, registrySvr.URL+"/api/v1.0/registry", "", "/foo/bar/test") + err = NamespaceRegister(privKey, registrySvr.URL+"/api/v2.0/registry", "", "/foo/bar/test") require.NoError(t, err) // Now we create a new key and try to use it to register a super/sub space. These shouldn't succeed @@ -243,18 +243,18 @@ func TestRegistryKeyChaining(t *testing.T) { privKey, err = config.GetIssuerPrivateJWK() require.NoError(t, err) - err = NamespaceRegister(privKey, registrySvr.URL+"/api/v1.0/registry", "", "/foo/bar/baz") + err = NamespaceRegister(privKey, registrySvr.URL+"/api/v2.0/registry", "", "/foo/bar/baz") require.ErrorContains(t, err, "Cannot register a namespace that is suffixed or prefixed") - err = NamespaceRegister(privKey, registrySvr.URL+"/api/v1.0/registry", "", "/foo") + err = NamespaceRegister(privKey, registrySvr.URL+"/api/v2.0/registry", "", "/foo") require.ErrorContains(t, err, "Cannot register a namespace that is suffixed or prefixed") // Now turn off token chaining and retry -- no errors should occur viper.Set("Registry.RequireKeyChaining", false) - err = NamespaceRegister(privKey, registrySvr.URL+"/api/v1.0/registry", "", "/foo/bar/baz") + err = NamespaceRegister(privKey, registrySvr.URL+"/api/v2.0/registry", "", "/foo/bar/baz") require.NoError(t, err) - err = NamespaceRegister(privKey, registrySvr.URL+"/api/v1.0/registry", "", "/foo") + err = NamespaceRegister(privKey, registrySvr.URL+"/api/v2.0/registry", "", "/foo") require.NoError(t, err) viper.Reset() diff --git a/registry/registry.go b/registry/registry.go index 2d1d0101f..8c159a24d 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -80,6 +80,10 @@ type Response struct { DeviceCode string `json:"device_code"` } +type AdminJSON struct { + AdminApproved bool `json:"admin_approved"` +} + type TokenResponse struct { AccessToken string `json:"access_token"` Error string `json:"error"` @@ -111,7 +115,7 @@ func matchKeys(incomingKey jwk.Key, registeredNamespaces []string) (bool, error) // permitting the action (assuming their keys haven't been stolen!) foundMatch := false for _, ns := range registeredNamespaces { - keyset, err := dbGetPrefixJwks(ns) + keyset, err := dbGetPrefixJwks(ns, false) if err != nil { return false, errors.Wrapf(err, "Cannot get keyset for %s from the database", ns) } @@ -599,6 +603,16 @@ func dbAddNamespace(ctx *gin.Context, data *registrationData) error { ns.Identity = data.Identity } + //All caches added will not be approved (also false for origins, but that's fine as it doesn't check for origins) + jResult, err := json.Marshal(AdminJSON{ + AdminApproved: false, + }) + + if err != nil { + return errors.Wrapf(err, "Failure to unmarshal json struct") + } + ns.AdminMetadata = string(jResult) + err = addNamespace(&ns) if err != nil { return errors.Wrapf(err, "Failed to add prefix %s", ns.Prefix) @@ -640,7 +654,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 := dbGetPrefixJwks(prefix) + originJwks, err := dbGetPrefixJwks(prefix, false) 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) @@ -731,8 +745,14 @@ func metadataHandler(ctx *gin.Context) { if filepath.Base(path) == "issuer.jwks" { // do something prefix := strings.TrimSuffix(path, "/.well-known/issuer.jwks") - jwks, err := dbGetPrefixJwks(prefix) + + jwks, err := dbGetPrefixJwks(prefix, true) + if err != nil { + if err == serverCredsErr { + ctx.JSON(404, gin.H{"error": "cache has not been approved by federation administrator"}) + return + } 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 @@ -755,6 +775,17 @@ func metadataHandler(ctx *gin.Context) { } +func dbGetNamespace(ctx *gin.Context) { + prefix := ctx.GetHeader("X-Pelican-Prefix") + ns, err := getNamespace(prefix) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, ns) +} + // func getJwks(prefix string) (*jwk.Set, error) { // jwks, err := dbGetPrefixJwks(prefix) // if err != nil { @@ -772,13 +803,22 @@ func getOpenIDConfiguration(c *gin.Context) { */ func RegisterRegistryRoutes(router *gin.RouterGroup) { - registry := router.Group("/api/v1.0/registry") + v1registry := router.Group("/api/v1.0/registry") { - registry.POST("", cliRegisterNamespace) - registry.GET("", dbGetAllNamespaces) + v1registry.POST("", cliRegisterNamespace) + v1registry.GET("", dbGetAllNamespaces) // Will handle getting jwks, openid config, and listing namespaces - registry.GET("/*wildcard", metadataHandler) + v1registry.GET("/*wildcard", metadataHandler) + v1registry.DELETE("/*wildcard", dbDeleteNamespace) + } - registry.DELETE("/*wildcard", dbDeleteNamespace) + v2registry := router.Group("/api/v2.0/registry") + { + v2registry.POST("", cliRegisterNamespace) + v2registry.GET("", dbGetAllNamespaces) + v2registry.GET("/getNamespace", dbGetNamespace) + // Will handle getting jwks, openid config, and listing namespaces + v2registry.GET("/metadata/*wildcard", metadataHandler) + v2registry.DELETE("/*wildcard", dbDeleteNamespace) } } diff --git a/registry/registry_db.go b/registry/registry_db.go index 1ec3be23a..c50520541 100644 --- a/registry/registry_db.go +++ b/registry/registry_db.go @@ -20,9 +20,11 @@ package registry import ( "database/sql" + "encoding/json" "fmt" "os" "path/filepath" + "strings" "time" "github.com/lestrrat-go/jwx/v2/jwk" @@ -72,13 +74,15 @@ https://www.alexedwards.net/blog/organising-database-access var db *sql.DB func createNamespaceTable() { + //We put a size limit on admin_metadata to guard against potentially future + //malicious large inserts query := ` CREATE TABLE IF NOT EXISTS namespace ( id INTEGER PRIMARY KEY AUTOINCREMENT, prefix TEXT NOT NULL UNIQUE, pubkey TEXT NOT NULL, identity TEXT, - admin_metadata TEXT + admin_metadata TEXT CHECK (length("admin_metadata") <= 4000) );` _, err := db.Exec(query) @@ -227,15 +231,35 @@ func getPrefixJwksById(id int) (jwk.Set, error) { return set, nil } -func dbGetPrefixJwks(prefix string) (*jwk.Set, error) { - jwksQuery := `SELECT pubkey FROM namespace WHERE prefix = ?` +func dbGetPrefixJwks(prefix string, approvalRequired bool) (*jwk.Set, error) { + var jwksQuery string var pubkeyStr string - err := db.QueryRow(jwksQuery, prefix).Scan(&pubkeyStr) - if err != nil { - if err == sql.ErrNoRows { - return nil, errors.New("prefix not found in database") + if strings.HasPrefix(prefix, "/caches/") && approvalRequired { + var admin_metadata string + jwksQuery = `SELECT pubkey, admin_metadata FROM namespace WHERE prefix = ?` + err := db.QueryRow(jwksQuery, prefix).Scan(&pubkeyStr, &admin_metadata) + if err != nil { + if err == sql.ErrNoRows { + return nil, errors.New("prefix not found in database") + } + return nil, errors.Wrap(err, "error performing cache pubkey query") + } + + var adminData AdminJSON + err = json.Unmarshal([]byte(admin_metadata), &adminData) + + if !adminData.AdminApproved || err != nil { + return nil, serverCredsErr + } + } else { + jwksQuery := `SELECT pubkey FROM namespace WHERE prefix = ?` + err := db.QueryRow(jwksQuery, prefix).Scan(&pubkeyStr) + if err != nil { + if err == sql.ErrNoRows { + return nil, errors.New("prefix not found in database") + } + return nil, errors.Wrap(err, "error performing origin pubkey query") } - return nil, errors.Wrap(err, "error performing origin pubkey query") } set, err := jwk.ParseString(pubkeyStr) @@ -292,8 +316,6 @@ func deleteNamespace(prefix string) error { return tx.Commit() } -/** - * Commenting this out until we are ready to use it. -BB func getNamespace(prefix string) (*Namespace, error) { ns := &Namespace{} query := `SELECT * FROM namespace WHERE prefix = ?` @@ -303,7 +325,6 @@ func getNamespace(prefix string) (*Namespace, error) { } return ns, nil } -*/ func getAllNamespaces() ([]*Namespace, error) { query := `SELECT * FROM namespace` diff --git a/registry/registry_db_test.go b/registry/registry_db_test.go index 8c74c90c3..6fa4be2e8 100644 --- a/registry/registry_db_test.go +++ b/registry/registry_db_test.go @@ -335,3 +335,103 @@ func TestRegistryTopology(t *testing.T) { viper.Reset() } + +func TestCacheAdminTrue(t *testing.T) { + + registryDBDir := t.TempDir() + viper.Set("Registry.DbLocation", registryDBDir) + + err := InitializeDB() + defer ShutdownDB() + + require.NoError(t, err, "error initializing registry database") + jResult, err := json.Marshal(AdminJSON{ + AdminApproved: true, + }) + + require.NoError(t, err, "error marshalling json admin data") + + adminTester := func(ns Namespace) func(t *testing.T) { + return func(t *testing.T) { + err = addNamespace(&ns) + + require.NoError(t, err, "error adding test cache to registry database") + + // This will return a serverCredsError if the admin_approval == false check is triggered, which we don't want to happen + // For these tests, otherwise it will get a key parsing error as ns.Pubkey isn't a real jwk + _, err = dbGetPrefixJwks(ns.Prefix, true) + require.NotErrorIsf(t, err, serverCredsErr, "error chain contains serverCredErr") + + require.ErrorContainsf(t, err, "Failed to parse pubkey as a jwks: failed to unmarshal JWK set: invalid character 'k' in literal true (expecting 'r')", "error doesn't contain jwks parsing error") + } + } + + var ns Namespace + ns.Prefix = "/caches/test3" + ns.Identity = "testident3" + ns.Pubkey = "tkey" + ns.AdminMetadata = string(jResult) + + t.Run("WithApproval", adminTester(ns)) + + jResult, err = json.Marshal(AdminJSON{ + AdminApproved: false, + }) + + ns.Prefix = "/orig/test1" + ns.Identity = "testident4" + ns.Pubkey = "tkey" + ns.AdminMetadata = string(jResult) + + t.Run("OriginNoApproval", adminTester(ns)) + + ns.Prefix = "/orig/test2" + ns.Identity = "testident5" + ns.Pubkey = "tkey" + ns.AdminMetadata = "" + + t.Run("OriginEmptyApproval", adminTester(ns)) + + viper.Reset() +} + +func TestCacheAdminFalse(t *testing.T) { + registryDBDir := t.TempDir() + viper.Set("Registry.DbLocation", registryDBDir) + + err := InitializeDB() + defer ShutdownDB() + + require.NoError(t, err, "error initializing registry database") + jResult, err := json.Marshal(AdminJSON{ + AdminApproved: false, + }) + + adminTester := func(ns Namespace) func(t *testing.T) { + return func(t *testing.T) { + err = addNamespace(&ns) + require.NoError(t, err, "error adding test cache to registry database") + + // This will return a serverCredsError if the admin_approval == false check is triggered, which we want to happen + _, err = dbGetPrefixJwks(ns.Prefix, true) + + require.ErrorIs(t, err, serverCredsErr) + } + } + + var ns Namespace + ns.Prefix = "/caches/test1" + ns.Identity = "testident1" + ns.Pubkey = "tkey" + ns.AdminMetadata = string(jResult) + + t.Run("NoAdmin", adminTester(ns)) + + ns.Prefix = "/caches/test2" + ns.Identity = "testident2" + ns.AdminMetadata = "" + + t.Run("EmptyAdmin", adminTester(ns)) + + viper.Reset() +} diff --git a/server_ui/advertise.go b/server_ui/advertise.go index a19a150ea..ec74dfe96 100644 --- a/server_ui/advertise.go +++ b/server_ui/advertise.go @@ -132,6 +132,9 @@ func Advertise(server server_utils.XRootDServer) error { if unmarshalErr := json.Unmarshal(body, &respErr); unmarshalErr != nil { // Error creating json return errors.Wrapf(unmarshalErr, "Could not unmarshall the director's response, which responded %v from director registration: %v", resp.StatusCode, resp.Status) } + if resp.StatusCode == http.StatusForbidden { + return errors.Errorf("Error during director advertisement: Cache has not been approved by administrator.") + } return errors.Errorf("Error during director registration: %v\n", respErr.Error) } diff --git a/server_ui/register_namespace.go b/server_ui/register_namespace.go index 0987e05cf..964fef3f0 100644 --- a/server_ui/register_namespace.go +++ b/server_ui/register_namespace.go @@ -48,7 +48,7 @@ const ( keyMatch ) -func keyIsRegistered(privkey jwk.Key, url string) (keyStatus, error) { +func keyIsRegistered(privkey jwk.Key, url string, prefix string) (keyStatus, error) { keyId := privkey.KeyID() if keyId == "" { return noKeyPresent, errors.New("Provided key is missing a key ID") @@ -58,12 +58,15 @@ func keyIsRegistered(privkey jwk.Key, url string) (keyStatus, error) { return noKeyPresent, err } - req, err := http.NewRequest("GET", url+"/.well-known/issuer.jwks", nil) + req, err := http.NewRequest("GET", url, nil) + if err != nil { return noKeyPresent, err } req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Pelican-Prefix", prefix) + tr := config.GetTransport() client := &http.Client{Transport: tr} @@ -92,7 +95,13 @@ func keyIsRegistered(privkey jwk.Key, url string) (keyStatus, error) { } } - registrySet, err := jwk.Parse(body) + var ns *registry.Namespace + err = json.Unmarshal(body, &ns) + if err != nil { + return noKeyPresent, errors.Errorf("Failed unmarshal namespace from response") + } + + registrySet, err := jwk.ParseString(ns.Pubkey) if err != nil { log.Debugln("Failed to parse registry response:", string(body)) return noKeyPresent, errors.Wrap(err, "Failed to parse registry response as a JWKS") @@ -127,9 +136,14 @@ func registerNamespacePrep() (key jwk.Key, prefix string, registrationEndpointUR return } - registrationEndpointURL, err = url.JoinPath(namespaceEndpoint, "api", "v1.0", "registry") + registrationEndpointURL, err = url.JoinPath(namespaceEndpoint, "api", "v2.0", "registry") + if err != nil { + err = errors.Wrap(err, "Failed to construct registration endpoint URL: %v") + return + } + registrationCheckEndpointURL, err := url.JoinPath(registrationEndpointURL, "getNamespace") if err != nil { - err = errors.Wrap(err, "Failed to construction registration endpoint URL: %v") + err = errors.Wrap(err, "Failed to construct registration check endpoint URL: %v") return } @@ -144,7 +158,7 @@ func registerNamespacePrep() (key jwk.Key, prefix string, registrationEndpointUR return } } - keyStatus, err := keyIsRegistered(key, registrationEndpointURL+prefix) + keyStatus, err := keyIsRegistered(key, registrationCheckEndpointURL, prefix) if err != nil { err = errors.Wrap(err, "Failed to determine whether namespace is already registered") return diff --git a/server_ui/register_namespace_test.go b/server_ui/register_namespace_test.go index 147b2c8ee..81f12610b 100644 --- a/server_ui/register_namespace_test.go +++ b/server_ui/register_namespace_test.go @@ -92,13 +92,13 @@ func TestRegistration(t *testing.T) { key, prefix, registerURL, isRegistered, err := registerNamespacePrep() require.NoError(t, err) assert.False(t, isRegistered) - assert.Equal(t, registerURL, svr.URL+"/api/v1.0/registry") + assert.Equal(t, registerURL, svr.URL+"/api/v2.0/registry") assert.Equal(t, prefix, "/test123") err = registerNamespaceImpl(key, prefix, registerURL) require.NoError(t, err) // Test we can query for the new key - req, err := http.NewRequest("GET", svr.URL+"/api/v1.0/registry", nil) + req, err := http.NewRequest("GET", svr.URL+"/api/v2.0/registry", nil) require.NoError(t, err) req.Header.Set("Content-Type", "application/json") tr := config.GetTransport() @@ -125,7 +125,7 @@ func TestRegistration(t *testing.T) { assert.True(t, jwk.Equal(registryKey, key)) // Test the functionality of the keyIsRegistered function - keyStatus, err := keyIsRegistered(key, svr.URL+"/api/v1.0/registry/test123") + keyStatus, err := keyIsRegistered(key, svr.URL+"/api/v2.0/registry/getNamespace", "/test123") assert.NoError(t, err) require.Equal(t, keyStatus, keyMatch) @@ -137,18 +137,18 @@ func TestRegistration(t *testing.T) { keyAlt, err := privKeyAlt.PublicKey() require.NoError(t, err) assert.NoError(t, jwk.AssignKeyID(keyAlt)) - keyStatus, err = keyIsRegistered(keyAlt, svr.URL+"/api/v1.0/registry/test123") + keyStatus, err = keyIsRegistered(keyAlt, svr.URL+"/api/v2.0/registry/getNamespace", "/test123") assert.NoError(t, err) assert.Equal(t, keyStatus, keyMismatch) // Verify that no key is present for an alternate prefix - keyStatus, err = keyIsRegistered(key, svr.URL+"/test456") + keyStatus, err = keyIsRegistered(key, svr.URL, "test456") assert.NoError(t, err) assert.Equal(t, keyStatus, noKeyPresent) // Redo the namespace prep, ensure that isPresent is true _, prefix, registerURL, isRegistered, err = registerNamespacePrep() - assert.Equal(t, svr.URL+"/api/v1.0/registry", registerURL) + assert.Equal(t, svr.URL+"/api/v2.0/registry", registerURL) assert.NoError(t, err) assert.Equal(t, prefix, "/test123") assert.True(t, isRegistered)