From 6ee63997e6a20e6ea897d1230ad41cba31982fd4 Mon Sep 17 00:00:00 2001 From: "Brett V. Forsgren" Date: Wed, 10 Jun 2026 12:17:09 -0600 Subject: [PATCH 1/3] Add host and username/password support to cargo registry handler The cargo handler now accepts 'host' as an alternative to 'url' for matching requests, and 'username'/'password' as an alternative to 'token' for authentication (using basic auth). Token takes precedence when both are present. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- internal/handlers/cargo_registry.go | 52 +++++++--- internal/handlers/cargo_registry_test.go | 118 +++++++++++++++++++++++ 2 files changed, 158 insertions(+), 12 deletions(-) diff --git a/internal/handlers/cargo_registry.go b/internal/handlers/cargo_registry.go index 2795464..b62c0c6 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,33 @@ 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, + host: host, + 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 == "" { + 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 for registry (url: %s, host: %s)", cargoCred.url, cargoCred.host) continue } + handler.credentials = append(handler.credentials, cargoCred) } return &handler @@ -96,15 +116,23 @@ 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 !helpers.UrlMatchesRequest(req, cred.url, true) && !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..7b3b719 100644 --- a/internal/handlers/cargo_registry_test.go +++ b/internal/handlers/cargo_registry_test.go @@ -96,3 +96,121 @@ 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") +} From 50252a02b3a5d6e4fa2c32fea0c72072bf1c0c23 Mon Sep 17 00:00:00 2001 From: "Brett V. Forsgren" Date: Wed, 10 Jun 2026 12:43:50 -0600 Subject: [PATCH 2/3] Address review: scope host matching only when url is empty - Only use host-based matching when url is not set, preventing credential leakage to unrelated paths on the same host - Update warning message to mention both token and password - Add regression test for url+host scoping behavior Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- internal/handlers/cargo_registry.go | 8 ++++++-- internal/handlers/cargo_registry_test.go | 25 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/internal/handlers/cargo_registry.go b/internal/handlers/cargo_registry.go index b62c0c6..456945b 100644 --- a/internal/handlers/cargo_registry.go +++ b/internal/handlers/cargo_registry.go @@ -95,7 +95,7 @@ func NewCargoRegistryHandler(credentials config.Credentials) *CargoRegistryHandl } if token == "" && password == "" { - logrus.Warnf("missing token for registry (url: %s, host: %s)", cargoCred.url, cargoCred.host) + logrus.Warnf("missing token or password for registry (url: %s, host: %s)", cargoCred.url, cargoCred.host) continue } @@ -116,7 +116,11 @@ 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) && !helpers.CheckHost(req, cred.host) { + if cred.url != "" { + if !helpers.UrlMatchesRequest(req, cred.url, true) { + continue + } + } else if !helpers.CheckHost(req, cred.host) { continue } diff --git a/internal/handlers/cargo_registry_test.go b/internal/handlers/cargo_registry_test.go index 7b3b719..cd81ac1 100644 --- a/internal/handlers/cargo_registry_test.go +++ b/internal/handlers/cargo_registry_test.go @@ -214,3 +214,28 @@ func TestCargoRegistryHandlerIgnoresNoUrlOrHost(t *testing.T) { 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/registry", + "host": "cargo.example.com", + "token": token, + }, + } + + handler := NewCargoRegistryHandler(credentials) + + // matching url path should authenticate + req := httptest.NewRequest("GET", "https://cargo.example.com/registry/crate", nil) + req = handleRequestAndClose(handler, req, nil) + assertHasTokenAuth(t, req, "", token, "url-matched request") + + // different path on same host should NOT authenticate (url scoping takes precedence) + req = httptest.NewRequest("GET", "https://cargo.example.com/other/path", nil) + req = handleRequestAndClose(handler, req, nil) + assertUnauthenticated(t, req, "different path on same host should not be authenticated when url is set") +} From 56dc1e0c061a1b80c87757d1fa406b2978d7f913 Mon Sep 17 00:00:00 2001 From: "Brett V. Forsgren" Date: Wed, 10 Jun 2026 13:15:33 -0600 Subject: [PATCH 3/3] Apply review suggestions: tighten host scoping and improve messages - Only assign host to credential struct when url is empty (clearer intent) - Update warning to say 'missing token or username/password' - Use more realistic org-scoped paths in regression test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- internal/handlers/cargo_registry.go | 9 ++++++--- internal/handlers/cargo_registry_test.go | 14 +++++++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/internal/handlers/cargo_registry.go b/internal/handlers/cargo_registry.go index 456945b..35f0f7f 100644 --- a/internal/handlers/cargo_registry.go +++ b/internal/handlers/cargo_registry.go @@ -78,7 +78,6 @@ func NewCargoRegistryHandler(credentials config.Credentials) *CargoRegistryHandl cargoCred := cargoRepositoryCredentials{ url: url, - host: host, token: token, username: username, password: password, @@ -89,13 +88,17 @@ func NewCargoRegistryHandler(credentials config.Credentials) *CargoRegistryHandl logrus.Warnf("ignoring invalid registry url (%s): %v", cargoCred.url, err) continue } - } else if host == "" { + } 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 token == "" && password == "" { - logrus.Warnf("missing token or password for registry (url: %s, host: %s)", cargoCred.url, cargoCred.host) + logrus.Warnf("missing token or username/password for registry (url: %s, host: %s)", cargoCred.url, cargoCred.host) continue } diff --git a/internal/handlers/cargo_registry_test.go b/internal/handlers/cargo_registry_test.go index cd81ac1..9fd9c7c 100644 --- a/internal/handlers/cargo_registry_test.go +++ b/internal/handlers/cargo_registry_test.go @@ -221,7 +221,7 @@ func TestCargoRegistryHandlerUrlScopingNotBypassedByHost(t *testing.T) { credentials := config.Credentials{ config.Credential{ "type": "cargo_registry", - "url": "https://cargo.example.com/registry", + "url": "https://cargo.example.com/myorg", "host": "cargo.example.com", "token": token, }, @@ -229,13 +229,13 @@ func TestCargoRegistryHandlerUrlScopingNotBypassedByHost(t *testing.T) { handler := NewCargoRegistryHandler(credentials) - // matching url path should authenticate - req := httptest.NewRequest("GET", "https://cargo.example.com/registry/crate", nil) + // 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, "url-matched request") + assertHasTokenAuth(t, req, "", token, "in-scope path request") - // different path on same host should NOT authenticate (url scoping takes precedence) - req = httptest.NewRequest("GET", "https://cargo.example.com/other/path", nil) + // 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, "different path on same host should not be authenticated when url is set") + assertUnauthenticated(t, req, "host must not bypass url path scoping") }