diff --git a/remotes/docker/fetcher.go b/remotes/docker/fetcher.go index a48c8bdcd..e4a70ac85 100644 --- a/remotes/docker/fetcher.go +++ b/remotes/docker/fetcher.go @@ -31,6 +31,11 @@ func (r dockerFetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.R return nil, err } + ctx, err = contextWithRepositoryScope(ctx, r.refspec, false) + if err != nil { + return nil, err + } + for _, path := range paths { u := r.url(path) diff --git a/remotes/docker/pusher.go b/remotes/docker/pusher.go index abaaac386..24bd278a1 100644 --- a/remotes/docker/pusher.go +++ b/remotes/docker/pusher.go @@ -28,6 +28,10 @@ type dockerPusher struct { } func (p dockerPusher) Push(ctx context.Context, desc ocispec.Descriptor) (content.Writer, error) { + ctx, err := contextWithRepositoryScope(ctx, p.refspec, true) + if err != nil { + return nil, err + } ref := remotes.MakeRefKey(ctx, desc) status, err := p.tracker.GetStatus(ref) if err == nil { diff --git a/remotes/docker/resolver.go b/remotes/docker/resolver.go index 712d21b9f..e331f3ba5 100644 --- a/remotes/docker/resolver.go +++ b/remotes/docker/resolver.go @@ -116,6 +116,10 @@ func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocisp urls = append(urls, fetcher.url("manifests", refspec.Object)) } + ctx, err = contextWithRepositoryScope(ctx, refspec, false) + if err != nil { + return "", ocispec.Descriptor{}, err + } for _, u := range urls { req, err := http.NewRequest(http.MethodHead, u, nil) if err != nil { @@ -228,8 +232,9 @@ func (r *dockerResolver) Pusher(ctx context.Context, ref string) (remotes.Pusher } type dockerBase struct { - base url.URL - token string + refspec reference.Spec + base url.URL + token string client *http.Client useBasic bool @@ -268,6 +273,7 @@ func (r *dockerResolver) base(refspec reference.Spec) (*dockerBase, error) { base.Path = path.Join("/v2", prefix) return &dockerBase{ + refspec: refspec, base: base, client: r.client, username: username, @@ -430,14 +436,10 @@ func (r *dockerBase) setTokenAuth(ctx context.Context, params map[string]string) service: params["service"], } - scope, ok := params["scope"] - if !ok { + to.scopes = getTokenScopes(ctx, params) + if len(to.scopes) == 0 { return errors.Errorf("no scope specified for token auth challenge") } - - // TODO: Get added scopes from context - to.scopes = []string{scope} - if r.secret != "" { // Credential information is provided, use oauth POST endpoint r.token, err = r.fetchTokenWithOAuth(ctx, to) @@ -491,8 +493,9 @@ func (r *dockerBase) fetchTokenWithOAuth(ctx context.Context, to tokenOptions) ( } defer resp.Body.Close() - if resp.StatusCode == 405 && r.username != "" { - // It would be nice if registries would implement the specifications + // Registries without support for POST may return 404 for POST /v2/token. + // As of September 2017, GCR is known to return 404. + if (resp.StatusCode == 405 && r.username != "") || resp.StatusCode == 404 { return r.getToken(ctx, to) } else if resp.StatusCode < 200 || resp.StatusCode >= 400 { b, _ := ioutil.ReadAll(resp.Body) diff --git a/remotes/docker/scope.go b/remotes/docker/scope.go new file mode 100644 index 000000000..9cf0997dc --- /dev/null +++ b/remotes/docker/scope.go @@ -0,0 +1,60 @@ +package docker + +import ( + "context" + "net/url" + "sort" + "strings" + + "github.com/containerd/containerd/reference" +) + +// repositoryScope returns a repository scope string such as "repository:foo/bar:pull" +// for "host/foo/bar:baz". +// When push is true, both pull and push are added to the scope. +func repositoryScope(refspec reference.Spec, push bool) (string, error) { + u, err := url.Parse("dummy://" + refspec.Locator) + if err != nil { + return "", err + } + s := "repository:" + strings.TrimPrefix(u.Path, "/") + ":pull" + if push { + s += ",push" + } + return s, nil +} + +// tokenScopesKey is used for the key for context.WithValue(). +// value: []string (e.g. {"registry:foo/bar:pull"}) +type tokenScopesKey struct{} + +// contextWithRepositoryScope returns a context with tokenScopesKey{} and the repository scope value. +func contextWithRepositoryScope(ctx context.Context, refspec reference.Spec, push bool) (context.Context, error) { + s, err := repositoryScope(refspec, push) + if err != nil { + return nil, err + } + return context.WithValue(ctx, tokenScopesKey{}, []string{s}), nil +} + +// getTokenScopes returns deduplicated and sorted scopes from ctx.Value(tokenScopesKey{}) and params["scope"]. +func getTokenScopes(ctx context.Context, params map[string]string) []string { + var scopes []string + if x := ctx.Value(tokenScopesKey{}); x != nil { + scopes = append(scopes, x.([]string)...) + } + if scope, ok := params["scope"]; ok { + for _, s := range scopes { + // Note: this comparison is unaware of the scope grammar (https://docs.docker.com/registry/spec/auth/scope/) + // So, "repository:foo/bar:pull,push" != "repository:foo/bar:push,pull", although semantically they are equal. + if s == scope { + // already appended + goto Sort + } + } + scopes = append(scopes, scope) + } +Sort: + sort.Strings(scopes) + return scopes +} diff --git a/remotes/docker/scope_test.go b/remotes/docker/scope_test.go new file mode 100644 index 000000000..8eb5a3366 --- /dev/null +++ b/remotes/docker/scope_test.go @@ -0,0 +1,38 @@ +package docker + +import ( + "testing" + + "github.com/containerd/containerd/reference" + "github.com/stretchr/testify/assert" +) + +func TestRepositoryScope(t *testing.T) { + testCases := []struct { + refspec reference.Spec + push bool + expected string + }{ + { + refspec: reference.Spec{ + Locator: "host/foo/bar", + Object: "ignored", + }, + push: false, + expected: "repository:foo/bar:pull", + }, + { + refspec: reference.Spec{ + Locator: "host:4242/foo/bar", + Object: "ignored", + }, + push: true, + expected: "repository:foo/bar:pull,push", + }, + } + for _, x := range testCases { + actual, err := repositoryScope(x.refspec, x.push) + assert.NoError(t, err) + assert.Equal(t, x.expected, actual) + } +}