From 26ee14641829b88d26a8392a0ac846fe7e54b022 Mon Sep 17 00:00:00 2001 From: Ivan Ilves Date: Thu, 5 Mar 2020 22:53:40 +0100 Subject: [PATCH] feat(auth): Set BASIC auth disregarding remote preferences --- .../registry/client/auth/basic/store/store.go | 83 +++++++++++++++++++ .../client/auth/basic/store/store_test.go | 55 ++++++++++++ api/v1/registry/client/auth/token.go | 36 +++++--- go.mod | 2 +- go.sum | 6 +- main.go | 6 ++ 6 files changed, 175 insertions(+), 13 deletions(-) create mode 100644 api/v1/registry/client/auth/basic/store/store.go create mode 100644 api/v1/registry/client/auth/basic/store/store_test.go diff --git a/api/v1/registry/client/auth/basic/store/store.go b/api/v1/registry/client/auth/basic/store/store.go new file mode 100644 index 0000000..241e13e --- /dev/null +++ b/api/v1/registry/client/auth/basic/store/store.go @@ -0,0 +1,83 @@ +package store + +import ( + "fmt" + "net/url" + "strings" +) + +// Store stores BASIC authentication credentials +type Store struct { + logins map[string]*Login +} + +// Login stores username and password for BASIC authentication +type Login struct { + Username string + Password string +} + +// LoadAll parses and loads a list of BASIC authentication strings +func (st *Store) LoadAll(aa []string) error { + logins := make(map[string]*Login, 0) + + for _, a := range aa { + registry, login, err := loadOne(strings.TrimSpace(a)) + + if err != nil { + return err + } + + logins[registry] = login + } + + st.logins = logins + + return nil +} + +// GetByHostname gets a BASIC auth login for a registry hostname passed +func (st *Store) GetByHostname(registryHostname string) *Login { + login, defined := st.logins[registryHostname] + if !defined { + return nil + } + + return login +} + +// GetByURL gets a BASIC auth login for a registry URL passed +func (st *Store) GetByURL(registryURL string) *Login { + u, _ := url.Parse(registryURL) + + return st.GetByHostname(u.Host) +} + +func loadOne(a string) (string, *Login, error) { + const format = "REGISTRY[:PORT] username:password" + + var formatErr = fmt.Errorf( + "invalid format for BASIC auth (should be: %s)", + format, + ) + + ss := strings.SplitN(a, " ", 2) + if len(ss) != 2 { + return "", nil, formatErr + } + + up := strings.SplitN(ss[1], ":", 2) + if len(up) != 2 { + return "", nil, formatErr + } + + registry := ss[0] + username := up[0] + password := up[1] + + if password == "" { + return "", nil, formatErr + } + + return registry, &Login{Username: username, Password: password}, nil +} diff --git a/api/v1/registry/client/auth/basic/store/store_test.go b/api/v1/registry/client/auth/basic/store/store_test.go new file mode 100644 index 0000000..25a6449 --- /dev/null +++ b/api/v1/registry/client/auth/basic/store/store_test.go @@ -0,0 +1,55 @@ +package store + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var examples = []string{"localhost:5000 foo:bar", "quay.io quser:qpass"} + +func TestLoadAllValid(t *testing.T) { + var store Store + + err := store.LoadAll(examples) + + assert.NoError(t, err) +} + +func TestLoadAllInvalid(t *testing.T) { + var store Store + + assert.Error(t, store.LoadAll([]string{""})) + assert.Error(t, store.LoadAll([]string{"us.gcr.io"})) + assert.Error(t, store.LoadAll([]string{"us.gcr.io forgotsomething"})) + assert.Error(t, store.LoadAll([]string{"quay.io quser:"})) + assert.Error(t, store.LoadAll([]string{" foo:bar"})) +} + +func TestGet(t *testing.T) { + var store Store + + store.LoadAll(examples) + + assert.NotNil(t, store.GetByHostname("localhost:5000")) + assert.NotNil(t, store.GetByHostname("quay.io")) + assert.Nil(t, store.GetByHostname("eu.gcr.io")) + + assert.NotNil(t, store.GetByURL("http://localhost:5000")) + assert.NotNil(t, store.GetByURL("https://quay.io")) + assert.Nil(t, store.GetByURL("https://eu.gcr.io")) +} + +func TestGetValues(t *testing.T) { + var store Store + + store.LoadAll(examples) + + login1 := store.GetByHostname("localhost:5000") + login2 := store.GetByHostname("quay.io") + + assert.Equal(t, login1.Username, "foo") + assert.Equal(t, login1.Password, "bar") + assert.Equal(t, login2.Username, "quser") + assert.Equal(t, login2.Password, "qpass") +} diff --git a/api/v1/registry/client/auth/token.go b/api/v1/registry/client/auth/token.go index 045df37..b8e9bbc 100644 --- a/api/v1/registry/client/auth/token.go +++ b/api/v1/registry/client/auth/token.go @@ -8,10 +8,14 @@ import ( log "github.com/sirupsen/logrus" "github.com/ivanilves/lstags/api/v1/registry/client/auth/basic" + basicstore "github.com/ivanilves/lstags/api/v1/registry/client/auth/basic/store" "github.com/ivanilves/lstags/api/v1/registry/client/auth/bearer" "github.com/ivanilves/lstags/api/v1/registry/client/auth/none" ) +// BasicStore stores explicitly set BASIC authorization headers +var BasicStore basicstore.Store + // Token is an abstraction for aggregated token-related information we get from authentication services type Token interface { Method() string @@ -58,18 +62,30 @@ func getAuthParams(h authHeader) map[string]string { // * detects authentication type ("Bearer", "Basic" or "None") // * delegates actual authentication to the type-specific implementation func NewToken(url, username, password, scope string) (Token, error) { - resp, err := http.Get(url) - if err != nil { - return nil, err - } + var method = "" + var params = make(map[string]string) - authHeader, err := extractAuthHeader(resp.Header["Www-Authenticate"]) - if err != nil { - return nil, err - } + storedBasicAuth := BasicStore.GetByURL(url) + + if storedBasicAuth == nil { + resp, err := http.Get(url) + if err != nil { + return nil, err + } - method := strings.ToLower(getAuthMethod(authHeader)) - params := getAuthParams(authHeader) + authHeader, err := extractAuthHeader(resp.Header["Www-Authenticate"]) + if err != nil { + return nil, err + } + + method = strings.ToLower(getAuthMethod(authHeader)) + params = getAuthParams(authHeader) + } else { + method = "basic" + + username = storedBasicAuth.Username + password = storedBasicAuth.Password + } switch method { case "none": diff --git a/go.mod b/go.mod index 67a7943..7f8b674 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/sirupsen/logrus v1.4.2 github.com/stevvooe/resumable v0.0.0-20180830230917-22b14a53ba50 // indirect github.com/stretchr/testify v1.4.0 - golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc // indirect + golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 // indirect golang.org/x/net v0.0.0-20191007182048-72f939374954 gopkg.in/yaml.v2 v2.2.4 ) diff --git a/go.sum b/go.sum index 58de99f..b5a9924 100644 --- a/go.sum +++ b/go.sum @@ -44,6 +44,7 @@ github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/stevvooe/resumable v0.0.0-20180830230917-22b14a53ba50 h1:4bT0pPowCpQImewr+BjzfUKcuFW+KVyB8d1OF3b6oTI= github.com/stevvooe/resumable v0.0.0-20180830230917-22b14a53ba50/go.mod h1:1pdIZTAHUz+HDKDVZ++5xg/duPlhKAIzw9qy42CWYp4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -52,8 +53,8 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc h1:c0o/qxkaO2LF5t6fQrT4b5hzyggAkLLlCUjqfRxd8Q4= -golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20191007182048-72f939374954 h1:JGZucVF/L/TotR719NbujzadOZ2AgnYlqphQGHDCKaU= golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -64,6 +65,7 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b h1:ag/x1USPSsqHud38I9BAC88qdNLDHHtQ4mlgQIZPPNA= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= diff --git a/main.go b/main.go index 75ae8d0..2cafe24 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,7 @@ import ( log "github.com/sirupsen/logrus" v1 "github.com/ivanilves/lstags/api/v1" + "github.com/ivanilves/lstags/api/v1/registry/client/auth" "github.com/ivanilves/lstags/config" ) @@ -34,6 +35,7 @@ type Options struct { RetryRequests int `short:"y" long:"retry-requests" default:"2" description:"Number of retries for failed Docker registry requests" env:"RETRY_REQUESTS"` RetryDelay time.Duration `short:"D" long:"retry-delay" default:"2s" description:"Delay between retries of failed registry requests" env:"RETRY_DELAY"` InsecureRegistryEx string `short:"I" long:"insecure-registry-ex" description:"Expression to match insecure registry hostnames" env:"INSECURE_REGISTRY_EX"` + BasicAuth []string `short:"B" long:"basic-auth" description:"Set per-registry BASIC auth username:password pair" env:"BASIC_AUTH"` TraceRequests bool `short:"T" long:"trace-requests" description:"Trace Docker registry HTTP requests" env:"TRACE_REQUESTS"` DoNotFail bool `short:"N" long:"do-not-fail" description:"Do not fail on non-critical errors (could be dangerous!)" env:"DO_NOT_FAIL"` DaemonMode bool `short:"d" long:"daemon-mode" description:"Run as daemon instead of just execute and exit" env:"DAEMON_MODE"` @@ -105,6 +107,10 @@ func main() { suicide(err, true) } + if err := auth.BasicStore.LoadAll(o.BasicAuth); err != nil { + suicide(err, true) + } + apiConfig := v1.Config{ DockerJSONConfigFile: o.DockerJSON, ConcurrentRequests: o.ConcurrentRequests,