diff --git a/README.md b/README.md index d5a64ba..17ea585 100644 --- a/README.md +++ b/README.md @@ -460,4 +460,4 @@ This is **by design** - Barnacle routes to multiple upstream registries, requiri | Accept header content negotiation | Can't negotiate manifest format (image vs list) | Medium | | Range request support (206 Partial Content) | Can't resume interrupted blob downloads | Low | | Accept-Ranges header on blob endpoints | Clients don't know ranges are unsupported | Low | -| Content-Type header on HEAD blob | Minor, not strictly required by spec | Low | \ No newline at end of file +| Content-Type header on HEAD blob | Minor, not strictly required by spec | Low |Ok s \ No newline at end of file diff --git a/docs/architecture/auth/next.md b/docs/architecture/auth/next.md index 9def853..a4458b7 100644 --- a/docs/architecture/auth/next.md +++ b/docs/architecture/auth/next.md @@ -99,6 +99,19 @@ If the HEAD check returns `401` or `403`, barnacle returns the upstream's error **Environment:** Cache is shared across all clients. No per-client scoping or revalidation needed since barnacle owns the credentials and all clients are treated equally. +## Configuration schema design + +Auth settings are configured using a flat, named-key structure where each supported auth method has a dedicated top-level field. Only one method should be set at a time; this is enforced at validation rather than at the schema level. + +```yaml +auth: + basic: + username: xxx + password: xxx +``` + +This approach keeps the schema straightforward and self-documenting — valid fields for each auth method are fully described without requiring a polymorphic type/spec envelope. Mutual exclusivity is validated explicitly on startup, failing fast with a clear error if misconfigured. + ## Out of Scope - Client-facing auth enforcement (no plans to require clients to authenticate to barnacle itself) diff --git a/internal/configloader/configloader.go b/internal/configloader/configloader.go index 66beaf8..4412de5 100644 --- a/internal/configloader/configloader.go +++ b/internal/configloader/configloader.go @@ -67,7 +67,7 @@ func LoadConfig(configDirectory string, logger *zap.Logger) (*configuration.Conf // Validate the configuration log.Debug("Validating configuration") - if err := config.Validate(); err != nil { + if err := config.Validate(log); err != nil { log.Error("Configuration validation failed", zap.Error(err)) return nil, fmt.Errorf("%w: %w", ErrValidateConfiguration, err) } diff --git a/internal/registry/registry_internal_test.go b/internal/registry/registry_internal_test.go index 8d09d2e..cd886d0 100644 --- a/internal/registry/registry_internal_test.go +++ b/internal/registry/registry_internal_test.go @@ -71,10 +71,8 @@ func TestNewUpstreamRegistry(t *testing.T) { name: "single upstream", upstreams: map[string]configuration.UpstreamConfiguration{ "dockerio": { - Registry: "https://registry-1.docker.io", - Authentication: configuration.UpstreamAuthentication{ - Anonymous: &configuration.UpstreamAnonymousAuthentication{}, - }, + Registry: "https://registry-1.docker.io", + Authentication: configuration.UpstreamAuthentication{}, }, }, wantErr: false, @@ -83,19 +81,12 @@ func TestNewUpstreamRegistry(t *testing.T) { name: "multiple upstreams", upstreams: map[string]configuration.UpstreamConfiguration{ "dockerio": { - Registry: "https://registry-1.docker.io", - Authentication: configuration.UpstreamAuthentication{ - Anonymous: &configuration.UpstreamAnonymousAuthentication{}, - }, + Registry: "https://registry-1.docker.io", + Authentication: configuration.UpstreamAuthentication{}, }, "gcr": { - Registry: "https://gcr.io", - Authentication: configuration.UpstreamAuthentication{ - Basic: &configuration.UpstreamBasicAuthentication{ - Username: "user", - Password: "pass", - }, - }, + Registry: "https://gcr.io", + Authentication: configuration.UpstreamAuthentication{}, }, }, wantErr: false, @@ -158,10 +149,8 @@ func TestUpstreamRegistry_ListUpstreams(t *testing.T) { name: "single upstream", upstreams: map[string]configuration.UpstreamConfiguration{ "dockerio": { - Registry: "https://registry-1.docker.io", - Authentication: configuration.UpstreamAuthentication{ - Anonymous: &configuration.UpstreamAnonymousAuthentication{}, - }, + Registry: "https://registry-1.docker.io", + Authentication: configuration.UpstreamAuthentication{}, }, }, wantList: []string{"dockerio"}, @@ -170,22 +159,16 @@ func TestUpstreamRegistry_ListUpstreams(t *testing.T) { name: "multiple upstreams", upstreams: map[string]configuration.UpstreamConfiguration{ "dockerio": { - Registry: "https://registry-1.docker.io", - Authentication: configuration.UpstreamAuthentication{ - Anonymous: &configuration.UpstreamAnonymousAuthentication{}, - }, + Registry: "https://registry-1.docker.io", + Authentication: configuration.UpstreamAuthentication{}, }, "gcr": { - Registry: "https://gcr.io", - Authentication: configuration.UpstreamAuthentication{ - Anonymous: &configuration.UpstreamAnonymousAuthentication{}, - }, + Registry: "https://gcr.io", + Authentication: configuration.UpstreamAuthentication{}, }, "quay": { - Registry: "https://quay.io", - Authentication: configuration.UpstreamAuthentication{ - Anonymous: &configuration.UpstreamAnonymousAuthentication{}, - }, + Registry: "https://quay.io", + Authentication: configuration.UpstreamAuthentication{}, }, }, wantList: []string{"dockerio", "gcr", "quay"}, @@ -229,16 +212,12 @@ func TestUpstreamRegistry_GetUpstream(t *testing.T) { logger := testutils.CreateTestLogger(t) config := newTestConfig(t, map[string]configuration.UpstreamConfiguration{ "dockerio": { - Registry: "https://registry-1.docker.io", - Authentication: configuration.UpstreamAuthentication{ - Anonymous: &configuration.UpstreamAnonymousAuthentication{}, - }, + Registry: "https://registry-1.docker.io", + Authentication: configuration.UpstreamAuthentication{}, }, "gcr": { - Registry: "https://gcr.io", - Authentication: configuration.UpstreamAuthentication{ - Anonymous: &configuration.UpstreamAnonymousAuthentication{}, - }, + Registry: "https://gcr.io", + Authentication: configuration.UpstreamAuthentication{}, }, }) redisClient, cleanup := setupTestRedis(t) diff --git a/internal/registry/upstream/standard/standard.go b/internal/registry/upstream/standard/standard.go index 0874cd1..ee08d73 100644 --- a/internal/registry/upstream/standard/standard.go +++ b/internal/registry/upstream/standard/standard.go @@ -6,7 +6,6 @@ import ( "io" "strings" - "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/remote" @@ -34,27 +33,9 @@ type standardUpstream struct { logger *zap.Logger } -// remoteOptions returns the remote options for upstream registry operations, -// including authentication if configured. +// remoteOptions returns the remote options for upstream registry operations. func (s *standardUpstream) remoteOptions(ctx context.Context) []remote.Option { - opts := []remote.Option{remote.WithContext(ctx)} - - auth := s.config.Authentication - switch { - case auth.Basic != nil: - opts = append(opts, remote.WithAuth(authn.FromConfig(authn.AuthConfig{ - Username: auth.Basic.Username, - Password: auth.Basic.Password, - }))) - case auth.Bearer != nil: - opts = append(opts, remote.WithAuth(authn.FromConfig(authn.AuthConfig{ - RegistryToken: auth.Bearer.Token, - }))) - default: - // Anonymous authentication - no auth option needed - } - - return opts + return []remote.Option{remote.WithContext(ctx)} } // buildReference constructs a reference string from registry, repo, and tag/digest. diff --git a/internal/routes/apiroutes/upstreamsapi/v1.go b/internal/routes/apiroutes/upstreamsapi/v1.go index 46ab22a..5250f90 100644 --- a/internal/routes/apiroutes/upstreamsapi/v1.go +++ b/internal/routes/apiroutes/upstreamsapi/v1.go @@ -76,17 +76,6 @@ func (c *upstreamControllerV1) Get(ctx *gin.Context) { ctx.JSON(http.StatusOK, upstreamdtos.GetUpstreamResponse{ Alias: alias, Registry: cfg.Registry, - AuthType: authType(&cfg.Authentication), + AuthType: cfg.Authentication.GetAuthType().GetName(), }) } - -// authType returns a string identifying the configured authentication type. -func authType(auth *configuration.UpstreamAuthentication) string { - if auth.Basic != nil { - return "basic" - } - if auth.Bearer != nil { - return "bearer" - } - return "anonymous" -} diff --git a/internal/routes/apiroutes/upstreamsapi/v1_test.go b/internal/routes/apiroutes/upstreamsapi/v1_test.go index f847095..04310db 100644 --- a/internal/routes/apiroutes/upstreamsapi/v1_test.go +++ b/internal/routes/apiroutes/upstreamsapi/v1_test.go @@ -83,22 +83,16 @@ func TestUpstreamsAPI_List_EmptyConfiguration(t *testing.T) { func TestUpstreamsAPI_List_WithUpstreams(t *testing.T) { config := newTestConfig(t, map[string]configuration.UpstreamConfiguration{ "dockerio": { - Registry: "https://registry-1.docker.io", - Authentication: configuration.UpstreamAuthentication{ - Anonymous: &configuration.UpstreamAnonymousAuthentication{}, - }, + Registry: "https://registry-1.docker.io", + Authentication: configuration.UpstreamAuthentication{}, }, "gcr": { - Registry: "https://gcr.io", - Authentication: configuration.UpstreamAuthentication{ - Anonymous: &configuration.UpstreamAnonymousAuthentication{}, - }, + Registry: "https://gcr.io", + Authentication: configuration.UpstreamAuthentication{}, }, "quay": { - Registry: "https://quay.io", - Authentication: configuration.UpstreamAuthentication{ - Anonymous: &configuration.UpstreamAnonymousAuthentication{}, - }, + Registry: "https://quay.io", + Authentication: configuration.UpstreamAuthentication{}, }, }) @@ -140,19 +134,12 @@ func TestUpstreamsAPI_List_WithUpstreams(t *testing.T) { func TestUpstreamsAPI_Get_ExistingUpstream(t *testing.T) { config := newTestConfig(t, map[string]configuration.UpstreamConfiguration{ "dockerio": { - Registry: "https://registry-1.docker.io", - Authentication: configuration.UpstreamAuthentication{ - Anonymous: &configuration.UpstreamAnonymousAuthentication{}, - }, + Registry: "https://registry-1.docker.io", + Authentication: configuration.UpstreamAuthentication{}, }, "gcr": { - Registry: "https://gcr.io", - Authentication: configuration.UpstreamAuthentication{ - Basic: &configuration.UpstreamBasicAuthentication{ - Username: "user", - Password: "pass", - }, - }, + Registry: "https://gcr.io", + Authentication: configuration.UpstreamAuthentication{}, }, }) @@ -175,7 +162,7 @@ func TestUpstreamsAPI_Get_ExistingUpstream(t *testing.T) { name: "get gcr", alias: "gcr", wantRegistry: "https://gcr.io", - wantAuthType: "basic", + wantAuthType: "anonymous", }, } @@ -213,10 +200,8 @@ func TestUpstreamsAPI_Get_ExistingUpstream(t *testing.T) { func TestUpstreamsAPI_Get_UnknownUpstream(t *testing.T) { config := newTestConfig(t, map[string]configuration.UpstreamConfiguration{ "dockerio": { - Registry: "https://registry-1.docker.io", - Authentication: configuration.UpstreamAuthentication{ - Anonymous: &configuration.UpstreamAnonymousAuthentication{}, - }, + Registry: "https://registry-1.docker.io", + Authentication: configuration.UpstreamAuthentication{}, }, }) @@ -270,10 +255,8 @@ func TestUpstreamsAPI_Get_EmptyConfiguration(t *testing.T) { func TestUpstreamsAPI_MethodNotAllowed(t *testing.T) { config := newTestConfig(t, map[string]configuration.UpstreamConfiguration{ "dockerio": { - Registry: "https://registry-1.docker.io", - Authentication: configuration.UpstreamAuthentication{ - Anonymous: &configuration.UpstreamAnonymousAuthentication{}, - }, + Registry: "https://registry-1.docker.io", + Authentication: configuration.UpstreamAuthentication{}, }, }) diff --git a/pkg/configuration/auth.go b/pkg/configuration/auth.go new file mode 100644 index 0000000..446c9bd --- /dev/null +++ b/pkg/configuration/auth.go @@ -0,0 +1,107 @@ +package configuration + +import ( + "errors" + "fmt" + + "go.uber.org/zap" +) + +// AuthType is implemented by all authentication mode configurations. +type AuthType interface { + GetName() string +} + +// ErrMultipleAuthTypes is returned when multiple authentication types are configured simultaneously. +var ErrMultipleAuthTypes = errors.New("multiple authentication types configured") + +// ErrInvalidAuthConfiguration is returned when an authentication configuration is invalid. +var ErrInvalidAuthConfiguration = errors.New("invalid authentication configuration") + +// UpstreamAuthentication defines the authentication configuration for an upstream registry. +// Each upstream is configured for exactly one auth mode. Only one field should be set at a time; +// mutual exclusivity is enforced at validation. +type UpstreamAuthentication struct { + // Anonymous configures unauthenticated access to the upstream registry. + Anonymous *AnonymousAuthentication `koanf:"anonymous"` + + // Passthrough configures barnacle to forward client-provided credentials to the upstream registry. + Passthrough *PassthroughAuthentication `koanf:"passthrough"` +} + +// Validate checks that the authentication configuration is valid. +// At most one auth mode may be set. If none are set, a warning is logged +// and behavior defaults to anonymous. +func (u *UpstreamAuthentication) Validate(logger *zap.Logger) error { + count := 0 + + if u.Anonymous != nil { + count++ + } + + if u.Passthrough != nil { + count++ + } + + if count > 1 { + return fmt.Errorf("%w: only one authentication mode may be configured per upstream", ErrMultipleAuthTypes) + } + + if count == 0 { + logger.Warn( + "no authentication mode configured, defaulting to anonymous — set anonymous auth explicitly to suppress this warning", + ) + return nil + } + + if u.Anonymous != nil { + return u.Anonymous.Validate() + } + + if u.Passthrough != nil { + return u.Passthrough.Validate() + } + + return nil +} + +// GetAuthType returns the configured authentication mode. +// If no mode is explicitly configured, it defaults to anonymous. +func (u *UpstreamAuthentication) GetAuthType() AuthType { + if u.Passthrough != nil { + return u.Passthrough + } + + if u.Anonymous != nil { + return u.Anonymous + } + + return &AnonymousAuthentication{} +} + +// AnonymousAuthentication configures unauthenticated access to the upstream registry. +type AnonymousAuthentication struct{} + +// GetName returns the name of the anonymous authentication type. +func (a *AnonymousAuthentication) GetName() string { + return "anonymous" +} + +// Validate checks that the anonymous authentication configuration is valid. +func (a *AnonymousAuthentication) Validate() error { + return nil +} + +// PassthroughAuthentication configures barnacle to forward client-provided credentials +// directly to the upstream registry without inspection or caching. +type PassthroughAuthentication struct{} + +// GetName returns the name of the passthrough authentication type. +func (p *PassthroughAuthentication) GetName() string { + return "passthrough" +} + +// Validate checks that the passthrough authentication configuration is valid. +func (p *PassthroughAuthentication) Validate() error { + return nil +} diff --git a/pkg/configuration/auth_test.go b/pkg/configuration/auth_test.go new file mode 100644 index 0000000..3af2dee --- /dev/null +++ b/pkg/configuration/auth_test.go @@ -0,0 +1,58 @@ +package configuration_test + +import ( + "testing" + + "github.com/pdylanross/barnacle/pkg/configuration" +) + +func TestAnonymousAuthentication_GetName(t *testing.T) { + auth := &configuration.AnonymousAuthentication{} + if got := auth.GetName(); got != "anonymous" { + t.Errorf("GetName() = %q, want %q", got, "anonymous") + } +} + +func TestPassthroughAuthentication_GetName(t *testing.T) { + auth := &configuration.PassthroughAuthentication{} + if got := auth.GetName(); got != "passthrough" { + t.Errorf("GetName() = %q, want %q", got, "passthrough") + } +} + +func TestUpstreamAuthentication_GetAuthType(t *testing.T) { + tests := []struct { + name string + auth configuration.UpstreamAuthentication + wantName string + }{ + { + name: "no auth configured defaults to anonymous", + auth: configuration.UpstreamAuthentication{}, + wantName: "anonymous", + }, + { + name: "anonymous explicitly set", + auth: configuration.UpstreamAuthentication{ + Anonymous: &configuration.AnonymousAuthentication{}, + }, + wantName: "anonymous", + }, + { + name: "passthrough explicitly set", + auth: configuration.UpstreamAuthentication{ + Passthrough: &configuration.PassthroughAuthentication{}, + }, + wantName: "passthrough", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.auth.GetAuthType() + if got.GetName() != tt.wantName { + t.Errorf("GetAuthType().GetName() = %q, want %q", got.GetName(), tt.wantName) + } + }) + } +} diff --git a/pkg/configuration/configuration.go b/pkg/configuration/configuration.go index a97807c..768c3c1 100644 --- a/pkg/configuration/configuration.go +++ b/pkg/configuration/configuration.go @@ -5,6 +5,8 @@ import ( "fmt" "net/http" "time" + + "go.uber.org/zap" ) // ErrInvalidPort is returned when the server port is invalid. @@ -75,7 +77,7 @@ type Configuration struct { // Validate checks that the configuration is valid. // Returns an error if any configuration value is invalid. -func (c *Configuration) Validate() error { +func (c *Configuration) Validate(logger *zap.Logger) error { if err := c.Server.Validate(); err != nil { return err } @@ -93,7 +95,7 @@ func (c *Configuration) Validate() error { if alias == "" { return fmt.Errorf("%w: upstream alias cannot be empty", ErrInvalidConfiguration) } - if err := upstream.Validate(); err != nil { + if err := upstream.Validate(logger); err != nil { return fmt.Errorf("upstream %q: %w", alias, err) } } diff --git a/pkg/configuration/configuration_test.go b/pkg/configuration/configuration_test.go index 6be2574..a1ebfae 100644 --- a/pkg/configuration/configuration_test.go +++ b/pkg/configuration/configuration_test.go @@ -9,6 +9,7 @@ import ( "github.com/knadh/koanf/v2" "github.com/pdylanross/barnacle/pkg/configuration" + testutils "github.com/pdylanross/barnacle/test" ) func TestConfiguration_EmptyYAML(t *testing.T) { @@ -361,9 +362,6 @@ func TestConfiguration_Validate(t *testing.T) { Upstreams: map[string]configuration.UpstreamConfiguration{ "docker.io": { Registry: "https://registry-1.docker.io", - Authentication: configuration.UpstreamAuthentication{ - Anonymous: &configuration.UpstreamAnonymousAuthentication{}, - }, }, }, }, @@ -380,18 +378,9 @@ func TestConfiguration_Validate(t *testing.T) { Upstreams: map[string]configuration.UpstreamConfiguration{ "docker.io": { Registry: "https://registry-1.docker.io", - Authentication: configuration.UpstreamAuthentication{ - Anonymous: &configuration.UpstreamAnonymousAuthentication{}, - }, }, "gcr.io": { Registry: "https://gcr.io", - Authentication: configuration.UpstreamAuthentication{ - Basic: &configuration.UpstreamBasicAuthentication{ - Username: "user", - Password: "pass", - }, - }, }, }, }, @@ -408,9 +397,6 @@ func TestConfiguration_Validate(t *testing.T) { Upstreams: map[string]configuration.UpstreamConfiguration{ "": { Registry: "https://registry-1.docker.io", - Authentication: configuration.UpstreamAuthentication{ - Anonymous: &configuration.UpstreamAnonymousAuthentication{}, - }, }, }, }, @@ -428,32 +414,6 @@ func TestConfiguration_Validate(t *testing.T) { Upstreams: map[string]configuration.UpstreamConfiguration{ "docker.io": { Registry: "", - Authentication: configuration.UpstreamAuthentication{ - Anonymous: &configuration.UpstreamAnonymousAuthentication{}, - }, - }, - }, - }, - wantErr: true, - errType: configuration.ErrInvalidAuthConfiguration, - }, - { - name: "invalid upstream authentication", - config: configuration.Configuration{ - Server: configuration.ServerConfiguration{ - Port: 8080, - }, - Redis: validRedis, - Cache: validCache, - Upstreams: map[string]configuration.UpstreamConfiguration{ - "docker.io": { - Registry: "https://registry-1.docker.io", - Authentication: configuration.UpstreamAuthentication{ - Basic: &configuration.UpstreamBasicAuthentication{ - Username: "", - Password: "pass", - }, - }, }, }, }, @@ -462,9 +422,11 @@ func TestConfiguration_Validate(t *testing.T) { }, } + logger := testutils.CreateTestLogger(t) + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.config.Validate() + err := tt.config.Validate(logger) if tt.wantErr && err == nil { t.Error("expected error, got nil") diff --git a/pkg/configuration/upstream.go b/pkg/configuration/upstream.go index 64ac8b6..e89c2ab 100644 --- a/pkg/configuration/upstream.go +++ b/pkg/configuration/upstream.go @@ -1,15 +1,10 @@ package configuration import ( - "errors" "fmt" -) - -// ErrMultipleAuthTypes is returned when multiple authentication types are configured simultaneously. -var ErrMultipleAuthTypes = errors.New("multiple authentication types configured") -// ErrInvalidAuthConfiguration is returned when an authentication configuration is invalid. -var ErrInvalidAuthConfiguration = errors.New("invalid authentication configuration") + "go.uber.org/zap" +) // UpstreamConfiguration defines the configuration for an upstream container registry. // It specifies how to connect to and authenticate with the upstream registry. @@ -24,111 +19,10 @@ type UpstreamConfiguration struct { // Validate checks that the upstream configuration is valid. // Returns an error if the configuration is invalid. -func (u *UpstreamConfiguration) Validate() error { +func (u *UpstreamConfiguration) Validate(logger *zap.Logger) error { if u.Registry == "" { return fmt.Errorf("%w: registry cannot be empty", ErrInvalidAuthConfiguration) } - return u.Authentication.Validate() -} - -// UpstreamAuthentication defines the authentication configuration for an upstream registry. -// Only one authentication type can be set at a time. If none are set, anonymous authentication is used. -type UpstreamAuthentication struct { - // Anonymous indicates anonymous (unauthenticated) access to the registry. - Anonymous *UpstreamAnonymousAuthentication `koanf:"anonymous"` - - // Basic specifies HTTP Basic authentication credentials. - Basic *UpstreamBasicAuthentication `koanf:"basic"` - - // Bearer specifies bearer token authentication. - Bearer *UpstreamBearerAuthentication `koanf:"bearer"` -} - -// Validate ensures that only one authentication type is configured. -// If no authentication is configured, it defaults to anonymous. -// Returns an error if multiple authentication types are set or if the configured type is invalid. -func (u *UpstreamAuthentication) Validate() error { - setCount := 0 - - if u.Anonymous != nil { - setCount++ - } - if u.Basic != nil { - setCount++ - } - if u.Bearer != nil { - setCount++ - } - - if setCount > 1 { - return fmt.Errorf("%w: only one authentication type can be set", ErrMultipleAuthTypes) - } - - // Default to anonymous if nothing is set - if setCount == 0 { - u.Anonymous = &UpstreamAnonymousAuthentication{} - } - - // Validate the specific authentication type - if u.Anonymous != nil { - return u.Anonymous.Validate() - } - if u.Basic != nil { - return u.Basic.Validate() - } - if u.Bearer != nil { - return u.Bearer.Validate() - } - - return nil -} - -// UpstreamAnonymousAuthentication represents anonymous (unauthenticated) access to a registry. -// This is used when no credentials are required. -type UpstreamAnonymousAuthentication struct{} - -// Validate checks that the anonymous authentication configuration is valid. -// Anonymous authentication has no fields to validate, so this always returns nil. -func (u *UpstreamAnonymousAuthentication) Validate() error { - return nil -} - -// UpstreamBasicAuthentication represents HTTP Basic authentication credentials. -type UpstreamBasicAuthentication struct { - // Username is the username for basic authentication. - Username string `koanf:"username"` - - // Password is the password for basic authentication. - Password string `koanf:"password"` -} - -// Validate checks that the basic authentication configuration is valid. -// Returns an error if username or password is empty. -func (u *UpstreamBasicAuthentication) Validate() error { - if u.Username == "" { - return fmt.Errorf("%w: basic auth username cannot be empty", ErrInvalidAuthConfiguration) - } - - if u.Password == "" { - return fmt.Errorf("%w: basic auth password cannot be empty", ErrInvalidAuthConfiguration) - } - - return nil -} - -// UpstreamBearerAuthentication represents bearer token authentication. -type UpstreamBearerAuthentication struct { - // Token is the bearer token to use for authentication. - Token string `koanf:"token"` -} - -// Validate checks that the bearer authentication configuration is valid. -// Returns an error if the token is empty. -func (u *UpstreamBearerAuthentication) Validate() error { - if u.Token == "" { - return fmt.Errorf("%w: bearer token cannot be empty", ErrInvalidAuthConfiguration) - } - - return nil + return u.Authentication.Validate(logger) } diff --git a/pkg/configuration/upstream_test.go b/pkg/configuration/upstream_test.go index c95b784..7d2aa8e 100644 --- a/pkg/configuration/upstream_test.go +++ b/pkg/configuration/upstream_test.go @@ -5,9 +5,12 @@ import ( "testing" "github.com/pdylanross/barnacle/pkg/configuration" + testutils "github.com/pdylanross/barnacle/test" ) func TestUpstreamConfiguration_Validate(t *testing.T) { + logger := testutils.CreateTestLogger(t) + tests := []struct { name string config configuration.UpstreamConfiguration @@ -15,37 +18,9 @@ func TestUpstreamConfiguration_Validate(t *testing.T) { errType error }{ { - name: "valid configuration with anonymous auth", - config: configuration.UpstreamConfiguration{ - Registry: "https://registry-1.docker.io", - Authentication: configuration.UpstreamAuthentication{ - Anonymous: &configuration.UpstreamAnonymousAuthentication{}, - }, - }, - wantErr: false, - }, - { - name: "valid configuration with basic auth", + name: "valid configuration", config: configuration.UpstreamConfiguration{ Registry: "https://registry-1.docker.io", - Authentication: configuration.UpstreamAuthentication{ - Basic: &configuration.UpstreamBasicAuthentication{ - Username: "user", - Password: "pass", - }, - }, - }, - wantErr: false, - }, - { - name: "valid configuration with bearer auth", - config: configuration.UpstreamConfiguration{ - Registry: "https://registry-1.docker.io", - Authentication: configuration.UpstreamAuthentication{ - Bearer: &configuration.UpstreamBearerAuthentication{ - Token: "token123", - }, - }, }, wantErr: false, }, @@ -53,23 +28,6 @@ func TestUpstreamConfiguration_Validate(t *testing.T) { name: "empty registry", config: configuration.UpstreamConfiguration{ Registry: "", - Authentication: configuration.UpstreamAuthentication{ - Anonymous: &configuration.UpstreamAnonymousAuthentication{}, - }, - }, - wantErr: true, - errType: configuration.ErrInvalidAuthConfiguration, - }, - { - name: "invalid authentication", - config: configuration.UpstreamConfiguration{ - Registry: "https://registry-1.docker.io", - Authentication: configuration.UpstreamAuthentication{ - Basic: &configuration.UpstreamBasicAuthentication{ - Username: "", - Password: "pass", - }, - }, }, wantErr: true, errType: configuration.ErrInvalidAuthConfiguration, @@ -78,7 +36,7 @@ func TestUpstreamConfiguration_Validate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.config.Validate() + err := tt.config.Validate(logger) if tt.wantErr && err == nil { t.Error("expected error, got nil") @@ -96,6 +54,8 @@ func TestUpstreamConfiguration_Validate(t *testing.T) { } func TestUpstreamAuthentication_Validate(t *testing.T) { + logger := testutils.CreateTestLogger(t) + tests := []struct { name string auth configuration.UpstreamAuthentication @@ -103,191 +63,38 @@ func TestUpstreamAuthentication_Validate(t *testing.T) { errType error }{ { - name: "only anonymous set", - auth: configuration.UpstreamAuthentication{ - Anonymous: &configuration.UpstreamAnonymousAuthentication{}, - }, + name: "no auth mode set defaults to anonymous", + auth: configuration.UpstreamAuthentication{}, wantErr: false, }, { - name: "only basic set - valid", + name: "anonymous explicitly set", auth: configuration.UpstreamAuthentication{ - Basic: &configuration.UpstreamBasicAuthentication{ - Username: "user", - Password: "pass", - }, + Anonymous: &configuration.AnonymousAuthentication{}, }, wantErr: false, }, { - name: "only bearer set - valid", + name: "passthrough explicitly set", auth: configuration.UpstreamAuthentication{ - Bearer: &configuration.UpstreamBearerAuthentication{ - Token: "token123", - }, + Passthrough: &configuration.PassthroughAuthentication{}, }, wantErr: false, }, { - name: "no auth set - defaults to anonymous", - auth: configuration.UpstreamAuthentication{}, - wantErr: false, - }, - { - name: "multiple auth types - anonymous and basic", + name: "multiple auth modes set", auth: configuration.UpstreamAuthentication{ - Anonymous: &configuration.UpstreamAnonymousAuthentication{}, - Basic: &configuration.UpstreamBasicAuthentication{ - Username: "user", - Password: "pass", - }, + Anonymous: &configuration.AnonymousAuthentication{}, + Passthrough: &configuration.PassthroughAuthentication{}, }, wantErr: true, errType: configuration.ErrMultipleAuthTypes, }, - { - name: "multiple auth types - anonymous and bearer", - auth: configuration.UpstreamAuthentication{ - Anonymous: &configuration.UpstreamAnonymousAuthentication{}, - Bearer: &configuration.UpstreamBearerAuthentication{ - Token: "token123", - }, - }, - wantErr: true, - errType: configuration.ErrMultipleAuthTypes, - }, - { - name: "multiple auth types - basic and bearer", - auth: configuration.UpstreamAuthentication{ - Basic: &configuration.UpstreamBasicAuthentication{ - Username: "user", - Password: "pass", - }, - Bearer: &configuration.UpstreamBearerAuthentication{ - Token: "token123", - }, - }, - wantErr: true, - errType: configuration.ErrMultipleAuthTypes, - }, - { - name: "all auth types set", - auth: configuration.UpstreamAuthentication{ - Anonymous: &configuration.UpstreamAnonymousAuthentication{}, - Basic: &configuration.UpstreamBasicAuthentication{ - Username: "user", - Password: "pass", - }, - Bearer: &configuration.UpstreamBearerAuthentication{ - Token: "token123", - }, - }, - wantErr: true, - errType: configuration.ErrMultipleAuthTypes, - }, - { - name: "invalid basic auth", - auth: configuration.UpstreamAuthentication{ - Basic: &configuration.UpstreamBasicAuthentication{ - Username: "", - Password: "pass", - }, - }, - wantErr: true, - errType: configuration.ErrInvalidAuthConfiguration, - }, - { - name: "invalid bearer auth", - auth: configuration.UpstreamAuthentication{ - Bearer: &configuration.UpstreamBearerAuthentication{ - Token: "", - }, - }, - wantErr: true, - errType: configuration.ErrInvalidAuthConfiguration, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.auth.Validate() - - if tt.wantErr && err == nil { - t.Error("expected error, got nil") - } - - if !tt.wantErr && err != nil { - t.Errorf("expected no error, got %v", err) - } - - if tt.wantErr && tt.errType != nil && !errors.Is(err, tt.errType) { - t.Errorf("expected error type %v, got %v", tt.errType, err) - } - - // Verify that no auth defaults to anonymous - if tt.name == "no auth set - defaults to anonymous" && tt.auth.Anonymous == nil { - t.Error("expected Anonymous to be set when no auth is configured") - } - }) - } -} - -func TestUpstreamAnonymousAuthentication_Validate(t *testing.T) { - auth := configuration.UpstreamAnonymousAuthentication{} - err := auth.Validate() - - if err != nil { - t.Errorf("expected no error for anonymous auth, got %v", err) - } -} - -func TestUpstreamBasicAuthentication_Validate(t *testing.T) { - tests := []struct { - name string - auth configuration.UpstreamBasicAuthentication - wantErr bool - errType error - }{ - { - name: "valid credentials", - auth: configuration.UpstreamBasicAuthentication{ - Username: "user", - Password: "pass", - }, - wantErr: false, - }, - { - name: "empty username", - auth: configuration.UpstreamBasicAuthentication{ - Username: "", - Password: "pass", - }, - wantErr: true, - errType: configuration.ErrInvalidAuthConfiguration, - }, - { - name: "empty password", - auth: configuration.UpstreamBasicAuthentication{ - Username: "user", - Password: "", - }, - wantErr: true, - errType: configuration.ErrInvalidAuthConfiguration, - }, - { - name: "both empty", - auth: configuration.UpstreamBasicAuthentication{ - Username: "", - Password: "", - }, - wantErr: true, - errType: configuration.ErrInvalidAuthConfiguration, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.auth.Validate() + err := tt.auth.Validate(logger) if tt.wantErr && err == nil { t.Error("expected error, got nil") @@ -303,63 +110,3 @@ func TestUpstreamBasicAuthentication_Validate(t *testing.T) { }) } } - -func TestUpstreamBearerAuthentication_Validate(t *testing.T) { - tests := []struct { - name string - auth configuration.UpstreamBearerAuthentication - wantErr bool - errType error - }{ - { - name: "valid token", - auth: configuration.UpstreamBearerAuthentication{ - Token: "token123", - }, - wantErr: false, - }, - { - name: "empty token", - auth: configuration.UpstreamBearerAuthentication{ - Token: "", - }, - wantErr: true, - errType: configuration.ErrInvalidAuthConfiguration, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.auth.Validate() - - if tt.wantErr && err == nil { - t.Error("expected error, got nil") - } - - if !tt.wantErr && err != nil { - t.Errorf("expected no error, got %v", err) - } - - if tt.wantErr && tt.errType != nil && !errors.Is(err, tt.errType) { - t.Errorf("expected error type %v, got %v", tt.errType, err) - } - }) - } -} - -func TestUpstreamAuthentication_DefaultsToAnonymous(t *testing.T) { - auth := configuration.UpstreamAuthentication{} - - if auth.Anonymous != nil { - t.Error("expected Anonymous to be nil before validation") - } - - err := auth.Validate() - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - - if auth.Anonymous == nil { - t.Error("expected Anonymous to be set after validation") - } -}