Skip to content

Commit

Permalink
Allow checking credentials for image imports against repo url, not ju…
Browse files Browse the repository at this point in the history
…st passed url
  • Loading branch information
soltysh committed Aug 23, 2017
1 parent 933b638 commit fb6ba6c
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 13 deletions.
12 changes: 12 additions & 0 deletions pkg/image/importer/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,18 @@ func (r *repositoryRetriever) ping(registry url.URL, insecure bool, transport ht
}

r.context.Challenges.AddResponse(resp)
if challenges, err := r.context.Challenges.GetChallenges(*resp.Request.URL); err == nil {
for _, ch := range challenges {
// TODO: support multiple realm headers
if realm, ok := ch.Parameters["realm"]; ok {
if url, err := url.Parse(realm); err == nil {
if scs, ok := r.credentials.(*SecretCredentialStore); ok {
scs.AddRealm(&registry, url)
}
}
}
}
}

return nil, nil
}
Expand Down
34 changes: 34 additions & 0 deletions pkg/image/importer/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,40 @@ func TestPing(t *testing.T) {
}
}

func TestPingRecordRealm(t *testing.T) {
retriever := NewContext(http.DefaultTransport, http.DefaultTransport).WithCredentials(NewCredentialsForSecrets([]kapi.Secret{})).(*repositoryRetriever)

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Docker-Distribution-API-Version", "registry/2.0")
w.Header().Set("WWW-Authenticate", `Bearer realm="https://auth.example.com/token"`)
w.WriteHeader(http.StatusUnauthorized)
}))
uri, _ := url.Parse(server.URL)

_, err := retriever.ping(*uri, true, retriever.context.InsecureTransport)
if err != nil {
t.Errorf("Unexpected error, got %v", err)
}
scs, ok := retriever.credentials.(*SecretCredentialStore)
if !ok {
t.Fatalf("Unexpected credential store type %T", retriever.credentials)
}
if len(scs.realmStore.List()) != 1 {
t.Errorf("Unexpected realm # entries, expected 1, got %d", len(scs.realmStore.List()))
}
obj := scs.realmStore.List()[0]
ru, ok := obj.(*realmURL)
if !ok {
t.Errorf("Unexpected object type, expected realmURL, got %T", obj)
}
if ru.realm.String() != "https://auth.example.com/token" {
t.Errorf("Unexpected realm url, expected https://auth.example.com/token got %v", ru.realm)
}
if ru.registry.String() != server.URL {
t.Errorf("Unexpected registry url, expected %s, got %v", server.URL, ru.registry)
}
}

func TestShouldRetry(t *testing.T) {
r := NewRetryRepository(nil, 1, 0).(*retryRepository)

Expand Down
77 changes: 65 additions & 12 deletions pkg/image/importer/credentials.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
package importer

import (
"fmt"
"net/url"
"strings"
"sync"
"time"

"github.com/golang/glog"

"github.com/docker/distribution/registry/client/auth"

"k8s.io/client-go/tools/cache"
kapi "k8s.io/kubernetes/pkg/api"
kapiv1 "k8s.io/kubernetes/pkg/api/v1"
"k8s.io/kubernetes/pkg/credentialprovider"
)

const (
defaultRealmCacheTTL = time.Minute
)

var (
NoCredentials auth.CredentialStore = &noopCredentialStore{}

Expand Down Expand Up @@ -79,7 +86,7 @@ type keyringCredentialStore struct {
}

func (s *keyringCredentialStore) Basic(url *url.URL) (string, string) {
return basicCredentialsFromKeyring(s.DockerKeyring, url)
return basicCredentialsFromKeyring(s.DockerKeyring, url, nil)
}

func (s *keyringCredentialStore) RefreshToken(url *url.URL, service string) string {
Expand All @@ -90,23 +97,39 @@ func (s *keyringCredentialStore) SetRefreshToken(url *url.URL, service string, t
}

func NewCredentialsForSecrets(secrets []kapi.Secret) *SecretCredentialStore {
return &SecretCredentialStore{secrets: secrets}
return &SecretCredentialStore{
secrets: secrets,
realmStore: cache.NewTTLStore(realmKeyFunc, defaultRealmCacheTTL),
}
}

func NewLazyCredentialsForSecrets(secretsFn func() ([]kapi.Secret, error)) *SecretCredentialStore {
return &SecretCredentialStore{secretsFn: secretsFn}
return &SecretCredentialStore{
secretsFn: secretsFn,
realmStore: cache.NewTTLStore(realmKeyFunc, defaultRealmCacheTTL),
}
}

type SecretCredentialStore struct {
lock sync.Mutex
secrets []kapi.Secret
secretsFn func() ([]kapi.Secret, error)
err error
keyring credentialprovider.DockerKeyring
lock sync.Mutex
realmStore cache.Store
secrets []kapi.Secret
secretsFn func() ([]kapi.Secret, error)
err error
keyring credentialprovider.DockerKeyring
}

func (s *SecretCredentialStore) Basic(url *url.URL) (string, string) {
return basicCredentialsFromKeyring(s.init(), url)
// the store holds realm entries, if the target URL matches one it means
// we should auth against registry URL rather than realm one
entry, exists, err := s.realmStore.GetByKey(url.String())
if exists && err == nil {
if ru, ok := entry.(*realmURL); ok {
return basicCredentialsFromKeyring(s.init(), url, &ru.registry)
}
}

return basicCredentialsFromKeyring(s.init(), url, nil)
}

func (s *SecretCredentialStore) RefreshToken(url *url.URL, service string) string {
Expand All @@ -116,6 +139,14 @@ func (s *SecretCredentialStore) RefreshToken(url *url.URL, service string) strin
func (s *SecretCredentialStore) SetRefreshToken(url *url.URL, service string, token string) {
}

func (s *SecretCredentialStore) AddRealm(registry, realm *url.URL) {
ru := &realmURL{
realm: *realm,
registry: *registry,
}
s.realmStore.Add(ru)
}

func (s *SecretCredentialStore) Err() error {
s.lock.Lock()
defer s.lock.Unlock()
Expand Down Expand Up @@ -155,7 +186,7 @@ func (s *SecretCredentialStore) init() credentialprovider.DockerKeyring {
return keyring
}

func basicCredentialsFromKeyring(keyring credentialprovider.DockerKeyring, target *url.URL) (string, string) {
func basicCredentialsFromKeyring(keyring credentialprovider.DockerKeyring, target *url.URL, nonRealmURL *url.URL) (string, string) {
// TODO: compare this logic to Docker authConfig in v2 configuration
value := target.Host + target.Path

Expand All @@ -173,16 +204,38 @@ func basicCredentialsFromKeyring(keyring credentialprovider.DockerKeyring, targe
// do a special case check for docker.io to match historical lookups when we respond to a challenge
if value == "auth.docker.io/token" {
glog.V(5).Infof("Being asked for %s, trying %s for legacy behavior", target, "index.docker.io/v1")
return basicCredentialsFromKeyring(keyring, &url.URL{Host: "index.docker.io", Path: "/v1"})
return basicCredentialsFromKeyring(keyring, &url.URL{Host: "index.docker.io", Path: "/v1"}, nil)
}
// docker 1.9 saves 'docker.io' in config in f23, see https://bugzilla.redhat.com/show_bug.cgi?id=1309739
if value == "index.docker.io" {
glog.V(5).Infof("Being asked for %s, trying %s for legacy behavior", target, "docker.io")
return basicCredentialsFromKeyring(keyring, &url.URL{Host: "docker.io"})
return basicCredentialsFromKeyring(keyring, &url.URL{Host: "docker.io"}, nil)
}
if nonRealmURL != nil {
glog.V(5).Infof("Trying non realm url %s for target %s", nonRealmURL, target)
return basicCredentialsFromKeyring(keyring, nonRealmURL, nil)
}
glog.V(5).Infof("Unable to find a secret to match %s (%s)", target, value)
return "", ""
}
glog.V(5).Infof("Found secret to match %s (%s): %s", target, value, configs[0].ServerAddress)
return configs[0].Username, configs[0].Password
}

// realmURL is a container associating a realm URL with an actual registry URL
type realmURL struct {
realm url.URL
registry url.URL
}

// realmKeyFunc returns an actual registry URL for given realm URL
func realmKeyFunc(obj interface{}) (string, error) {
if key, ok := obj.(cache.ExplicitKey); ok {
return string(key), nil
}
ru, ok := obj.(*realmURL)
if !ok {
return "", fmt.Errorf("object %T is not a realmURL object", obj)
}
return ru.realm.String(), nil
}
2 changes: 1 addition & 1 deletion pkg/image/importer/credentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func (k *mockKeyring) Lookup(image string) ([]credentialprovider.LazyAuthConfigu

func TestHubFallback(t *testing.T) {
k := &mockKeyring{}
basicCredentialsFromKeyring(k, &url.URL{Host: "auth.docker.io", Path: "/token"})
basicCredentialsFromKeyring(k, &url.URL{Host: "auth.docker.io", Path: "/token"}, nil)
if !reflect.DeepEqual([]string{"auth.docker.io/token", "index.docker.io", "docker.io"}, k.calls) {
t.Errorf("unexpected calls: %v", k.calls)
}
Expand Down

0 comments on commit fb6ba6c

Please sign in to comment.