Skip to content

Commit

Permalink
Merge pull request #1373 from mtrmac/getcredentials-keys
Browse files Browse the repository at this point in the history
Namespaced credentials key enhancements
  • Loading branch information
rhatdan authored Dec 1, 2021
2 parents fa70420 + 11809de commit 603ec13
Show file tree
Hide file tree
Showing 3 changed files with 380 additions and 368 deletions.
165 changes: 96 additions & 69 deletions pkg/docker/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ var (

// SetCredentials stores the username and password in a location
// appropriate for sys and the users’ configuration.
// A valid key can be either a registry hostname or additionally a namespace if
// the AuthenticationFileHelper is being unsed.
// A valid key is a repository, a namespace within a registry, or a registry hostname;
// using forms other than just a registry may fail depending on configuration.
// Returns a human-redable description of the location that was updated.
// NOTE: The return value is only intended to be read by humans; its form is not an API,
// it may change (or new forms can be added) any time.
Expand Down Expand Up @@ -128,11 +128,15 @@ func GetAllCredentials(sys *types.SystemContext) (map[string]types.DockerAuthCon
// possible sources, and then call `GetCredentials` on them. That
// prevents us from having to reverse engineer the logic in
// `GetCredentials`.
allRegistries := make(map[string]bool)
addRegistry := func(s string) {
allRegistries[s] = true
allKeys := make(map[string]bool)
addKey := func(s string) {
allKeys[s] = true
}

// To use GetCredentials, we must at least convert the URL forms into host names.
// While we're at it, we’ll also canonicalize docker.io to the standard format.
normalizedDockerIORegistry := normalizeRegistry("docker.io")

helpers, err := sysregistriesv2.CredentialHelpers(sys)
if err != nil {
return nil, err
Expand All @@ -151,10 +155,14 @@ func GetAllCredentials(sys *types.SystemContext) (map[string]types.DockerAuthCon
// direct mapping to a registry, so we can just
// walk the map.
for registry := range auths.CredHelpers {
addRegistry(registry)
addKey(registry)
}
for registry := range auths.AuthConfigs {
addRegistry(registry)
for key := range auths.AuthConfigs {
key := normalizeAuthFileKey(key, path.legacyFormat)
if key == normalizedDockerIORegistry {
key = "docker.io"
}
addKey(key)
}
}
// External helpers.
Expand All @@ -166,7 +174,7 @@ func GetAllCredentials(sys *types.SystemContext) (map[string]types.DockerAuthCon
switch errors.Cause(err) {
case nil:
for registry := range creds {
addRegistry(registry)
addKey(registry)
}
case exec.ErrNotFound:
// It's okay if the helper doesn't exist.
Expand All @@ -179,8 +187,8 @@ func GetAllCredentials(sys *types.SystemContext) (map[string]types.DockerAuthCon
// Now use `GetCredentials` to the specific auth configs for each
// previously listed registry.
authConfigs := make(map[string]types.DockerAuthConfig)
for registry := range allRegistries {
authConf, err := GetCredentials(sys, registry)
for key := range allKeys {
authConf, err := GetCredentials(sys, key)
if err != nil {
if credentials.IsErrCredentialsNotFoundMessage(err.Error()) {
// Ignore if the credentials could not be found (anymore).
Expand All @@ -189,7 +197,7 @@ func GetAllCredentials(sys *types.SystemContext) (map[string]types.DockerAuthCon
// Note: we rely on the logging in `GetCredentials`.
return nil, err
}
authConfigs[registry] = authConf
authConfigs[key] = authConf
}

return authConfigs, nil
Expand Down Expand Up @@ -230,47 +238,49 @@ func getAuthFilePaths(sys *types.SystemContext, homeDir string) []authPath {
return paths
}

// GetCredentials returns the registry credentials stored in the
// registry-specific credential helpers or in the default global credentials
// helpers with falling back to using either auth.json
// file or .docker/config.json, including support for OAuth2 and IdentityToken.
// GetCredentials returns the registry credentials matching key, appropriate for
// sys and the users’ configuration.
// If an entry is not found, an empty struct is returned.
// A valid key is a repository, a namespace within a registry, or a registry hostname.
//
// GetCredentialsForRef should almost always be used in favor of this API to
// allow different credentials for different repositories on the same registry.
func GetCredentials(sys *types.SystemContext, registry string) (types.DockerAuthConfig, error) {
return getCredentialsWithHomeDir(sys, nil, registry, homedir.Get())
// GetCredentialsForRef should almost always be used in favor of this API.
func GetCredentials(sys *types.SystemContext, key string) (types.DockerAuthConfig, error) {
return getCredentialsWithHomeDir(sys, key, homedir.Get())
}

// GetCredentialsForRef returns the registry credentials necessary for
// accessing ref on the registry ref points to,
// appropriate for sys and the users’ configuration.
// If an entry is not found, an empty struct is returned.
func GetCredentialsForRef(sys *types.SystemContext, ref reference.Named) (types.DockerAuthConfig, error) {
return getCredentialsWithHomeDir(sys, ref, reference.Domain(ref), homedir.Get())
return getCredentialsWithHomeDir(sys, ref.Name(), homedir.Get())
}

// getCredentialsWithHomeDir is an internal implementation detail of
// GetCredentialsForRef and GetCredentials. It exists only to allow testing it
// with an artificial home directory.
func getCredentialsWithHomeDir(sys *types.SystemContext, ref reference.Named, registry, homeDir string) (types.DockerAuthConfig, error) {
// consistency check of the ref and registry arguments
if ref != nil && reference.Domain(ref) != registry {
return types.DockerAuthConfig{}, errors.Errorf(
"internal error: provided reference domain %q name does not match registry %q",
reference.Domain(ref), registry,
)
func getCredentialsWithHomeDir(sys *types.SystemContext, key, homeDir string) (types.DockerAuthConfig, error) {
_, err := validateKey(key)
if err != nil {
return types.DockerAuthConfig{}, err
}

if sys != nil && sys.DockerAuthConfig != nil {
logrus.Debugf("Returning credentials for %s from DockerAuthConfig", registry)
logrus.Debugf("Returning credentials for %s from DockerAuthConfig", key)
return *sys.DockerAuthConfig, nil
}

var registry string // We compute this once because it is used in several places.
if firstSlash := strings.IndexRune(key, '/'); firstSlash != -1 {
registry = key[:firstSlash]
} else {
registry = key
}

// Anonymous function to query credentials from auth files.
getCredentialsFromAuthFiles := func() (types.DockerAuthConfig, string, error) {
for _, path := range getAuthFilePaths(sys, homeDir) {
authConfig, err := findAuthentication(ref, registry, path.path, path.legacyFormat)
authConfig, err := findCredentialsInFile(key, registry, path.path, path.legacyFormat)
if err != nil {
return types.DockerAuthConfig{}, "", err
}
Expand All @@ -291,57 +301,61 @@ func getCredentialsWithHomeDir(sys *types.SystemContext, ref reference.Named, re
for _, helper := range helpers {
var (
creds types.DockerAuthConfig
helperKey string
credHelperPath string
err error
)
switch helper {
// Special-case the built-in helper for auth files.
case sysregistriesv2.AuthenticationFileHelper:
helperKey = key
creds, credHelperPath, err = getCredentialsFromAuthFiles()
// External helpers.
default:
// This intentionally uses "registry", not "key"; we don't support namespaced
// credentials in helpers, but a "registry" is a valid parent of "key".
helperKey = registry
creds, err = getAuthFromCredHelper(helper, registry)
}
if err != nil {
logrus.Debugf("Error looking up credentials for %s in credential helper %s: %v", registry, helper, err)
logrus.Debugf("Error looking up credentials for %s in credential helper %s: %v", helperKey, helper, err)
multiErr = multierror.Append(multiErr, err)
continue
}
if len(creds.Username)+len(creds.Password)+len(creds.IdentityToken) == 0 {
continue
}
msg := fmt.Sprintf("Found credentials for %s in credential helper %s", registry, helper)
if credHelperPath != "" {
msg = fmt.Sprintf("%s in file %s", msg, credHelperPath)
if creds != (types.DockerAuthConfig{}) {
msg := fmt.Sprintf("Found credentials for %s in credential helper %s", helperKey, helper)
if credHelperPath != "" {
msg = fmt.Sprintf("%s in file %s", msg, credHelperPath)
}
logrus.Debug(msg)
return creds, nil
}
logrus.Debug(msg)
return creds, nil
}
if multiErr != nil {
return types.DockerAuthConfig{}, multiErr
}

logrus.Debugf("No credentials for %s found", registry)
logrus.Debugf("No credentials for %s found", key)
return types.DockerAuthConfig{}, nil
}

// GetAuthentication returns the registry credentials stored in the
// registry-specific credential helpers or in the default global credentials
// helpers with falling back to using either auth.json file or
// .docker/config.json
// GetAuthentication returns the registry credentials matching key, appropriate for
// sys and the users’ configuration.
// If an entry is not found, an empty struct is returned.
// A valid key is a repository, a namespace within a registry, or a registry hostname.
//
// Deprecated: This API only has support for username and password. To get the
// support for oauth2 in container registry authentication, we added the new
// GetCredentials API. The new API should be used and this API is kept to
// GetCredentialsForRef and GetCredentials API. The new API should be used and this API is kept to
// maintain backward compatibility.
func GetAuthentication(sys *types.SystemContext, registry string) (string, string, error) {
return getAuthenticationWithHomeDir(sys, registry, homedir.Get())
func GetAuthentication(sys *types.SystemContext, key string) (string, string, error) {
return getAuthenticationWithHomeDir(sys, key, homedir.Get())
}

// getAuthenticationWithHomeDir is an internal implementation detail of GetAuthentication,
// it exists only to allow testing it with an artificial home directory.
func getAuthenticationWithHomeDir(sys *types.SystemContext, registry, homeDir string) (string, string, error) {
auth, err := getCredentialsWithHomeDir(sys, nil, registry, homeDir)
func getAuthenticationWithHomeDir(sys *types.SystemContext, key, homeDir string) (string, string, error) {
auth, err := getCredentialsWithHomeDir(sys, key, homeDir)
if err != nil {
return "", "", err
}
Expand All @@ -353,8 +367,8 @@ func getAuthenticationWithHomeDir(sys *types.SystemContext, registry, homeDir st

// RemoveAuthentication removes credentials for `key` from all possible
// sources such as credential helpers and auth files.
// A valid key can be either a registry hostname or additionally a namespace if
// the AuthenticationFileHelper is being unsed.
// A valid key is a repository, a namespace within a registry, or a registry hostname;
// using forms other than just a registry may fail depending on configuration.
func RemoveAuthentication(sys *types.SystemContext, key string) error {
isNamespaced, err := validateKey(key)
if err != nil {
Expand Down Expand Up @@ -639,26 +653,27 @@ func deleteAuthFromCredHelper(credHelper, registry string) error {
return helperclient.Erase(p, registry)
}

// findAuthentication looks for auth of registry in path. If ref is
// not nil, then it will be taken into account when looking up the
// authentication credentials.
func findAuthentication(ref reference.Named, registry, path string, legacyFormat bool) (types.DockerAuthConfig, error) {
// findCredentialsInFile looks for credentials matching "key"
// (which is "registry" or a namespace in "registry") in "path".
func findCredentialsInFile(key, registry, path string, legacyFormat bool) (types.DockerAuthConfig, error) {
auths, err := readJSONFile(path, legacyFormat)
if err != nil {
return types.DockerAuthConfig{}, errors.Wrapf(err, "reading JSON file %q", path)
}

// First try cred helpers. They should always be normalized.
// This intentionally uses "registry", not "key"; we don't support namespaced
// credentials in helpers.
if ch, exists := auths.CredHelpers[registry]; exists {
return getAuthFromCredHelper(ch, registry)
}

// Support for different paths in auth.
// Support sub-registry namespaces in auth.
// (This is not a feature of ~/.docker/config.json; we support it even for
// those files as an extension.)
var keys []string
if !legacyFormat && ref != nil {
keys = authKeysForRef(ref)
if !legacyFormat {
keys = authKeysForKey(key)
} else {
keys = []string{registry}
}
Expand Down Expand Up @@ -689,23 +704,22 @@ func findAuthentication(ref reference.Named, registry, path string, legacyFormat
return types.DockerAuthConfig{}, nil
}

// authKeysForRef returns the valid paths for a provided reference. For example,
// when given a reference "quay.io/repo/ns/image:tag", then it would return
// authKeysForKey returns the keys matching a provided auth file key, in order
// from the best match to worst. For example,
// when given a repository key "quay.io/repo/ns/image", it returns
// - quay.io/repo/ns/image
// - quay.io/repo/ns
// - quay.io/repo
// - quay.io
func authKeysForRef(ref reference.Named) (res []string) {
name := ref.Name()

func authKeysForKey(key string) (res []string) {
for {
res = append(res, name)
res = append(res, key)

lastSlash := strings.LastIndex(name, "/")
lastSlash := strings.LastIndex(key, "/")
if lastSlash == -1 {
break
}
name = name[:lastSlash]
key = key[:lastSlash]
}

return res
Expand Down Expand Up @@ -759,11 +773,24 @@ func normalizeRegistry(registry string) string {

// validateKey verifies that the input key does not have a prefix that is not
// allowed and returns an indicator if the key is namespaced.
func validateKey(key string) (isNamespaced bool, err error) {
func validateKey(key string) (bool, error) {
if strings.HasPrefix(key, "http://") || strings.HasPrefix(key, "https://") {
return isNamespaced, errors.Errorf("key %s contains http[s]:// prefix", key)
return false, errors.Errorf("key %s contains http[s]:// prefix", key)
}

// Ideally this should only accept explicitly valid keys, compare
// validateIdentityRemappingPrefix. For now, just reject values that look
// like tagged or digested values.
if strings.ContainsRune(key, '@') {
return false, fmt.Errorf(`key %s contains a '@' character`, key)
}

firstSlash := strings.IndexRune(key, '/')
isNamespaced := firstSlash != -1
// Reject host/repo:tag, but allow localhost:5000 and localhost:5000/foo.
if isNamespaced && strings.ContainsRune(key[firstSlash+1:], ':') {
return false, fmt.Errorf(`key %s contains a ':' character after host[:port]`, key)
}
// check if the provided key contains one or more subpaths.
return strings.ContainsRune(key, '/'), nil
return isNamespaced, nil
}
Loading

0 comments on commit 603ec13

Please sign in to comment.