From 90e1393585804f6a9a1edb5ccce30e43c4620a22 Mon Sep 17 00:00:00 2001 From: peusebiu Date: Thu, 30 Jan 2025 19:40:24 +0200 Subject: [PATCH] fix(sync): fixed harbor authentication issues on _catalog endpoint (#2891) Signed-off-by: Petu Eusebiu --- errors/errors.go | 1 + pkg/extensions/sync/httpclient/client.go | 100 ++++++++++++++--------- 2 files changed, 62 insertions(+), 39 deletions(-) diff --git a/errors/errors.go b/errors/errors.go index 60dbffea5..b49c0f87a 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -174,4 +174,5 @@ var ( ErrInvalidSearchQuery = errors.New("invalid search query") ErrImageNotFound = errors.New("image not found") ErrAmbiguousInput = errors.New("input is not specific enough") + ErrReceivedUnexpectedAuthHeader = errors.New("received unexpected www-authenticate header") ) diff --git a/pkg/extensions/sync/httpclient/client.go b/pkg/extensions/sync/httpclient/client.go index 4517a3621..be0e30c53 100644 --- a/pkg/extensions/sync/httpclient/client.go +++ b/pkg/extensions/sync/httpclient/client.go @@ -160,12 +160,12 @@ func (httpClient *Client) Ping() bool { defer cancel() //nolint: bodyclose - resp, _, err := httpClient.get(ctx, pingURL.String(), false) + resp, _, err := httpClient.get(ctx, pingURL.String(), "", false) if err != nil { return false } - httpClient.getAuthType(resp) + httpClient.authType = getAuthType(resp) if resp.StatusCode >= http.StatusOK && resp.StatusCode <= http.StatusForbidden { return true @@ -220,21 +220,6 @@ func (httpClient *Client) MakeGetRequest(ctx context.Context, resultPtr interfac return body, resp.Header, resp.StatusCode, err } -func (httpClient *Client) getAuthType(resp *http.Response) { - authHeader := resp.Header.Get("www-authenticate") - - authHeaderLower := strings.ToLower(authHeader) - - //nolint: gocritic - if strings.Contains(authHeaderLower, "bearer") { - httpClient.authType = tokenAuth - } else if strings.Contains(authHeaderLower, "basic") { - httpClient.authType = basicAuth - } else { - httpClient.authType = noneAuth - } -} - func (httpClient *Client) setupAuth(req *http.Request, namespace string) error { if httpClient.authType == tokenAuth { token, err := httpClient.getToken(req.URL.String(), namespace) @@ -254,13 +239,19 @@ func (httpClient *Client) setupAuth(req *http.Request, namespace string) error { return nil } -func (httpClient *Client) get(ctx context.Context, url string, setAuth bool) (*http.Response, []byte, error) { +func (httpClient *Client) get(ctx context.Context, url string, mediaType string, + setBasicAuth bool, +) (*http.Response, []byte, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) //nolint if err != nil { return nil, nil, err } - if setAuth && httpClient.config.Username != "" && httpClient.config.Password != "" { + if mediaType != "" { + req.Header.Set("Accept", mediaType) + } + + if setBasicAuth && httpClient.config.Username != "" && httpClient.config.Password != "" { req.SetBasicAuth(httpClient.config.Username, httpClient.config.Password) } @@ -298,7 +289,16 @@ func (httpClient *Client) makeAndDoRequest(method, mediaType, namespace, urlStr return nil, nil, err } - if err := httpClient.setupAuth(req, namespace); err != nil { + err = httpClient.setupAuth(req, namespace) + if err != nil { + // harbor catalog requests return basicAuth by default, even if bearer is used on the rest of endpoints. + if errors.Is(err, zerr.ErrReceivedUnexpectedAuthHeader) { + httpClient.log.Err(err).Msg("expected bearer auth header, received basic, retrying with basic auth...") + + // try with basic auth + return httpClient.get(context.Background(), urlStr, mediaType, true) + } + return nil, nil, err } @@ -311,25 +311,27 @@ func (httpClient *Client) makeAndDoRequest(method, mediaType, namespace, urlStr return nil, nil, err } - // let's retry one time if we get an insufficient_scope error - if ok, challengeParams := needsRetryWithUpdatedScope(err, resp); ok { - var tokenURL *url.URL + if httpClient.authType == tokenAuth { + // let's retry one time if we get an insufficient_scope error + if ok, challengeParams := needsRetryWithUpdatedScope(err, resp); ok { + var tokenURL *url.URL - var token *bearerToken + var token *bearerToken - tokenURL, err = getTokenURLFromChallengeParams(challengeParams, httpClient.config.Username) - if err != nil { - return nil, nil, err - } + tokenURL, err = getTokenURLFromChallengeParams(challengeParams, httpClient.config.Username) + if err != nil { + return nil, nil, err + } - token, err = httpClient.getTokenFromURL(tokenURL.String(), namespace) - if err != nil { - return nil, nil, err - } + token, err = httpClient.getTokenFromURL(tokenURL.String(), namespace) + if err != nil { + return nil, nil, err + } - req.Header.Set("Authorization", "Bearer "+token.Token) + req.Header.Set("Authorization", "Bearer "+token.Token) - resp, body, err = httpClient.doRequest(req) + resp, body, err = httpClient.doRequest(req) + } } return resp, body, err @@ -337,7 +339,7 @@ func (httpClient *Client) makeAndDoRequest(method, mediaType, namespace, urlStr func (httpClient *Client) getTokenFromURL(urlStr, namespace string) (*bearerToken, error) { //nolint: bodyclose - resp, body, err := httpClient.get(context.Background(), urlStr, true) + resp, body, err := httpClient.get(context.Background(), urlStr, "", true) if err != nil { return nil, err } @@ -366,12 +368,12 @@ func (httpClient *Client) getToken(urlStr, namespace string) (*bearerToken, erro } //nolint: bodyclose - resp, _, err := httpClient.get(context.Background(), urlStr, false) + resp, _, err := httpClient.get(context.Background(), urlStr, "", false) if err != nil { return nil, err } - challengeParams, err := parseAuthHeader(resp) + challengeParams, err := parseBearerAuthHeader(resp) if err != nil { return nil, err } @@ -384,6 +386,21 @@ func (httpClient *Client) getToken(urlStr, namespace string) (*bearerToken, erro return httpClient.getTokenFromURL(tokenURL.String(), namespace) } +func getAuthType(resp *http.Response) authType { + authHeader := resp.Header.Get("www-authenticate") + + authHeaderLower := strings.ToLower(authHeader) + + //nolint: gocritic + if strings.Contains(authHeaderLower, "bearer") { + return tokenAuth + } else if strings.Contains(authHeaderLower, "basic") { + return basicAuth + } else { + return noneAuth + } +} + func newBearerToken(blob []byte) (*bearerToken, error) { token := new(bearerToken) if err := json.Unmarshal(blob, &token); err != nil { @@ -426,7 +443,7 @@ func getTokenURLFromChallengeParams(params challengeParams, account string) (*ur return parsedRealm, nil } -func parseAuthHeader(resp *http.Response) (challengeParams, error) { +func parseBearerAuthHeader(resp *http.Response) (challengeParams, error) { authHeader := resp.Header.Get("www-authenticate") authHeaderSlice := strings.Split(authHeader, ",") @@ -434,6 +451,11 @@ func parseAuthHeader(resp *http.Response) (challengeParams, error) { params := challengeParams{} for _, elem := range authHeaderSlice { + // expected bearer auth header + if strings.Contains(strings.ToLower(elem), "basic") { + return params, zerr.ErrReceivedUnexpectedAuthHeader + } + if strings.Contains(strings.ToLower(elem), "bearer") { elem = strings.Split(elem, " ")[1] } @@ -469,7 +491,7 @@ func parseAuthHeader(resp *http.Response) (challengeParams, error) { func needsRetryWithUpdatedScope(err error, resp *http.Response) (bool, challengeParams) { params := challengeParams{} if err == nil && resp.StatusCode == http.StatusUnauthorized { - params, err = parseAuthHeader(resp) + params, err = parseBearerAuthHeader(resp) if err != nil { return false, params }