From 026b009dbb164f4fe5f5803cd1b25079eeb327cb Mon Sep 17 00:00:00 2001 From: Ramkumar Chinchani Date: Tue, 19 May 2020 13:17:15 -0700 Subject: [PATCH] compat: when in "world-readable" mode, return the WWW-Authenticate header containers/image is the dominant client library to interact with registries. It detects which authentication to use based on the WWW-Authenticate header returned when pinging "/v2/" end-point. If we didn't return this header, then creds are not used for other write-protected end-points. Hence, the compatibility fix. --- pkg/api/controller_test.go | 180 +++++++++++++++++++++++++++++++++++++ pkg/api/routes.go | 12 +++ 2 files changed, 192 insertions(+) diff --git a/pkg/api/controller_test.go b/pkg/api/controller_test.go index afae73f9..4cf4bd22 100644 --- a/pkg/api/controller_test.go +++ b/pkg/api/controller_test.go @@ -986,6 +986,186 @@ func TestBearerAuth(t *testing.T) { }) } +func TestBearerAuthWithAllowReadAccess(t *testing.T) { + Convey("Make a new controller", t, func() { + authTestServer := makeAuthTestServer() + defer authTestServer.Close() + + config := api.NewConfig() + config.HTTP.Port = SecurePort3 + + u, err := url.Parse(authTestServer.URL) + So(err, ShouldBeNil) + + config.HTTP.Auth = &api.AuthConfig{ + Bearer: &api.BearerConfig{ + Cert: ServerCert, + Realm: authTestServer.URL + "/auth/token", + Service: u.Host, + }, + } + config.HTTP.AllowReadAccess = true + c := api.NewController(config) + dir, err := ioutil.TempDir("", "oci-repo-test") + So(err, ShouldBeNil) + defer os.RemoveAll(dir) + c.Config.Storage.RootDirectory = dir + go func() { + // this blocks + if err := c.Run(); err != nil { + return + } + }() + + // wait till ready + for { + _, err := resty.R().Get(BaseURL3) + if err == nil { + break + } + time.Sleep(100 * time.Millisecond) + } + + defer func() { + ctx := context.Background() + _ = c.Server.Shutdown(ctx) + }() + + blob := []byte("hello, blob!") + digest := godigest.FromBytes(blob).String() + + resp, err := resty.R().Get(BaseURL3 + "/v2/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 401) + + authorizationHeader := parseBearerAuthHeader(resp.Header().Get("Www-Authenticate")) + resp, err = resty.R(). + SetQueryParam("service", authorizationHeader.Service). + SetQueryParam("scope", authorizationHeader.Scope). + Get(authorizationHeader.Realm) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + var goodToken accessTokenResponse + err = json.Unmarshal(resp.Body(), &goodToken) + So(err, ShouldBeNil) + + resp, err = resty.R(). + SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)). + Get(BaseURL3 + "/v2/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + resp, err = resty.R().Post(BaseURL3 + "/v2/" + AuthorizedNamespace + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 401) + + authorizationHeader = parseBearerAuthHeader(resp.Header().Get("Www-Authenticate")) + resp, err = resty.R(). + SetQueryParam("service", authorizationHeader.Service). + SetQueryParam("scope", authorizationHeader.Scope). + Get(authorizationHeader.Realm) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + err = json.Unmarshal(resp.Body(), &goodToken) + So(err, ShouldBeNil) + + resp, err = resty.R(). + SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)). + Post(BaseURL3 + "/v2/" + AuthorizedNamespace + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 202) + loc := resp.Header().Get("Location") + + resp, err = resty.R(). + SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))). + SetHeader("Content-Type", "application/octet-stream"). + SetQueryParam("digest", digest). + SetBody(blob). + Put(BaseURL3 + loc) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 401) + + authorizationHeader = parseBearerAuthHeader(resp.Header().Get("Www-Authenticate")) + resp, err = resty.R(). + SetQueryParam("service", authorizationHeader.Service). + SetQueryParam("scope", authorizationHeader.Scope). + Get(authorizationHeader.Realm) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + err = json.Unmarshal(resp.Body(), &goodToken) + So(err, ShouldBeNil) + + resp, err = resty.R(). + SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))). + SetHeader("Content-Type", "application/octet-stream"). + SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)). + SetQueryParam("digest", digest). + SetBody(blob). + Put(BaseURL3 + loc) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 201) + + resp, err = resty.R(). + SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)). + Get(BaseURL3 + "/v2/" + AuthorizedNamespace + "/tags/list") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 401) + + authorizationHeader = parseBearerAuthHeader(resp.Header().Get("Www-Authenticate")) + resp, err = resty.R(). + SetQueryParam("service", authorizationHeader.Service). + SetQueryParam("scope", authorizationHeader.Scope). + Get(authorizationHeader.Realm) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + err = json.Unmarshal(resp.Body(), &goodToken) + So(err, ShouldBeNil) + + resp, err = resty.R(). + SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)). + Get(BaseURL3 + "/v2/" + AuthorizedNamespace + "/tags/list") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + resp, err = resty.R(). + Post(BaseURL3 + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 401) + + authorizationHeader = parseBearerAuthHeader(resp.Header().Get("Www-Authenticate")) + resp, err = resty.R(). + SetQueryParam("service", authorizationHeader.Service). + SetQueryParam("scope", authorizationHeader.Scope). + Get(authorizationHeader.Realm) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + var badToken accessTokenResponse + err = json.Unmarshal(resp.Body(), &badToken) + So(err, ShouldBeNil) + + resp, err = resty.R(). + SetHeader("Authorization", fmt.Sprintf("Bearer %s", badToken.AccessToken)). + Post(BaseURL3 + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 401) + }) +} + func makeAuthTestServer() *httptest.Server { cmTokenGenerator, err := auth.NewTokenGenerator(&auth.TokenGeneratorOptions{ PrivateKeyPath: ServerKey, diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 6ec6574c..5dd69a1b 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -120,6 +120,18 @@ func (rh *RouteHandler) SetupRoutes() { // @Success 200 {string} string "ok" func (rh *RouteHandler) CheckVersionSupport(w http.ResponseWriter, r *http.Request) { w.Header().Set(DistAPIVersion, "registry/2.0") + // NOTE: compatibility workaround - return this header in "allowed-read" mode to allow for clients to + // work correctly + if rh.c.Config.HTTP.AllowReadAccess { + if rh.c.Config.HTTP.Auth != nil { + if rh.c.Config.HTTP.Auth.Bearer != nil { + w.Header().Set("WWW-Authenticate", fmt.Sprintf("bearer realm=%s", rh.c.Config.HTTP.Auth.Bearer.Realm)) + } else { + w.Header().Set("WWW-Authenticate", fmt.Sprintf("basic realm=%s", rh.c.Config.HTTP.Realm)) + } + } + } + WriteData(w, http.StatusOK, "application/json", []byte{}) }