Skip to content

Commit

Permalink
Auth configs in registries.conf
Browse files Browse the repository at this point in the history
Allow credential-helpers = [] and credential-helper = "" configuration in registries.conf as defautl store for credentials.

Signed-off-by: Qi Wang <[email protected]>
  • Loading branch information
QiWang19 committed Aug 18, 2020
1 parent 8c16c7d commit 8d65474
Show file tree
Hide file tree
Showing 9 changed files with 307 additions and 33 deletions.
5 changes: 5 additions & 0 deletions docs/containers-registries.conf.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ Container engines will use the `$HOME/.config/containers/registries.conf` if it
`unqualified-search-registries`
: An array of _host_[`:`_port_] registries to try when pulling an unqualified image, in order.

`credential-helpers`: An array of credential helpers used as external credential store for the registries.

### NAMESPACED `[[registry]]` SETTINGS

The bulk of the configuration is represented as an array of `[[registry]]`
Expand Down Expand Up @@ -52,6 +54,9 @@ Given an image name, a single `[[registry]]` TOML table is chosen based on its `
: `true` or `false`.
If `true`, pulling images with matching names is forbidden.

`credential-helper`
: The credential helper for registry specified by the `prefix` field. Global default `credential-helpers` is used if `credential-helper` is not specified. This configuration only works if the `prefix` field is a registry domain, not used for inner namespace.

#### Remapping and mirroring registries

The user-specified image reference is, primarily, a "logical" image name, always used for naming
Expand Down
159 changes: 128 additions & 31 deletions pkg/docker/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"runtime"
"strings"

"github.com/containers/image/v5/pkg/sysregistriesv2"
"github.com/containers/image/v5/types"
"github.com/containers/storage/pkg/homedir"
helperclient "github.com/docker/docker-credential-helpers/client"
Expand Down Expand Up @@ -53,9 +54,23 @@ var (
ErrNotSupported = errors.New("not supported")
)

// SetAuthentication stores the username and password in the auth.json file
// SetAuthentication stores the username and password in the credential helper or file
func SetAuthentication(sys *types.SystemContext, registry, username, password string) error {
helpers, err := sysregistriesv2.CredentialHelpersForRegistry(sys, registry)
if err != nil {
return err
}
for _, helper := range helpers {
if err = setAuthToCredHelper(helper, registry, username, password); err != nil {
logrus.Warnf("error storing credentials for %s to the credential helper %s, %v", registry, helper, err)
continue
}
logrus.Debugf("credentials for %s were stored in the credential helper %s", registry, helper)
return nil
}

return modifyJSON(sys, func(auths *dockerConfigFile) (bool, error) {

if ch, exists := auths.CredHelpers[registry]; exists {
return false, setAuthToCredHelper(ch, registry, username, password)
}
Expand All @@ -80,46 +95,67 @@ func SetAuthentication(sys *types.SystemContext, registry, username, password st
}

// GetAllCredentials returns the registry credentials for all registries stored
// in either the auth.json file or the docker/config.json.
// in the auth.json file, the docker/config.json, and the keyring
func GetAllCredentials(sys *types.SystemContext) (map[string]types.DockerAuthConfig, error) {
// Note: we need to read the auth files in the inverse order to prevent
// Note: we need to update the authConfigs only if the auth for the registry does not exist to prevent
// a priority inversion when writing to the map.
authConfigs := make(map[string]types.DockerAuthConfig)
paths := getAuthFilePaths(sys)
for i := len(paths) - 1; i >= 0; i-- {
path := paths[i]
// readJSONFile returns an empty map in case the path doesn't exist.
auths, err := readJSONFile(path.path, path.legacyFormat)
// query all credentials from the credential helpers
helpers, err := sysregistriesv2.AllConfiguredCredentialHelpers(sys)
if err != nil {
return nil, err
}
for _, helper := range helpers {
creds, err := listAuthFromCredHelper(helper)
if err != nil {
return nil, errors.Wrapf(err, "error reading JSON file %q", path.path)
return nil, err
}

for registry, data := range auths.AuthConfigs {
conf, err := decodeDockerAuth(data)
for registry := range creds {
if _, ok := authConfigs[normalizeRegistry(registry)]; ok {
continue
}
conf, err := getAuthFromCredHelper(helper, registry)
if err != nil {
return nil, err
}
authConfigs[normalizeRegistry(registry)] = conf
}
}

paths := getAuthFilePaths(sys)
for _, path := range paths {
// readJSONFile returns an empty map in case the path doesn't exist.
auths, err := readJSONFile(path.path, path.legacyFormat)
if err != nil {
return nil, errors.Wrapf(err, "error reading JSON file %q", path.path)
}

// Credential helpers may override credentials from the auth file.
for registry, credHelper := range auths.CredHelpers {
username, password, err := getAuthFromCredHelper(credHelper, registry)
if _, ok := authConfigs[normalizeRegistry(registry)]; ok {
continue
}
conf, err := getAuthFromCredHelper(credHelper, registry)
if err != nil {
if credentials.IsErrCredentialsNotFoundMessage(err.Error()) {
continue
}
return nil, err
}
authConfigs[normalizeRegistry(registry)] = conf
}

conf := types.DockerAuthConfig{Username: username, Password: password}
for registry, data := range auths.AuthConfigs {
if _, ok := authConfigs[normalizeRegistry(registry)]; ok {
continue
}
conf, err := decodeDockerAuth(data)
if err != nil {
return nil, err
}
authConfigs[normalizeRegistry(registry)] = conf
}
}

// TODO(keyring): if we ever reenable the keyring support, we had to
// query all credentials from the keyring here.

return authConfigs, nil
}

Expand Down Expand Up @@ -152,6 +188,23 @@ func GetCredentials(sys *types.SystemContext, registry string) (types.DockerAuth
return *sys.DockerAuthConfig, nil
}

helpers, err := sysregistriesv2.CredentialHelpersForRegistry(sys, registry)
if err != nil {
return types.DockerAuthConfig{}, err
}
for _, helper := range helpers {
creds, err := getAuthFromCredHelper(helper, registry)
if err != nil {
if credentials.IsErrCredentialsNotFoundMessage(err.Error()) {
logrus.Debugf("credentials not found in %s, %v", helper, err)
continue
}
return creds, err
}
logrus.Debugf("Returning credentials from credential helper %s", helper)
return creds, nil
}

if enableKeyring {
username, password, err := getAuthFromKernelKeyring(registry)
if err == nil {
Expand All @@ -166,7 +219,7 @@ func GetCredentials(sys *types.SystemContext, registry string) (types.DockerAuth
for _, path := range getAuthFilePaths(sys) {
authConfig, err := findAuthentication(registry, path.path, path.legacyFormat)
if err != nil {
logrus.Debugf("Credentials not found")
logrus.Debugf("Credentials not found, %v", err)
return types.DockerAuthConfig{}, err
}

Expand Down Expand Up @@ -201,6 +254,24 @@ func GetAuthentication(sys *types.SystemContext, registry string) (string, strin

// RemoveAuthentication deletes the credentials stored in auth.json
func RemoveAuthentication(sys *types.SystemContext, registry string) error {
helpers, err := sysregistriesv2.CredentialHelpersForRegistry(sys, registry)
if err != nil {
return err
}
for _, helper := range helpers {
if _, err = getAuthFromCredHelper(helper, registry); err != nil {
if credentials.IsErrCredentialsNotFoundMessage(err.Error()) {
logrus.Debugf("Not logged in to %s with credential helper %s", registry, helper)
}
continue
}
if err = deleteAuthFromCredHelper(helper, registry); err != nil {
return err
}
logrus.Debugf("Credentials were deleted from credential helper %s", helper)
return nil
}

return modifyJSON(sys, func(auths *dockerConfigFile) (bool, error) {
// First try cred helpers.
if ch, exists := auths.CredHelpers[registry]; exists {
Expand Down Expand Up @@ -230,6 +301,24 @@ func RemoveAuthentication(sys *types.SystemContext, registry string) error {

// RemoveAllAuthentication deletes all the credentials stored in auth.json and kernel keyring
func RemoveAllAuthentication(sys *types.SystemContext) error {
helpers, err := sysregistriesv2.AllConfiguredCredentialHelpers(sys)
if err != nil {
return err
}
for _, helper := range helpers {
creds, err := listAuthFromCredHelper(helper)
if err != nil {
return err
}
for registry := range creds {
if err = deleteAuthFromCredHelper(helper, registry); err != nil {
logrus.Warningf("error deleting credentials for %s from %s: %v", registry, helper, err)
} else {
logrus.Debugf("Credentials for %s were deleted from credential helper %s", registry, helper)
}
}
}

return modifyJSON(sys, func(auths *dockerConfigFile) (bool, error) {
if enableKeyring {
err := removeAllAuthFromKernelKeyring()
Expand All @@ -239,12 +328,25 @@ func RemoveAllAuthentication(sys *types.SystemContext) error {
}
logrus.Debugf("error removing credentials from kernel keyring")
}
for registry, helper := range auths.CredHelpers {
if err = deleteAuthFromCredHelper(helper, registry); err != nil {
logrus.Warningf("error deleting credentials for %s from %s: %v", registry, helper, err)
} else {
logrus.Debugf("Credentials for %s were deleted from credential helper %s", registry, helper)
}
}
auths.CredHelpers = make(map[string]string)
auths.AuthConfigs = make(map[string]dockerAuthConfig)
return true, nil
})
}

func listAuthFromCredHelper(credHelper string) (map[string]string, error) {
helperName := fmt.Sprintf("docker-credential-%s", credHelper)
p := helperclient.NewShellProgramFunc(helperName)
return helperclient.List(p)
}

// getPathToAuth gets the path of the auth.json file used for reading and writting credentials
// returns the path, and a bool specifies whether the file is in legacy format
func getPathToAuth(sys *types.SystemContext) (string, bool, error) {
Expand Down Expand Up @@ -353,14 +455,17 @@ func modifyJSON(sys *types.SystemContext, editor func(auths *dockerConfigFile) (
return nil
}

func getAuthFromCredHelper(credHelper, registry string) (string, string, error) {
func getAuthFromCredHelper(credHelper, registry string) (types.DockerAuthConfig, error) {
helperName := fmt.Sprintf("docker-credential-%s", credHelper)
p := helperclient.NewShellProgramFunc(helperName)
creds, err := helperclient.Get(p, registry)
if err != nil {
return "", "", err
return types.DockerAuthConfig{}, err
}
return creds.Username, creds.Secret, nil
return types.DockerAuthConfig{
Username: creds.Username,
Password: creds.Secret,
}, nil
}

func setAuthToCredHelper(credHelper, registry, username, password string) error {
Expand Down Expand Up @@ -389,15 +494,7 @@ func findAuthentication(registry, path string, legacyFormat bool) (types.DockerA

// First try cred helpers. They should always be normalized.
if ch, exists := auths.CredHelpers[registry]; exists {
username, password, err := getAuthFromCredHelper(ch, registry)
if err != nil {
return types.DockerAuthConfig{}, err
}

return types.DockerAuthConfig{
Username: username,
Password: password,
}, nil
return getAuthFromCredHelper(ch, registry)
}

// I'm feeling lucky
Expand Down
44 changes: 43 additions & 1 deletion pkg/docker/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,17 @@ func TestGetAuth(t *testing.T) {
os.Setenv("XDG_RUNTIME_DIR", origXDG)
}()

// override PATH for executing credHelper
curtDir, err := os.Getwd()
require.NoError(t, err)
origPath := os.Getenv("PATH")
newPath := fmt.Sprintf("%s:%s", filepath.Join(curtDir, "testdata"), origPath)
os.Setenv("PATH", newPath)
t.Logf("using PATH: %q", newPath)
defer func() {
os.Setenv("PATH", origPath)
}()

origHomeDir := homedir.Get()
tmpDir2, err := ioutil.TempDir("", "test_docker_client_get_auth")
if err != nil {
Expand Down Expand Up @@ -224,6 +235,17 @@ func TestGetAuth(t *testing.T) {
hostname: "https://localhost:5000",
path: filepath.Join("testdata", "empty.json"),
},
{
name: "credhelper from registries.conf",
hostname: "registry-a.com",
sys: &types.SystemContext{
SystemRegistriesConfPath: filepath.Join("testdata", "cred-helper.conf"),
},
expected: types.DockerAuthConfig{
Username: "foo",
Password: "bar",
},
},
} {
t.Run(tc.name, func(t *testing.T) {
if err := os.RemoveAll(configPath); err != nil {
Expand Down Expand Up @@ -470,7 +492,22 @@ func TestGetAllCredentials(t *testing.T) {
err = tmpFile.Close()
require.NoError(t, err)
authFilePath := tmpFile.Name()
sys := types.SystemContext{AuthFilePath: authFilePath}
// override PATH for executing credHelper
path, err := os.Getwd()
require.NoError(t, err)
origPath := os.Getenv("PATH")
newPath := fmt.Sprintf("%s:%s", filepath.Join(path, "testdata"), origPath)
os.Setenv("PATH", newPath)
t.Logf("using PATH: %q", newPath)
defer func() {
os.Setenv("PATH", origPath)
}()
err = os.Chmod(filepath.Join(path, "testdata", "docker-credential-helper-registry"), os.ModePerm)
require.NoError(t, err)
sys := types.SystemContext{
AuthFilePath: authFilePath,
SystemRegistriesConfPath: filepath.Join("testdata", "cred-helper.conf"),
}

data := []struct {
server string
Expand All @@ -492,6 +529,11 @@ func TestGetAllCredentials(t *testing.T) {
username: "local-user",
password: "local-password",
},
{
server: "registry-a.com",
username: "foo",
password: "bar",
},
}

// Write the credentials to the authfile.
Expand Down
3 changes: 3 additions & 0 deletions pkg/docker/config/testdata/cred-helper.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[[registry]]
credential-helper = "helper-registry"
location = "registry-a.com"
18 changes: 18 additions & 0 deletions pkg/docker/config/testdata/docker-credential-helper-registry
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/bash

case "${1}" in
get)
read REGISTRY
echo "{\"ServerURL\":\"${REGISTRY}\",\"Username\":\"foo\",\"Secret\":\"bar\"}"
exit 0
;;
list)
read UNUSED
echo "{\"registry-a.com\":\"foo\"}"
exit 0
;;
*)
echo "not implemented"
exit 1
;;
esac
Loading

0 comments on commit 8d65474

Please sign in to comment.