Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 47 additions & 12 deletions internal/handlers/cargo_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package handlers

import (
"net/http"
"strings"

"github.com/elazarl/goproxy"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -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 {
Expand All @@ -55,6 +59,7 @@ func NewCargoRegistryHandler(credentials config.Credentials) *CargoRegistryHandl
}

url := credential.GetString("url")
host := strings.ToLower(credential.GetString("host"))
Comment thread
kbukum1 marked this conversation as resolved.

// Cargo credentials must remain URL-scoped; do not allow OIDC
// registration to fall back to host-only matching when url is empty.
Expand All @@ -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
}
Comment thread
kbukum1 marked this conversation as resolved.
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
}
Comment thread
Copilot marked this conversation as resolved.

handler.credentials = append(handler.credentials, cargoCred)
}
return &handler
Expand All @@ -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
}
Comment on lines 121 to 128

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)
}
}
143 changes: 143 additions & 0 deletions internal/handlers/cargo_registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,146 @@ func TestCargoRegistryHandler(t *testing.T) {
req = handleRequestAndClose(handler, req, nil)
assertUnauthenticated(t, req, "non-GET request")
}

Comment thread
kbukum1 marked this conversation as resolved.
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")
}
Comment thread
kbukum1 marked this conversation as resolved.
Loading