diff --git a/director/origin_api_test.go b/director/origin_api_test.go new file mode 100644 index 000000000..1db45ce89 --- /dev/null +++ b/director/origin_api_test.go @@ -0,0 +1,162 @@ +package director + +import ( + "crypto/elliptic" + "path/filepath" + "testing" + "time" + + "github.com/jellydator/ttlcache/v3" + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/pelicanplatform/pelican/config" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +func TestVerifyAdvertiseToken(t *testing.T) { + /* + * Runs unit tests on the VerifyAdvertiseToken function + */ + + viper.Reset() + + tDir := t.TempDir() + kfile := filepath.Join(tDir, "t-key") + + //Setup a private key and a token + viper.Set("IssuerKey", kfile) + + viper.Set("NamespaceURL", "https://get-your-tokens.org") + viper.Set("DirectorURL", "https://director-url.org") + + kSet, err := config.LoadPublicKey("", kfile) + 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) + } + return *keyset, nil + }, + RegisterFn: func(m *MockCache) error { + m.keyset = *kSet + return nil + }, + } + + // Perform injections (ar.Register will create a jwk.keyset with the publickey in it) + func() { + if err = ar.Register("", jwk.WithMinRefreshInterval(15*time.Minute)); err != nil { + t.Errorf("this should never happen, should actually be impossible, including check for the linter") + } + namespaceKeysMutex.Lock() + defer namespaceKeysMutex.Unlock() + namespaceKeys.Set("test-namespace", &ar, ttlcache.DefaultTTL) + }() + + // A verified token with a the correct scope - should return no error + tok, err := CreateAdvertiseToken("test-namespace") + assert.NoError(t, err) + ok, err := VerifyAdvertiseToken(tok, "test-namespace") + assert.NoError(t, err) + assert.Equal(t, true, ok, "Expected scope to be 'pelican.advertise'") + + //Create token without a scope - should return an error + key, err := config.GetOriginJWK() + err = jwk.AssignKeyID(*key) + assert.NoError(t, err) + + scopelessTok, err := jwt.NewBuilder(). + Issuer(""). + Audience([]string{"director.test"}). + Subject("origin"). + Build() + + signed, err := jwt.Sign(scopelessTok, jwt.WithKey(jwa.ES512, *key)) + + ok, err = VerifyAdvertiseToken(string(signed), "test-namespace") + assert.Equal(t, false, ok) + assert.Equal(t, "No scope is present; required to advertise to director", err.Error()) + + //Create a token without a string valued scope + nonStrScopeTok, err := jwt.NewBuilder(). + Issuer(""). + Claim("scope", 22). + Audience([]string{"director.test"}). + Subject("origin"). + Build() + + signed, err = jwt.Sign(nonStrScopeTok, jwt.WithKey(jwa.ES512, *key)) + + ok, err = VerifyAdvertiseToken(string(signed), "test-namespace") + assert.Equal(t, false, ok) + assert.Equal(t, "scope claim in token is not string-valued", err.Error()) + + //Create a token without a pelican.namespace scope + wrongScopeTok, err := jwt.NewBuilder(). + Issuer(""). + Claim("scope", "wrong.scope"). + Audience([]string{"director.test"}). + Subject("origin"). + Build() + + signed, err = jwt.Sign(wrongScopeTok, jwt.WithKey(jwa.ES512, *key)) + + ok, err = VerifyAdvertiseToken(string(signed), "test-namespace") + assert.Equal(t, false, ok, "Should fail due to incorrect scope name") + assert.NoError(t, err, "Incorrect scope name should not throw and error") +} + +func TestCreateAdvertiseToken(t *testing.T) { + /* + * Runs unit tests on the CreateAdvertiseToken function + */ + + viper.Reset() + + // Create a temp directory to store the private key file + tDir := t.TempDir() + kfile := filepath.Join(tDir, "t-key") + + // Generate a private key + viper.Set("IssuerKey", kfile) + err := config.GeneratePrivateKey(kfile, elliptic.P521()) + assert.NoError(t, err) + + // Test without a namsepace set and check to see if it returns the expected error + tok, err := CreateAdvertiseToken("test-namespace") + assert.Equal(t, "", tok) + assert.Equal(t, "Namespace URL is not set", err.Error()) + viper.Set("NamespaceURL", "https://get-your-tokens.org") + + // Test without a DirectorURL set and check to see if it returns the expected error + tok, err = CreateAdvertiseToken("test-namespace") + assert.Equal(t, "", tok) + assert.Equal(t, "Director URL is not known; cannot create advertise token", err.Error()) + viper.Set("DirectorURL", "https://director-url.org") + + // Test the CreateAdvertiseToken with good values and test that it returns a non-nil token value and no error + tok, err = CreateAdvertiseToken("test-namespace") + assert.Equal(t, nil, err) + assert.NotEqual(t, "", tok) +} + +func TestGetIssuerURL(t *testing.T) { + /* + * Runs unit tests on the GetIssuerURL function + */ + viper.Reset() + + // No namespace url has been set, so an error is expected + url, err := GetIssuerURL("") + assert.Equal(t, "", url) + assert.Equal(t, "Namespace URL is not set", err.Error()) + + // Test to make sure the path is as expected + viper.Set("NamespaceURL", "test-path") + url, err = GetIssuerURL("test-prefix") + assert.Equal(t, nil, err) + assert.Equal(t, "test-path/api/v1.0/registry/test-prefix/.well-known/issuer.jwks", url) + +} diff --git a/director/redirect_test.go b/director/redirect_test.go index ab566aabc..cb0465167 100644 --- a/director/redirect_test.go +++ b/director/redirect_test.go @@ -24,7 +24,7 @@ import ( type MockCache struct { GetFn func(u string, kset *jwk.Set) (jwk.Set, error) - RegisterFn func(kset *jwk.Set) error + RegisterFn func(*MockCache) error keyset jwk.Set } @@ -35,7 +35,7 @@ func (m *MockCache) Get(ctx context.Context, u string) (jwk.Set, error) { func (m *MockCache) Register(u string, options ...jwk.RegisterOption) error { m.keyset = jwk.NewSet() - return m.RegisterFn(&m.keyset) + return m.RegisterFn(m) } func TestDirectorRegistration(t *testing.T) { @@ -45,6 +45,8 @@ func TestDirectorRegistration(t *testing.T) { * so again with an invalid token and confirms that the correct error is returned */ + viper.Reset() + // Setup httptest recorder and context for the the unit test w := httptest.NewRecorder() c, r := gin.CreateTestContext(w) @@ -106,8 +108,8 @@ func TestDirectorRegistration(t *testing.T) { } return *keyset, nil }, - RegisterFn: func(keyset *jwk.Set) error { - err := jwk.Set.AddKey(*keyset, publicKey) + RegisterFn: func(m *MockCache) error { + err := jwk.Set.AddKey(m.keyset, publicKey) if err != nil { t.Error(err) }