diff --git a/pkg/credentialprovider/gcp/metadata.go b/pkg/credentialprovider/gcp/metadata.go index 3960688ba0..ad24921dd9 100644 --- a/pkg/credentialprovider/gcp/metadata.go +++ b/pkg/credentialprovider/gcp/metadata.go @@ -37,7 +37,9 @@ const ( storageScopePrefix = "https://www.googleapis.com/auth/devstorage" ) -var containerRegistryUrls = []string{"container.cloud.google.com", "gcr.io"} +// For these urls, the parts of the host name can be glob, for example '*.gcr.io" will match +// "foo.gcr.io" and "bar.gcr.io". +var containerRegistryUrls = []string{"container.cloud.google.com", "gcr.io", "*.gcr.io"} var metadataHeader = &http.Header{ "Metadata-Flavor": []string{"Google"}, diff --git a/pkg/credentialprovider/keyring.go b/pkg/credentialprovider/keyring.go index 334cfadb1a..bab19a62cf 100644 --- a/pkg/credentialprovider/keyring.go +++ b/pkg/credentialprovider/keyring.go @@ -18,7 +18,9 @@ package credentialprovider import ( "encoding/json" + "net" "net/url" + "path/filepath" "sort" "strings" @@ -118,6 +120,79 @@ func isDefaultRegistryMatch(image string) bool { return !strings.ContainsAny(parts[0], ".:") } +// url.Parse require a scheme, but ours don't have schemes. Adding a +// scheme to make url.Parse happy, then clear out the resulting scheme. +func parseSchemelessUrl(schemelessUrl string) (*url.URL, error) { + parsed, err := url.Parse("https://" + schemelessUrl) + if err != nil { + return nil, err + } + // clear out the resulting scheme + parsed.Scheme = "" + return parsed, nil +} + +// split the host name into parts, as well as the port +func splitUrl(url *url.URL) (parts []string, port string) { + host, port, err := net.SplitHostPort(url.Host) + if err != nil { + // could not parse port + host, port = url.Host, "" + } + return strings.Split(host, "."), port +} + +// overloaded version of urlsMatch, operating on strings instead of URLs. +func urlsMatchStr(glob string, target string) (bool, error) { + globUrl, err := parseSchemelessUrl(glob) + if err != nil { + return false, err + } + targetUrl, err := parseSchemelessUrl(target) + if err != nil { + return false, err + } + return urlsMatch(globUrl, targetUrl) +} + +// check whether the given target url matches the glob url, which may have +// glob wild cards in the host name. +// +// Examples: +// globUrl=*.docker.io, targetUrl=blah.docker.io => match +// globUrl=*.docker.io, targetUrl=not.right.io => no match +// +// Note that we don't support wildcards in ports and paths yet. +func urlsMatch(globUrl *url.URL, targetUrl *url.URL) (bool, error) { + globUrlParts, globPort := splitUrl(globUrl) + targetUrlParts, targetPort := splitUrl(targetUrl) + if globPort != targetPort { + // port doesn't match + return false, nil + } + if len(globUrlParts) != len(targetUrlParts) { + // host name does not have the same number of parts + return false, nil + } + if !strings.HasPrefix(targetUrl.Path, globUrl.Path) { + // the path of the credential must be a prefix + return false, nil + } + for k, globUrlPart := range globUrlParts { + targetUrlPart := targetUrlParts[k] + matched, err := filepath.Match(globUrlPart, targetUrlPart) + if err != nil { + return false, err + } + if !matched { + // glob mismatch for some part + return false, nil + } + } + // everything matches + return true, nil +} + // Lookup implements the DockerKeyring method for fetching credentials based on image name. // Multiple credentials may be returned if there are multiple potentially valid credentials // available. This allows for rotation. @@ -125,9 +200,9 @@ func (dk *BasicDockerKeyring) Lookup(image string) ([]docker.AuthConfiguration, // range over the index as iterating over a map does not provide a predictable ordering ret := []docker.AuthConfiguration{} for _, k := range dk.index { - // NOTE: prefix is a sufficient check because while scheme is allowed, - // it is stripped as part of 'Add' - if !strings.HasPrefix(image, k) { + // both k and image are schemeless URLs because even though schemes are allowed + // in the credential configurations, we remove them in Add. + if matched, _ := urlsMatchStr(k, image); !matched { continue } diff --git a/pkg/credentialprovider/keyring_test.go b/pkg/credentialprovider/keyring_test.go index 77bb78b4d6..412d283c91 100644 --- a/pkg/credentialprovider/keyring_test.go +++ b/pkg/credentialprovider/keyring_test.go @@ -22,71 +22,246 @@ import ( "testing" ) -func TestDockerKeyringFromBytes(t *testing.T) { - url := "hello.kubernetes.io" - email := "foo@bar.baz" - username := "foo" - password := "bar" - auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password))) - sampleDockerConfig := fmt.Sprintf(`{ +func TestUrlsMatch(t *testing.T) { + tests := []struct { + globUrl string + targetUrl string + matchExpected bool + }{ + // match when there is no path component + { + globUrl: "*.kubernetes.io", + targetUrl: "prefix.kubernetes.io", + matchExpected: true, + }, + { + globUrl: "prefix.*.io", + targetUrl: "prefix.kubernetes.io", + matchExpected: true, + }, + { + globUrl: "prefix.kubernetes.*", + targetUrl: "prefix.kubernetes.io", + matchExpected: true, + }, + { + globUrl: "*-good.kubernetes.io", + targetUrl: "prefix-good.kubernetes.io", + matchExpected: true, + }, + // match with path components + { + globUrl: "*.kubernetes.io/blah", + targetUrl: "prefix.kubernetes.io/blah", + matchExpected: true, + }, + { + globUrl: "prefix.*.io/foo", + targetUrl: "prefix.kubernetes.io/foo/bar", + matchExpected: true, + }, + // match with path components and ports + { + globUrl: "*.kubernetes.io:1111/blah", + targetUrl: "prefix.kubernetes.io:1111/blah", + matchExpected: true, + }, + { + globUrl: "prefix.*.io:1111/foo", + targetUrl: "prefix.kubernetes.io:1111/foo/bar", + matchExpected: true, + }, + // no match when number of parts mismatch + { + globUrl: "*.kubernetes.io", + targetUrl: "kubernetes.io", + matchExpected: false, + }, + { + globUrl: "*.*.kubernetes.io", + targetUrl: "prefix.kubernetes.io", + matchExpected: false, + }, + { + globUrl: "*.*.kubernetes.io", + targetUrl: "kubernetes.io", + matchExpected: false, + }, + // no match when some parts mismatch + { + globUrl: "kubernetes.io", + targetUrl: "kubernetes.com", + matchExpected: false, + }, + { + globUrl: "k*.io", + targetUrl: "quay.io", + matchExpected: false, + }, + // no match when ports mismatch + { + globUrl: "*.kubernetes.io:1234/blah", + targetUrl: "prefix.kubernetes.io:1111/blah", + matchExpected: false, + }, + { + globUrl: "prefix.*.io/foo", + targetUrl: "prefix.kubernetes.io:1111/foo/bar", + matchExpected: false, + }, + } + for _, test := range tests { + matched, _ := urlsMatchStr(test.globUrl, test.targetUrl) + if matched != test.matchExpected { + t.Errorf("Expected match result of %s and %s to be %t, but was %t", + test.globUrl, test.targetUrl, test.matchExpected, matched) + } + } +} + +func TestDockerKeyringForGlob(t *testing.T) { + tests := []struct { + globUrl string + targetUrl string + }{ + { + globUrl: "hello.kubernetes.io", + targetUrl: "hello.kubernetes.io", + }, + { + globUrl: "*.docker.io", + targetUrl: "prefix.docker.io", + }, + { + globUrl: "prefix.*.io", + targetUrl: "prefix.docker.io", + }, + { + globUrl: "prefix.docker.*", + targetUrl: "prefix.docker.io", + }, + { + globUrl: "*.docker.io/path", + targetUrl: "prefix.docker.io/path", + }, + { + globUrl: "prefix.*.io/path", + targetUrl: "prefix.docker.io/path/subpath", + }, + { + globUrl: "prefix.docker.*/path", + targetUrl: "prefix.docker.io/path", + }, + { + globUrl: "*.docker.io:8888", + targetUrl: "prefix.docker.io:8888", + }, + { + globUrl: "prefix.*.io:8888", + targetUrl: "prefix.docker.io:8888", + }, + { + globUrl: "prefix.docker.*:8888", + targetUrl: "prefix.docker.io:8888", + }, + { + globUrl: "*.docker.io/path:1111", + targetUrl: "prefix.docker.io/path:1111", + }, + { + globUrl: "prefix.*.io/path:1111", + targetUrl: "prefix.docker.io/path/subpath:1111", + }, + { + globUrl: "prefix.docker.*/path:1111", + targetUrl: "prefix.docker.io/path:1111", + }, + } + for _, test := range tests { + email := "foo@bar.baz" + username := "foo" + password := "bar" + auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password))) + sampleDockerConfig := fmt.Sprintf(`{ "https://%s": { "email": %q, "auth": %q } -}`, url, email, auth) +}`, test.globUrl, email, auth) - keyring := &BasicDockerKeyring{} - if cfg, err := readDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil { - t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err) - } else { - keyring.Add(cfg) - } + keyring := &BasicDockerKeyring{} + if cfg, err := readDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil { + t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err) + } else { + keyring.Add(cfg) + } - creds, ok := keyring.Lookup(url + "/foo/bar") - if !ok { - t.Errorf("Didn't find expected URL: %s", url) - return - } - if len(creds) > 1 { - t.Errorf("Got more hits than expected: %s", creds) - } - val := creds[0] + creds, ok := keyring.Lookup(test.targetUrl + "/foo/bar") + if !ok { + t.Errorf("Didn't find expected URL: %s", test.targetUrl) + return + } + val := creds[0] - if username != val.Username { - t.Errorf("Unexpected username value, want: %s, got: %s", username, val.Username) - } - if password != val.Password { - t.Errorf("Unexpected password value, want: %s, got: %s", password, val.Password) - } - if email != val.Email { - t.Errorf("Unexpected email value, want: %s, got: %s", email, val.Email) + if username != val.Username { + t.Errorf("Unexpected username value, want: %s, got: %s", username, val.Username) + } + if password != val.Password { + t.Errorf("Unexpected password value, want: %s, got: %s", password, val.Password) + } + if email != val.Email { + t.Errorf("Unexpected email value, want: %s, got: %s", email, val.Email) + } } } func TestKeyringMiss(t *testing.T) { - url := "hello.kubernetes.io" - email := "foo@bar.baz" - username := "foo" - password := "bar" - auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password))) - sampleDockerConfig := fmt.Sprintf(`{ + tests := []struct { + globUrl string + lookupUrl string + }{ + { + globUrl: "hello.kubernetes.io", + lookupUrl: "world.mesos.org/foo/bar", + }, + { + globUrl: "*.docker.com", + lookupUrl: "prefix.docker.io", + }, + { + globUrl: "suffix.*.io", + lookupUrl: "prefix.docker.io", + }, + { + globUrl: "prefix.docker.c*", + lookupUrl: "prefix.docker.io", + }, + } + for _, test := range tests { + email := "foo@bar.baz" + username := "foo" + password := "bar" + auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password))) + sampleDockerConfig := fmt.Sprintf(`{ "https://%s": { "email": %q, "auth": %q } -}`, url, email, auth) +}`, test.globUrl, email, auth) - keyring := &BasicDockerKeyring{} - if cfg, err := readDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil { - t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err) - } else { - keyring.Add(cfg) + keyring := &BasicDockerKeyring{} + if cfg, err := readDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil { + t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err) + } else { + keyring.Add(cfg) + } + + _, ok := keyring.Lookup(test.lookupUrl + "/foo/bar") + if ok { + t.Errorf("Expected not to find URL %s, but found", test.lookupUrl) + } } - val, ok := keyring.Lookup("world.mesos.org/foo/bar") - if ok { - t.Errorf("Found unexpected credential: %+v", val) - } } func TestKeyringMissWithDockerHubCredentials(t *testing.T) {