diff --git a/pkg/registry/auth/auth.go b/pkg/registry/auth/auth.go index 63467e1..cb81f2a 100644 --- a/pkg/registry/auth/auth.go +++ b/pkg/registry/auth/auth.go @@ -11,7 +11,7 @@ import ( "github.com/containrrr/watchtower/pkg/registry/helpers" "github.com/containrrr/watchtower/pkg/types" - "github.com/docker/distribution/reference" + ref "github.com/docker/distribution/reference" "github.com/sirupsen/logrus" ) @@ -20,12 +20,12 @@ const ChallengeHeader = "WWW-Authenticate" // GetToken fetches a token for the registry hosting the provided image func GetToken(container types.Container, registryAuth string) (string, error) { - var err error - var URL url.URL - - if URL, err = GetChallengeURL(container.ImageName()); err != nil { + normalizedRef, err := ref.ParseNormalizedNamed(container.ImageName()) + if err != nil { return "", err } + + URL := GetChallengeURL(normalizedRef) logrus.WithField("URL", URL.String()).Debug("Building challenge URL") var req *http.Request @@ -55,7 +55,7 @@ func GetToken(container types.Container, registryAuth string) (string, error) { return fmt.Sprintf("Basic %s", registryAuth), nil } if strings.HasPrefix(challenge, "bearer") { - return GetBearerHeader(challenge, container.ImageName(), registryAuth) + return GetBearerHeader(challenge, normalizedRef, registryAuth) } return "", errors.New("unsupported challenge type from registry") @@ -73,12 +73,9 @@ func GetChallengeRequest(URL url.URL) (*http.Request, error) { } // GetBearerHeader tries to fetch a bearer token from the registry based on the challenge instructions -func GetBearerHeader(challenge string, img string, registryAuth string) (string, error) { +func GetBearerHeader(challenge string, imageRef ref.Named, registryAuth string) (string, error) { client := http.Client{} - if strings.Contains(img, ":") { - img = strings.Split(img, ":")[0] - } - authURL, err := GetAuthURL(challenge, img) + authURL, err := GetAuthURL(challenge, imageRef) if err != nil { return "", err @@ -114,7 +111,7 @@ func GetBearerHeader(challenge string, img string, registryAuth string) (string, } // GetAuthURL from the instructions in the challenge -func GetAuthURL(challenge string, img string) (*url.URL, error) { +func GetAuthURL(challenge string, imageRef ref.Named) (*url.URL, error) { loweredChallenge := strings.ToLower(challenge) raw := strings.TrimPrefix(loweredChallenge, "bearer") @@ -141,51 +138,25 @@ func GetAuthURL(challenge string, img string) (*url.URL, error) { q := authURL.Query() q.Add("service", values["service"]) - scopeImage := GetScopeFromImageName(img, values["service"]) + scopeImage := ref.Path(imageRef) scope := fmt.Sprintf("repository:%s:pull", scopeImage) - logrus.WithFields(logrus.Fields{"scope": scope, "image": img}).Debug("Setting scope for auth token") + logrus.WithFields(logrus.Fields{"scope": scope, "image": imageRef.Name()}).Debug("Setting scope for auth token") q.Add("scope", scope) authURL.RawQuery = q.Encode() return authURL, nil } -// GetScopeFromImageName normalizes an image name for use as scope during auth and head requests -func GetScopeFromImageName(img, svc string) string { - parts := strings.Split(img, "/") - - if len(parts) > 2 { - if strings.Contains(svc, "docker.io") { - return fmt.Sprintf("%s/%s", parts[1], strings.Join(parts[2:], "/")) - } - return strings.Join(parts, "/") - } - - if len(parts) == 2 { - if strings.Contains(parts[0], "docker.io") { - return fmt.Sprintf("library/%s", parts[1]) - } - return strings.Replace(img, svc+"/", "", 1) - } - - if strings.Contains(svc, "docker.io") { - return fmt.Sprintf("library/%s", parts[0]) - } - return img -} - -// GetChallengeURL creates a URL object based on the image info -func GetChallengeURL(img string) (url.URL, error) { - host, err := helpers.GetRegistryAddress(img) - if err != nil { - return url.URL{}, err - } +// GetChallengeURL returns the URL to check auth requirements +// for access to a given image +func GetChallengeURL(imageRef ref.Named) url.URL { + host, _ := helpers.GetRegistryAddress(imageRef.Name()) URL := url.URL{ Scheme: "https", Host: host, Path: "/v2/", } - return URL, nil + return URL } diff --git a/pkg/registry/auth/auth_test.go b/pkg/registry/auth/auth_test.go index 6ad2307..f85d271 100644 --- a/pkg/registry/auth/auth_test.go +++ b/pkg/registry/auth/auth_test.go @@ -6,10 +6,12 @@ import ( "github.com/containrrr/watchtower/pkg/registry/auth" "net/url" "os" + "strings" "testing" "time" wtTypes "github.com/containrrr/watchtower/pkg/types" + ref "github.com/docker/distribution/reference" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) @@ -69,52 +71,71 @@ var _ = Describe("the auth module", func() { Path: "/token", RawQuery: "scope=repository%3Acontainrrr%2Fwatchtower%3Apull&service=ghcr.io", } - res, err := auth.GetAuthURL(input, "containrrr/watchtower") + imageRef, _ := ref.ParseNormalizedNamed("containrrr/watchtower") + res, err := auth.GetAuthURL(input, imageRef) Expect(err).NotTo(HaveOccurred()) Expect(res).To(Equal(expected)) }) It("should create a valid auth url object based on the challenge header supplied", func() { input := `bearer realm="https://ghcr.io/token"` - res, err := auth.GetAuthURL(input, "containrrr/watchtower") + imageRef, _ := ref.ParseNormalizedNamed("containrrr/watchtower") + res, err := auth.GetAuthURL(input, imageRef) Expect(err).To(HaveOccurred()) Expect(res).To(BeNil()) }) + + When("deriving the auth scope from an image name", func() { + It("should prepend official dockerhub images with \"library/\"", func() { + Expect(getScopeFromImageAuthURL("docker.io/registry")).To(Equal("library/registry")) + Expect(getScopeFromImageAuthURL("docker.io/registry")).To(Equal("library/registry")) + Expect(getScopeFromImageAuthURL("index.docker.io/registry")).To(Equal("library/registry")) + Expect(getScopeFromImageAuthURL("index.docker.io/registry")).To(Equal("library/registry")) + + Expect(getScopeFromImageAuthURL("registry")).To(Equal("library/registry")) + Expect(getScopeFromImageAuthURL("registry")).To(Equal("library/registry")) + }) + It("should not include vanity hosts\"", func() { + Expect(getScopeFromImageAuthURL("docker.io/containrrr/watchtower")).To(Equal("containrrr/watchtower")) + Expect(getScopeFromImageAuthURL("index.docker.io/containrrr/watchtower")).To(Equal("containrrr/watchtower")) + }) + It("should not destroy three segment image names\"", func() { + Expect(getScopeFromImageAuthURL("piksel/containrrr/watchtower")).To(Equal("piksel/containrrr/watchtower")) + Expect(getScopeFromImageAuthURL("piksel/containrrr/watchtower")).To(Equal("piksel/containrrr/watchtower")) + }) + It("should not add \"library/\" for one segment image names if they're not on dockerhub", func() { + Expect(getScopeFromImageAuthURL("ghcr.io/watchtower")).To(Equal("watchtower")) + // Expect(getScopeFromImageName("watchtower")).To(Equal("watchtower")) + }) + }) }) When("getting a challenge url", func() { It("should create a valid challenge url object based on the image ref supplied", func() { expected := url.URL{Host: "ghcr.io", Scheme: "https", Path: "/v2/"} - Expect(auth.GetChallengeURL("ghcr.io/containrrr/watchtower:latest")).To(Equal(expected)) + imageRef, _ := ref.ParseNormalizedNamed("ghcr.io/containrrr/watchtower:latest") + Expect(auth.GetChallengeURL(imageRef)).To(Equal(expected)) }) - It("should assume dockerhub if the image ref is not fully qualified", func() { + It("should assume docker hub if the image ref is not fully qualified", func() { expected := url.URL{Host: "index.docker.io", Scheme: "https", Path: "/v2/"} - Expect(auth.GetChallengeURL("containrrr/watchtower:latest")).To(Equal(expected)) + imageRef, _ := ref.ParseNormalizedNamed("containrrr/watchtower:latest") + Expect(auth.GetChallengeURL(imageRef)).To(Equal(expected)) }) - It("should convert legacy dockerhub hostnames to index.docker.io", func() { + It("should use docker hub if the image ref specifies docker.io", func() { expected := url.URL{Host: "index.docker.io", Scheme: "https", Path: "/v2/"} - Expect(auth.GetChallengeURL("docker.io/containrrr/watchtower:latest")).To(Equal(expected)) - Expect(auth.GetChallengeURL("registry-1.docker.io/containrrr/watchtower:latest")).To(Equal(expected)) - }) - }) - When("getting the auth scope from an image name", func() { - It("should prepend official dockerhub images with \"library/\"", func() { - Expect(auth.GetScopeFromImageName("docker.io/registry", "index.docker.io")).To(Equal("library/registry")) - Expect(auth.GetScopeFromImageName("docker.io/registry", "docker.io")).To(Equal("library/registry")) - - Expect(auth.GetScopeFromImageName("registry", "index.docker.io")).To(Equal("library/registry")) - Expect(auth.GetScopeFromImageName("watchtower", "registry-1.docker.io")).To(Equal("library/watchtower")) - - }) - It("should not include vanity hosts\"", func() { - Expect(auth.GetScopeFromImageName("docker.io/containrrr/watchtower", "index.docker.io")).To(Equal("containrrr/watchtower")) - Expect(auth.GetScopeFromImageName("index.docker.io/containrrr/watchtower", "index.docker.io")).To(Equal("containrrr/watchtower")) - }) - It("should not destroy three segment image names\"", func() { - Expect(auth.GetScopeFromImageName("piksel/containrrr/watchtower", "index.docker.io")).To(Equal("containrrr/watchtower")) - Expect(auth.GetScopeFromImageName("piksel/containrrr/watchtower", "ghcr.io")).To(Equal("piksel/containrrr/watchtower")) - }) - It("should not add \"library/\" for one segment image names if they're not on dockerhub", func() { - Expect(auth.GetScopeFromImageName("ghcr.io/watchtower", "ghcr.io")).To(Equal("watchtower")) - Expect(auth.GetScopeFromImageName("watchtower", "ghcr.io")).To(Equal("watchtower")) + imageRef, _ := ref.ParseNormalizedNamed("docker.io/containrrr/watchtower:latest") + Expect(auth.GetChallengeURL(imageRef)).To(Equal(expected)) }) }) }) + +var scopeImageRegexp = MatchRegexp("^repository:[a-z0-9]+(/[a-z0-9]+)*:pull$") + +func getScopeFromImageAuthURL(imageName string) string { + challenge := `bearer realm="https://dummy.host/token",service="dummy.host",scope="repository:user/image:pull"` + + normalizedRef, _ := ref.ParseNormalizedNamed(imageName) + URL, _ := auth.GetAuthURL(challenge, normalizedRef) + + scope := URL.Query().Get("scope") + Expect(scopeImageRegexp.Match(scope)).To(BeTrue()) + return strings.Replace(scope[11:], ":pull", "", 1) +}