Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Namespaced credentials key enhancements #1373

Merged
merged 24 commits into from
Dec 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
73279ad
Fold most expected: constants in TestGetAuth to a single line
mtrmac Sep 10, 2021
b612df8
Automate the key presence checking in TestSetCredentials
mtrmac Sep 10, 2021
041f09f
Automate the check of GetCredentialsForRef in TestSetCredentials
mtrmac Sep 11, 2021
d1c00af
Don't wrap the test cases in an unnecessary struct in TestSetCredentials
mtrmac Sep 11, 2021
3b30f6f
Prepare for having multiple tests in TestGetAllCredentials
mtrmac Sep 11, 2021
ce4fac6
Replace noStore with writeKey/expectedKey in TestGetAllCredentials
mtrmac Sep 11, 2021
1d7f3fd
Remove unused expectedError fields from test cases
mtrmac Sep 11, 2021
b60c5c3
Add tests for matching docker.io keys and refs
mtrmac Sep 10, 2021
bb2d95c
Add a comment about only looking up credential helpers by registry
mtrmac Sep 11, 2021
bed9e1b
Reorganize the success case in getCredentialsWithHomeDir
mtrmac Sep 11, 2021
c952f6c
Introduce a string key in getCredentialsWithHomeDir
mtrmac Sep 11, 2021
2605176
Modify findAuthentication to use a string key instead of a reference.…
mtrmac Sep 11, 2021
797035a
Allow using namespaced keys in GetCredentials and GetAuthentication
mtrmac Sep 11, 2021
e6bbcd8
Rename useLegacyAPI to useLegacyFormat
mtrmac Sep 11, 2021
9339016
Fix GetAllCredentials
mtrmac Sep 11, 2021
6b2820b
Reject invalid keys in GetCredentials/GetAuthentication
mtrmac Sep 11, 2021
94da5da
Also test GetCredentials in TestSetCredentials
mtrmac Sep 11, 2021
062c224
Simplify TestGetAuthKeysForKey
mtrmac Sep 11, 2021
19ecfcd
Remove testPreviousAPI from TestGetAuth
mtrmac Sep 11, 2021
b3a6e75
Be explicit about expected auth values, even if empty, in TestGetAuth
mtrmac Sep 11, 2021
ba387b2
Consolidate hostname and ref into a single "key" in TestGetAuth
mtrmac Sep 11, 2021
02138a2
Add a test for the docker.io/vendor key
mtrmac Sep 11, 2021
37c9811
Split TestValidateKey into invalid/valid tests
mtrmac Sep 11, 2021
11809de
Make validateKey a bit more strict
mtrmac Sep 11, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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