Skip to content

Commit

Permalink
Merge pull request juju#16465 from hpidcock/fix-docker-auth-challenges
Browse files Browse the repository at this point in the history
juju#16465

Docker v2 registries employ a standard process for authentication by returning challenge in the WWW-Authenticate header. This change attempts to use this process for all generic repositories.

## QA steps

This has been tested with quay.io, jfrog artifactory, google cloud artifact repository and digital ocean registry.

- `export DOCKER_USERNAME=my-registry.com/juju-test`
- `make seed-repository`
- `JUJU_BUILD_NUMBER=0 make push-release-operator-image`
- `juju bootstrap minikube --config caas-image-repo='{"repository":"my-registry.com/juju-test","username":"<username>","password":"<password>"}'`

## Documentation changes

N/A

## Links

**Launchpad bug:** https://bugs.launchpad.net/juju/+bug/2039727

**Jira card:** JUJU-4820
  • Loading branch information
jujubot authored Nov 24, 2023
2 parents d4b9e00 + 942f573 commit f80096d
Show file tree
Hide file tree
Showing 7 changed files with 364 additions and 19 deletions.
4 changes: 3 additions & 1 deletion docker/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"fmt"
"os"
"reflect"
"strings"
"time"

"github.com/docker/distribution/reference"
Expand Down Expand Up @@ -166,14 +167,15 @@ func (rid ImageRepoDetails) SecretData() ([]byte, error) {
if rid.BasicAuthConfig.Empty() && rid.TokenAuthConfig.Empty() {
return nil, nil
}
repo := strings.Split(rid.Repository, "/")[0]
rid.Repository = ""
if !rid.BasicAuthConfig.Empty() && rid.BasicAuthConfig.Auth.Empty() {
rid.BasicAuthConfig.Auth = NewToken(
base64.StdEncoding.EncodeToString([]byte(rid.BasicAuthConfig.Username + ":" + rid.BasicAuthConfig.Password)))
}
o := dockerConfigData{
Auths: map[string]ImageRepoDetails{
rid.ServerAddress: rid,
repo: rid,
},
}
return json.Marshal(o)
Expand Down
2 changes: 1 addition & 1 deletion docker/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func (s *authSuite) TestValidateImageRepoDetails(c *gc.C) {

func (s *authSuite) TestSecretData(c *gc.C) {
imageRepoDetails := docker.ImageRepoDetails{
Repository: "test-account",
Repository: "quay.io/test-account",
ServerAddress: "quay.io",
BasicAuthConfig: docker.BasicAuthConfig{
Auth: docker.NewToken("xxxxx=="),
Expand Down
4 changes: 2 additions & 2 deletions docker/registry/internal/base_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ func transportCommon(transport http.RoundTripper, repoDetails *docker.ImageRepoD
),
)
}
return newTokenTransport(
transport, repoDetails.Username, repoDetails.Password, repoDetails.Auth.Content(), "", false,
return newChallengeTransport(
transport, repoDetails.Username, repoDetails.Password, repoDetails.Auth.Content(),
), nil
}

Expand Down
1 change: 1 addition & 0 deletions docker/registry/internal/package_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type (

var (
NewErrorTransport = newErrorTransport
NewChallengeTransport = newChallengeTransport
NewBasicTransport = newBasicTransport
NewTokenTransport = newTokenTransport
NewElasticContainerRegistryForTest = newElasticContainerRegistryForTest
Expand Down
88 changes: 79 additions & 9 deletions docker/registry/internal/transports.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,78 @@ func (f dynamicTransportFunc) RoundTrip(req *http.Request) (*http.Response, erro
return transport.RoundTrip(req)
}

type challengeTransport struct {
baseTransport http.RoundTripper
currentTransport http.RoundTripper

username string
password string
authToken string
}

func newChallengeTransport(
transport http.RoundTripper, username string, password string, authToken string,
) http.RoundTripper {
return &challengeTransport{
baseTransport: transport,
username: username,
password: password,
authToken: authToken,
}
}

func (t *challengeTransport) RoundTrip(req *http.Request) (*http.Response, error) {
transport := t.baseTransport
if t.currentTransport != nil {
transport = t.currentTransport
}
resp, err := transport.RoundTrip(req)
if err != nil {
return nil, errors.Trace(err)
}
originalResp := resp
if !isUnauthorizedResponse(originalResp) {
return resp, nil
}
for _, c := range challenge.ResponseChallenges(originalResp) {
if err != nil {
logger.Warningf("authentication failed: %s", err.Error())
err = nil
}
switch strings.ToLower(c.Scheme) {
case "bearer":
tokenTransport := &tokenTransport{
transport: t.baseTransport,
username: t.password,
password: t.password,
authToken: t.authToken,
}
err = tokenTransport.refreshOAuthToken(originalResp)
if err != nil {
continue
}
transport = tokenTransport
case "basic":
transport = newBasicTransport(t.baseTransport, t.username, t.password, t.authToken)
default:
err = fmt.Errorf("unknown WWW-Authenticate challenge scheme: %s", c.Scheme)
continue
}
resp, err = transport.RoundTrip(req)
if err == nil && !isUnauthorizedResponse(resp) {
t.currentTransport = transport
return resp, nil
}
}
if err != nil {
return nil, errors.Trace(err)
}
if t.password == "" && t.authToken == "" {
return nil, errors.NewUnauthorized(err, "authorization is required for a private registry")
}
return resp, nil
}

type basicTransport struct {
transport http.RoundTripper
username string
Expand Down Expand Up @@ -75,19 +147,19 @@ type tokenTransport struct {
username string
password string
authToken string
OAuthToken string
oauthToken string
reuseOAuthToken bool
}

func newTokenTransport(
transport http.RoundTripper, username, password, authToken, OAuthToken string, reuseOAuthToken bool,
transport http.RoundTripper, username, password, authToken, oauthToken string, reuseOAuthToken bool,
) http.RoundTripper {
return &tokenTransport{
transport: transport,
username: username,
password: password,
authToken: authToken,
OAuthToken: OAuthToken,
oauthToken: oauthToken,
reuseOAuthToken: reuseOAuthToken,
}
}
Expand Down Expand Up @@ -131,8 +203,6 @@ func (t tokenResponse) token() string {
}

func (t *tokenTransport) refreshOAuthToken(failedResp *http.Response) error {
t.OAuthToken = ""

parameters := getChallengeParameters(t.scheme(), failedResp)
if len(parameters) == 0 {
return errors.NewForbidden(nil, "failed to refresh bearer token")
Expand Down Expand Up @@ -180,13 +250,13 @@ func (t *tokenTransport) refreshOAuthToken(failedResp *http.Response) error {
if err = decoder.Decode(&tr); err != nil {
return fmt.Errorf("unable to decode token response: %s", err)
}
t.OAuthToken = tr.token()
t.oauthToken = tr.token()
return nil
}

func (t *tokenTransport) authorizeRequest(req *http.Request) error {
if t.OAuthToken != "" {
req.Header.Set("Authorization", fmt.Sprintf("%s %s", t.scheme(), t.OAuthToken))
if t.oauthToken != "" {
req.Header.Set("Authorization", fmt.Sprintf("%s %s", t.scheme(), t.oauthToken))
}
return nil
}
Expand All @@ -197,7 +267,7 @@ func (t *tokenTransport) RoundTrip(req *http.Request) (*http.Response, error) {
if !t.reuseOAuthToken {
// We usually do not re-use the OAuth token because each API call might have different scope.
// But some of the provider use long life token and there is no need to refresh.
t.OAuthToken = ""
t.oauthToken = ""
}
}()

Expand Down
Loading

0 comments on commit f80096d

Please sign in to comment.