diff --git a/internal/handlers/cargo_registry.go b/internal/handlers/cargo_registry.go index 2795464..35f0f7f 100644 --- a/internal/handlers/cargo_registry.go +++ b/internal/handlers/cargo_registry.go @@ -2,6 +2,7 @@ package handlers import ( "net/http" + "strings" "github.com/elazarl/goproxy" "github.com/sirupsen/logrus" @@ -39,8 +40,11 @@ type CargoRegistryHandler struct { } type cargoRepositoryCredentials struct { - url string - authorization string + url string + host string + token string + username string + password string } func NewCargoRegistryHandler(credentials config.Credentials) *CargoRegistryHandler { @@ -55,6 +59,7 @@ func NewCargoRegistryHandler(credentials config.Credentials) *CargoRegistryHandl } url := credential.GetString("url") + host := strings.ToLower(credential.GetString("host")) // Cargo credentials must remain URL-scoped; do not allow OIDC // registration to fall back to host-only matching when url is empty. @@ -67,18 +72,36 @@ func NewCargoRegistryHandler(credentials config.Credentials) *CargoRegistryHandl continue } + token := credential.GetString("token") + username := credential.GetString("username") + password := credential.GetString("password") + cargoCred := cargoRepositoryCredentials{ - url: url, - authorization: credential.GetString("token"), + url: url, + token: token, + username: username, + password: password, } - if _, err := helpers.ParseURLLax(cargoCred.url); err != nil { - logrus.Warnf("ignoring invalid registry url (%s): %v", cargoCred.url, err) + + if url != "" { + if _, err := helpers.ParseURLLax(cargoCred.url); err != nil { + logrus.Warnf("ignoring invalid registry url (%s): %v", cargoCred.url, err) + continue + } + } else if host != "" { + // Only set host when url is empty so URL/path scoping always + // takes precedence and never falls back to host-only matching. + cargoCred.host = host + } else { + logrus.Warn("ignoring cargo_registry credential with no url or host") continue } - if cargoCred.authorization == "" { - logrus.Warnf("missing token for registry url (%s)", cargoCred.url) + + if token == "" && password == "" { + logrus.Warnf("missing token or username/password for registry (url: %s, host: %s)", cargoCred.url, cargoCred.host) continue } + handler.credentials = append(handler.credentials, cargoCred) } return &handler @@ -96,15 +119,27 @@ func (h *CargoRegistryHandler) HandleRequest(req *http.Request, ctx *goproxy.Pro // Fall back to static credentials for _, cred := range h.credentials { - if !helpers.UrlMatchesRequest(req, cred.url, true) { + if cred.url != "" { + if !helpers.UrlMatchesRequest(req, cred.url, true) { + continue + } + } else if !helpers.CheckHost(req, cred.host) { continue } - logging.RequestLogf(ctx, "* authenticating cargo registry request (url: %s)", cred.url) - helpers.SetRawAuthorization(req, cred.authorization) - + authenticateCargoRequest(req, cred, ctx) return req, nil } return req, nil } + +func authenticateCargoRequest(req *http.Request, cred cargoRepositoryCredentials, ctx *goproxy.ProxyCtx) { + if cred.token != "" { + logging.RequestLogf(ctx, "* authenticating cargo registry request (url: %s, host: %s, token auth)", cred.url, cred.host) + helpers.SetRawAuthorization(req, cred.token) + } else if cred.password != "" { + logging.RequestLogf(ctx, "* authenticating cargo registry request (url: %s, host: %s, basic auth)", cred.url, cred.host) + helpers.SetBasicAuthorization(req, cred.username, cred.password) + } +} diff --git a/internal/handlers/cargo_registry_test.go b/internal/handlers/cargo_registry_test.go index e4a0d75..9fd9c7c 100644 --- a/internal/handlers/cargo_registry_test.go +++ b/internal/handlers/cargo_registry_test.go @@ -96,3 +96,146 @@ func TestCargoRegistryHandler(t *testing.T) { req = handleRequestAndClose(handler, req, nil) assertUnauthenticated(t, req, "non-GET request") } + +func TestCargoRegistryHandlerWithHost(t *testing.T) { + token := "Bearer abc123" //nolint:gosec // test credential + + credentials := config.Credentials{ + config.Credential{ + "type": "cargo_registry", + "host": "cargo.example.com", + "token": token, + }, + } + + handler := NewCargoRegistryHandler(credentials) + + // matching host should authenticate + req := httptest.NewRequest("GET", "https://cargo.example.com/some/path", nil) + req = handleRequestAndClose(handler, req, nil) + assertHasTokenAuth(t, req, "", token, "host-matched request") + + // non-matching host should not authenticate + req = httptest.NewRequest("GET", "https://other.example.com/some/path", nil) + req = handleRequestAndClose(handler, req, nil) + assertUnauthenticated(t, req, "non-matching host request") + + // HTTP should not authenticate + req = httptest.NewRequest("GET", "http://cargo.example.com/some/path", nil) + req = handleRequestAndClose(handler, req, nil) + assertUnauthenticated(t, req, "HTTP request to matching host") +} + +func TestCargoRegistryHandlerWithUsernamePassword(t *testing.T) { + username := "some-user" + password := "some-password" + + credentials := config.Credentials{ + config.Credential{ + "type": "cargo_registry", + "url": "https://cargo.example.com/registry", + "username": username, + "password": password, + }, + } + + handler := NewCargoRegistryHandler(credentials) + + // matching url should authenticate with basic auth + req := httptest.NewRequest("GET", "https://cargo.example.com/registry/crate", nil) + req = handleRequestAndClose(handler, req, nil) + assertHasBasicAuth(t, req, username, password, "basic auth via username/password") + + // non-matching url should not authenticate + req = httptest.NewRequest("GET", "https://other.example.com/registry/crate", nil) + req = handleRequestAndClose(handler, req, nil) + assertUnauthenticated(t, req, "non-matching url should not authenticate") +} + +func TestCargoRegistryHandlerWithHostAndUsernamePassword(t *testing.T) { + username := "some-user" + password := "some-password" + + credentials := config.Credentials{ + config.Credential{ + "type": "cargo_registry", + "host": "cargo.example.com", + "username": username, + "password": password, + }, + } + + handler := NewCargoRegistryHandler(credentials) + + // matching host should authenticate with basic auth + req := httptest.NewRequest("GET", "https://cargo.example.com/any/path", nil) + req = handleRequestAndClose(handler, req, nil) + assertHasBasicAuth(t, req, username, password, "host-matched basic auth") + + // non-matching host should not authenticate + req = httptest.NewRequest("GET", "https://other.example.com/any/path", nil) + req = handleRequestAndClose(handler, req, nil) + assertUnauthenticated(t, req, "non-matching host should not authenticate") +} + +func TestCargoRegistryHandlerTokenTakesPrecedenceOverPassword(t *testing.T) { + token := "Bearer abc123" //nolint:gosec // test credential + + credentials := config.Credentials{ + config.Credential{ + "type": "cargo_registry", + "url": "https://cargo.example.com/registry", + "token": token, + "username": "user", + "password": "pass", + }, + } + + handler := NewCargoRegistryHandler(credentials) + + // token should take precedence over username/password + req := httptest.NewRequest("GET", "https://cargo.example.com/registry/crate", nil) + req = handleRequestAndClose(handler, req, nil) + assertHasTokenAuth(t, req, "", token, "token takes precedence over password") +} + +func TestCargoRegistryHandlerIgnoresNoUrlOrHost(t *testing.T) { + credentials := config.Credentials{ + config.Credential{ + "type": "cargo_registry", + "token": "some-token", + }, + } + + handler := NewCargoRegistryHandler(credentials) + + // should not authenticate any request since no url or host was provided + req := httptest.NewRequest("GET", "https://anything.example.com/path", nil) + req = handleRequestAndClose(handler, req, nil) + assertUnauthenticated(t, req, "credential with no url or host should be ignored") +} + +func TestCargoRegistryHandlerUrlScopingNotBypassedByHost(t *testing.T) { + token := "Bearer abc123" //nolint:gosec // test credential + + credentials := config.Credentials{ + config.Credential{ + "type": "cargo_registry", + "url": "https://cargo.example.com/myorg", + "host": "cargo.example.com", + "token": token, + }, + } + + handler := NewCargoRegistryHandler(credentials) + + // in-scope path should authenticate + req := httptest.NewRequest("GET", "https://cargo.example.com/myorg/crate", nil) + req = handleRequestAndClose(handler, req, nil) + assertHasTokenAuth(t, req, "", token, "in-scope path request") + + // different path on the same host must NOT authenticate + req = httptest.NewRequest("GET", "https://cargo.example.com/otherorg/crate", nil) + req = handleRequestAndClose(handler, req, nil) + assertUnauthenticated(t, req, "host must not bypass url path scoping") +}