feat: add namespace blocking functionality for JWT authentication (#313)

## Summary

Implements a denylist mechanism to prevent blocked namespaces from
publishing packages:

- Added `BlockedNamespaces` field to `JWTManager` struct for maintaining
the denylist
- Added validation logic in `GenerateTokenResponse` that checks user
permissions against blocked namespaces
- Admins with global permissions (`*`) bypass the blocking logic
- Added comprehensive test coverage for all blocking scenarios

Fixes #98
This commit is contained in:
adam jones 2025-08-27 23:46:35 +01:00 • committed by GitHub
commit 9d04671ee3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -283,3 +283,118 @@ func TestNewJWTManager_InvalidKeySize(t *testing.T) {
auth.NewJWTManager(cfg)
})
}
func TestJWTManager_BlockedNamespaces(t *testing.T) {
// Generate a proper Ed25519 seed for testing
testSeed := make([]byte, ed25519.SeedSize)
_, err := rand.Read(testSeed)
require.NoError(t, err)
cfg := &config.Config{
JWTPrivateKey: hex.EncodeToString(testSeed),
}
ctx := context.Background()
t.Run("blocked namespace should deny token", func(t *testing.T) {
// Temporarily override blocked namespaces for testing
originalBlocked := auth.BlockedNamespaces
auth.BlockedNamespaces = []string{"io.github.spammer"}
defer func() { auth.BlockedNamespaces = originalBlocked }()
jwtManager := auth.NewJWTManager(cfg)
claims := auth.JWTClaims{
AuthMethod: model.AuthMethodGitHubAT,
AuthMethodSubject: "spammer",
Permissions: []auth.Permission{
{
Action: auth.PermissionActionPublish,
ResourcePattern: "io.github.spammer/*",
},
},
}
tokenResponse, err := jwtManager.GenerateTokenResponse(ctx, claims)
assert.Error(t, err)
assert.Contains(t, err.Error(), "your namespace is blocked")
assert.Nil(t, tokenResponse)
})
t.Run("non-blocked namespace should allow token", func(t *testing.T) {
// Temporarily override blocked namespaces for testing
originalBlocked := auth.BlockedNamespaces
auth.BlockedNamespaces = []string{"io.github.spammer"}
defer func() { auth.BlockedNamespaces = originalBlocked }()
jwtManager := auth.NewJWTManager(cfg)
claims := auth.JWTClaims{
AuthMethod: model.AuthMethodGitHubAT,
AuthMethodSubject: "gooduser",
Permissions: []auth.Permission{
{
Action: auth.PermissionActionPublish,
ResourcePattern: "io.github.gooduser/*",
},
},
}
tokenResponse, err := jwtManager.GenerateTokenResponse(ctx, claims)
require.NoError(t, err)
assert.NotEmpty(t, tokenResponse.RegistryToken)
})
t.Run("multiple permissions with one blocked should deny token", func(t *testing.T) {
// Temporarily override blocked namespaces for testing
originalBlocked := auth.BlockedNamespaces
auth.BlockedNamespaces = []string{"io.github.badorg"}
defer func() { auth.BlockedNamespaces = originalBlocked }()
jwtManager := auth.NewJWTManager(cfg)
claims := auth.JWTClaims{
AuthMethod: model.AuthMethodGitHubAT,
AuthMethodSubject: "user",
Permissions: []auth.Permission{
{
Action: auth.PermissionActionPublish,
ResourcePattern: "io.github.user/*", // allowed
},
{
Action: auth.PermissionActionPublish,
ResourcePattern: "io.github.badorg/*", // blocked
},
},
}
tokenResponse, err := jwtManager.GenerateTokenResponse(ctx, claims)
assert.Error(t, err)
assert.Contains(t, err.Error(), "your namespace is blocked")
assert.Nil(t, tokenResponse)
})
t.Run("global admin permissions should bypass denylist", func(t *testing.T) {
// Temporarily override blocked namespaces for testing
originalBlocked := auth.BlockedNamespaces
auth.BlockedNamespaces = []string{"io.github.spammer"}
defer func() { auth.BlockedNamespaces = originalBlocked }()
jwtManager := auth.NewJWTManager(cfg)
claims := auth.JWTClaims{
AuthMethod: model.AuthMethodNone,
AuthMethodSubject: "admin",
Permissions: []auth.Permission{
{
Action: auth.PermissionActionPublish,
ResourcePattern: "*", // global permission should bypass blocking
},
},
}
tokenResponse, err := jwtManager.GenerateTokenResponse(ctx, claims)
require.NoError(t, err)
assert.NotEmpty(t, tokenResponse.RegistryToken)
})
}