k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/credentialprovider/keyring.go (about)

     1  /*
     2  Copyright 2014 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package credentialprovider
    18  
    19  import (
    20  	"net"
    21  	"net/url"
    22  	"path/filepath"
    23  	"sort"
    24  	"strings"
    25  
    26  	"k8s.io/apimachinery/pkg/util/sets"
    27  	"k8s.io/klog/v2"
    28  )
    29  
    30  // DockerKeyring tracks a set of docker registry credentials, maintaining a
    31  // reverse index across the registry endpoints. A registry endpoint is made
    32  // up of a host (e.g. registry.example.com), but it may also contain a path
    33  // (e.g. registry.example.com/foo) This index is important for two reasons:
    34  //   - registry endpoints may overlap, and when this happens we must find the
    35  //     most specific match for a given image
    36  //   - iterating a map does not yield predictable results
    37  type DockerKeyring interface {
    38  	Lookup(image string) ([]AuthConfig, bool)
    39  }
    40  
    41  // BasicDockerKeyring is a trivial map-backed implementation of DockerKeyring
    42  type BasicDockerKeyring struct {
    43  	index []string
    44  	creds map[string][]AuthConfig
    45  }
    46  
    47  // providersDockerKeyring is an implementation of DockerKeyring that
    48  // materializes its dockercfg based on a set of dockerConfigProviders.
    49  type providersDockerKeyring struct {
    50  	Providers []DockerConfigProvider
    51  }
    52  
    53  // AuthConfig contains authorization information for connecting to a Registry
    54  // This type mirrors "github.com/docker/docker/api/types.AuthConfig"
    55  type AuthConfig struct {
    56  	Username string `json:"username,omitempty"`
    57  	Password string `json:"password,omitempty"`
    58  	Auth     string `json:"auth,omitempty"`
    59  
    60  	// Email is an optional value associated with the username.
    61  	// This field is deprecated and will be removed in a later
    62  	// version of docker.
    63  	Email string `json:"email,omitempty"`
    64  
    65  	ServerAddress string `json:"serveraddress,omitempty"`
    66  
    67  	// IdentityToken is used to authenticate the user and get
    68  	// an access token for the registry.
    69  	IdentityToken string `json:"identitytoken,omitempty"`
    70  
    71  	// RegistryToken is a bearer token to be sent to a registry
    72  	RegistryToken string `json:"registrytoken,omitempty"`
    73  }
    74  
    75  // Add add some docker config in basic docker keyring
    76  func (dk *BasicDockerKeyring) Add(cfg DockerConfig) {
    77  	if dk.index == nil {
    78  		dk.index = make([]string, 0)
    79  		dk.creds = make(map[string][]AuthConfig)
    80  	}
    81  	for loc, ident := range cfg {
    82  		creds := AuthConfig{
    83  			Username: ident.Username,
    84  			Password: ident.Password,
    85  			Email:    ident.Email,
    86  		}
    87  
    88  		value := loc
    89  		if !strings.HasPrefix(value, "https://") && !strings.HasPrefix(value, "http://") {
    90  			value = "https://" + value
    91  		}
    92  		parsed, err := url.Parse(value)
    93  		if err != nil {
    94  			klog.Errorf("Entry %q in dockercfg invalid (%v), ignoring", loc, err)
    95  			continue
    96  		}
    97  
    98  		// The docker client allows exact matches:
    99  		//    foo.bar.com/namespace
   100  		// Or hostname matches:
   101  		//    foo.bar.com
   102  		// It also considers /v2/  and /v1/ equivalent to the hostname
   103  		// See ResolveAuthConfig in docker/registry/auth.go.
   104  		effectivePath := parsed.Path
   105  		if strings.HasPrefix(effectivePath, "/v2/") || strings.HasPrefix(effectivePath, "/v1/") {
   106  			effectivePath = effectivePath[3:]
   107  		}
   108  		var key string
   109  		if (len(effectivePath) > 0) && (effectivePath != "/") {
   110  			key = parsed.Host + effectivePath
   111  		} else {
   112  			key = parsed.Host
   113  		}
   114  		dk.creds[key] = append(dk.creds[key], creds)
   115  		dk.index = append(dk.index, key)
   116  	}
   117  
   118  	eliminateDupes := sets.NewString(dk.index...)
   119  	dk.index = eliminateDupes.List()
   120  
   121  	// Update the index used to identify which credentials to use for a given
   122  	// image. The index is reverse-sorted so more specific paths are matched
   123  	// first. For example, if for the given image "gcr.io/etcd-development/etcd",
   124  	// credentials for "quay.io/coreos" should match before "quay.io".
   125  	sort.Sort(sort.Reverse(sort.StringSlice(dk.index)))
   126  }
   127  
   128  const (
   129  	defaultRegistryHost = "index.docker.io"
   130  	defaultRegistryKey  = defaultRegistryHost + "/v1/"
   131  )
   132  
   133  // isDefaultRegistryMatch determines whether the given image will
   134  // pull from the default registry (DockerHub) based on the
   135  // characteristics of its name.
   136  func isDefaultRegistryMatch(image string) bool {
   137  	parts := strings.SplitN(image, "/", 2)
   138  
   139  	if len(parts[0]) == 0 {
   140  		return false
   141  	}
   142  
   143  	if len(parts) == 1 {
   144  		// e.g. library/ubuntu
   145  		return true
   146  	}
   147  
   148  	if parts[0] == "docker.io" || parts[0] == "index.docker.io" {
   149  		// resolve docker.io/image and index.docker.io/image as default registry
   150  		return true
   151  	}
   152  
   153  	// From: http://blog.docker.com/2013/07/how-to-use-your-own-registry/
   154  	// Docker looks for either a “.” (domain separator) or “:” (port separator)
   155  	// to learn that the first part of the repository name is a location and not
   156  	// a user name.
   157  	return !strings.ContainsAny(parts[0], ".:")
   158  }
   159  
   160  // ParseSchemelessURL parses a schemeless url and returns a url.URL
   161  // url.Parse require a scheme, but ours don't have schemes.  Adding a
   162  // scheme to make url.Parse happy, then clear out the resulting scheme.
   163  func ParseSchemelessURL(schemelessURL string) (*url.URL, error) {
   164  	parsed, err := url.Parse("https://" + schemelessURL)
   165  	if err != nil {
   166  		return nil, err
   167  	}
   168  	// clear out the resulting scheme
   169  	parsed.Scheme = ""
   170  	return parsed, nil
   171  }
   172  
   173  // SplitURL splits the host name into parts, as well as the port
   174  func SplitURL(url *url.URL) (parts []string, port string) {
   175  	host, port, err := net.SplitHostPort(url.Host)
   176  	if err != nil {
   177  		// could not parse port
   178  		host, port = url.Host, ""
   179  	}
   180  	return strings.Split(host, "."), port
   181  }
   182  
   183  // URLsMatchStr is wrapper for URLsMatch, operating on strings instead of URLs.
   184  func URLsMatchStr(glob string, target string) (bool, error) {
   185  	globURL, err := ParseSchemelessURL(glob)
   186  	if err != nil {
   187  		return false, err
   188  	}
   189  	targetURL, err := ParseSchemelessURL(target)
   190  	if err != nil {
   191  		return false, err
   192  	}
   193  	return URLsMatch(globURL, targetURL)
   194  }
   195  
   196  // URLsMatch checks whether the given target url matches the glob url, which may have
   197  // glob wild cards in the host name.
   198  //
   199  // Examples:
   200  //
   201  //	globURL=*.docker.io, targetURL=blah.docker.io => match
   202  //	globURL=*.docker.io, targetURL=not.right.io   => no match
   203  //
   204  // Note that we don't support wildcards in ports and paths yet.
   205  func URLsMatch(globURL *url.URL, targetURL *url.URL) (bool, error) {
   206  	globURLParts, globPort := SplitURL(globURL)
   207  	targetURLParts, targetPort := SplitURL(targetURL)
   208  	if globPort != targetPort {
   209  		// port doesn't match
   210  		return false, nil
   211  	}
   212  	if len(globURLParts) != len(targetURLParts) {
   213  		// host name does not have the same number of parts
   214  		return false, nil
   215  	}
   216  	if !strings.HasPrefix(targetURL.Path, globURL.Path) {
   217  		// the path of the credential must be a prefix
   218  		return false, nil
   219  	}
   220  	for k, globURLPart := range globURLParts {
   221  		targetURLPart := targetURLParts[k]
   222  		matched, err := filepath.Match(globURLPart, targetURLPart)
   223  		if err != nil {
   224  			return false, err
   225  		}
   226  		if !matched {
   227  			// glob mismatch for some part
   228  			return false, nil
   229  		}
   230  	}
   231  	// everything matches
   232  	return true, nil
   233  }
   234  
   235  // Lookup implements the DockerKeyring method for fetching credentials based on image name.
   236  // Multiple credentials may be returned if there are multiple potentially valid credentials
   237  // available.  This allows for rotation.
   238  func (dk *BasicDockerKeyring) Lookup(image string) ([]AuthConfig, bool) {
   239  	// range over the index as iterating over a map does not provide a predictable ordering
   240  	ret := []AuthConfig{}
   241  	for _, k := range dk.index {
   242  		// both k and image are schemeless URLs because even though schemes are allowed
   243  		// in the credential configurations, we remove them in Add.
   244  		if matched, _ := URLsMatchStr(k, image); matched {
   245  			ret = append(ret, dk.creds[k]...)
   246  		}
   247  	}
   248  
   249  	if len(ret) > 0 {
   250  		return ret, true
   251  	}
   252  
   253  	// Use credentials for the default registry if provided, and appropriate
   254  	if isDefaultRegistryMatch(image) {
   255  		if auth, ok := dk.creds[defaultRegistryHost]; ok {
   256  			return auth, true
   257  		}
   258  	}
   259  
   260  	return []AuthConfig{}, false
   261  }
   262  
   263  // Lookup implements the DockerKeyring method for fetching credentials
   264  // based on image name.
   265  func (dk *providersDockerKeyring) Lookup(image string) ([]AuthConfig, bool) {
   266  	keyring := &BasicDockerKeyring{}
   267  
   268  	for _, p := range dk.Providers {
   269  		keyring.Add(p.Provide(image))
   270  	}
   271  
   272  	return keyring.Lookup(image)
   273  }
   274  
   275  // FakeKeyring a fake config credentials
   276  type FakeKeyring struct {
   277  	auth []AuthConfig
   278  	ok   bool
   279  }
   280  
   281  // Lookup implements the DockerKeyring method for fetching credentials based on image name
   282  // return fake auth and ok
   283  func (f *FakeKeyring) Lookup(image string) ([]AuthConfig, bool) {
   284  	return f.auth, f.ok
   285  }
   286  
   287  // UnionDockerKeyring delegates to a set of keyrings.
   288  type UnionDockerKeyring []DockerKeyring
   289  
   290  // Lookup implements the DockerKeyring method for fetching credentials based on image name.
   291  // return each credentials
   292  func (k UnionDockerKeyring) Lookup(image string) ([]AuthConfig, bool) {
   293  	authConfigs := []AuthConfig{}
   294  	for _, subKeyring := range k {
   295  		if subKeyring == nil {
   296  			continue
   297  		}
   298  
   299  		currAuthResults, _ := subKeyring.Lookup(image)
   300  		authConfigs = append(authConfigs, currAuthResults...)
   301  	}
   302  
   303  	return authConfigs, (len(authConfigs) > 0)
   304  }