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

Implement credsStore in credential config files #1179

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
49 changes: 49 additions & 0 deletions pkg/docker/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type dockerAuthConfig struct {
type dockerConfigFile struct {
AuthConfigs map[string]dockerAuthConfig `json:"auths"`
CredHelpers map[string]string `json:"credHelpers,omitempty"`
CredsStore string `json:"credsStore,omitempty"`
}

type authPath struct {
Expand Down Expand Up @@ -85,6 +86,9 @@ func SetCredentials(sys *types.SystemContext, key, username, password string) (s
}
return false, setAuthToCredHelper(ch, key, username, password)
}
if auths.CredsStore != "" {
return false, setAuthToCredHelper(auths.CredsStore, key, username, password)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep the order of handling CredHelpers/CredsStore/AuthConfigs consistent throughout the package, to make it easier to check that all operations handle the same data. (I guess in the order above, unless there’s a specific reason to do something else.)

creds := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
newCreds := dockerAuthConfig{Auth: creds}
auths.AuthConfigs[key] = newCreds
Expand Down Expand Up @@ -156,6 +160,16 @@ func GetAllCredentials(sys *types.SystemContext) (map[string]types.DockerAuthCon
for registry := range auths.AuthConfigs {
addRegistry(registry)
}
if auths.CredsStore != "" {
creds, err := listAuthsFromCredHelper(auths.CredsStore)
if err == nil {
for registry := range creds {
addRegistry(registry)
}
} else {
return nil, err
}
}
}
// External helpers.
default:
Expand Down Expand Up @@ -390,6 +404,21 @@ func RemoveAuthentication(sys *types.SystemContext, key string) error {
if innerHelper, exists := auths.CredHelpers[key]; exists {
removeFromCredHelper(innerHelper)
}
if auths.CredsStore != "" {
c, err := getAuthFromCredHelper(auths.CredsStore, key)
if err != nil {
multiErr = multierror.Append(multiErr, errors.Wrapf(err, "removing credentials for %s from credential helper %s", key, auths.CredsStore))
} else {
if c.Username != "" || c.IdentityToken != "" {
err = deleteAuthFromCredHelper(auths.CredsStore, key)
if err != nil {
multiErr = multierror.Append(multiErr, errors.Wrapf(err, "removing credentials for %s from credential helper %s", key, auths.CredsStore))
} else {
isLoggedIn = true
}
}
}
}
if _, ok := auths.AuthConfigs[key]; ok {
isLoggedIn = true
delete(auths.AuthConfigs, key)
Expand Down Expand Up @@ -440,6 +469,20 @@ func RemoveAllAuthentication(sys *types.SystemContext) error {
}
auths.CredHelpers = make(map[string]string)
auths.AuthConfigs = make(map[string]dockerAuthConfig)
if auths.CredsStore != "" {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RemoveAuthentication also needs adding the support.

creds, err := listAuthsFromCredHelper(auths.CredsStore)
if err == nil {
for registry := range creds {
err = deleteAuthFromCredHelper(auths.CredsStore, registry)
if err != nil {
return false, err
}
}
} else {
return false, err
}

}
return true, nil
})
// External helpers.
Expand Down Expand Up @@ -646,6 +689,12 @@ func findAuthentication(ref reference.Named, registry, path string, legacyFormat
return getAuthFromCredHelper(ch, registry)
}

if auths.CredsStore != "" {
if cred, err := getAuthFromCredHelper(auths.CredsStore, registry); err == nil {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it OK to completely ignore errors here?

return cred, err
}
}

// Support for different paths in auth.
// (This is not a feature of ~/.docker/config.json; we support it even for
// those files as an extension.)
Expand Down
112 changes: 112 additions & 0 deletions pkg/docker/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,33 @@ import (
"github.com/stretchr/testify/require"
)

func TestMain(m *testing.M) {
// fake homedir
// let's hope nobody uses C getpwuid()
tmp, err := ioutil.TempDir("", "testHome")
if err != nil {
fmt.Fprintf(os.Stderr, "failed to create tmp file: %v\n", err)
os.Exit(1)
}

homeEnvVar := "HOME"
if runtime.GOOS == "windows" {
homeEnvVar = "USERPROFILE"
}

oldHome, hadHome := os.LookupEnv(homeEnvVar)
defer func() {
if hadHome {
os.Setenv(homeEnvVar, oldHome)
} else {
os.Unsetenv(homeEnvVar)
}
}()

os.Setenv(homeEnvVar, tmp)
m.Run()
}

func TestGetPathToAuth(t *testing.T) {
const linux = "linux"
const darwin = "darwin"
Expand Down Expand Up @@ -636,6 +663,91 @@ func TestGetAllCredentials(t *testing.T) {
require.Equal(t, d.password, conf.Password, "%v", d)
}

// test "credStore"
f, err := os.OpenFile(authFilePath, os.O_RDWR|os.O_TRUNC, 0644)
require.NoError(t, err)
_, err = f.Write([]byte(`{ "credsStore": "helper-registry" }`))
require.NoError(t, err)
err = f.Close()
require.NoError(t, err)
authConfigs, err = GetAllCredentials(&sys)
require.NoError(t, err)
require.Equal(t, 1, len(authConfigs))
require.Equal(t, "foo", authConfigs["registry-a.com"].Username)
require.Equal(t, "bar", authConfigs["registry-a.com"].Password)
}

func TestCredStore(t *testing.T) {
// Create a temporary authentication file.
tmpFile, err := ioutil.TempFile("", "auth.json.")
require.NoError(t, err)
authFilePath := tmpFile.Name()
defer os.Remove(authFilePath)
_, err = tmpFile.Write([]byte(`{ "credsStore": "mock-helper" }`))
require.NoError(t, err)
err = tmpFile.Close()
require.NoError(t, err)
require.NoError(t, err)

sys := types.SystemContext{
AuthFilePath: authFilePath,
}

// Set up path to mock cred helper
path, err := os.Getwd()
if err != nil {
t.Fatal(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 os.Setenv("PATH", origPath)

// Set up temporary store for mock cred helper
mockHelperStore, err := ioutil.TempFile("", "cred-helper-store.json")
require.NoError(t, err)
defer os.Remove(mockHelperStore.Name())
_, err = mockHelperStore.Write([]byte("{}"))
require.NoError(t, err)
err = mockHelperStore.Close()
require.NoError(t, err)
os.Setenv("CRED_HELPER_STORE_FILE", mockHelperStore.Name())

expectedCreds := map[string]types.DockerAuthConfig{
"quay.io": {Username: "test-user", Password: "test-pwd"},
"docker.io": {Username: "test-user-2", Password: "test-pwd-2"},
"example.io": {Username: "test-user-3", Password: "test-pwd-3"},
}

for server, cred := range expectedCreds {
err = SetAuthentication(&sys, server, cred.Username, cred.Password)
require.NoError(t, err)
}

creds, err := GetAllCredentials(&sys)
require.NoError(t, err)
require.Equal(t, expectedCreds, creds)

cred, err := GetCredentials(&sys, "quay.io")
require.NoError(t, err)
require.Equal(t, types.DockerAuthConfig{Username: "test-user", Password: "test-pwd"}, cred)

err = RemoveAuthentication(&sys, "quay.io")
require.NoError(t, err)

err = RemoveAuthentication(&sys, "quay.io")
require.ErrorIs(t, err, ErrNotLoggedIn)

creds, err = GetAllCredentials(&sys)
require.NoError(t, err)
require.Equal(t, 2, len(creds))

err = RemoveAllAuthentication(&sys)
require.NoError(t, err)
creds, err = GetAllCredentials(&sys)
require.NoError(t, err)
require.Equal(t, 0, len(creds))
}

func TestAuthKeysForRef(t *testing.T) {
Expand Down
3 changes: 3 additions & 0 deletions pkg/docker/config/testdata/docker-credential-mock-helper
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env sh

exec /usr/bin/env go run ./testdata/docker-credential-mock-helper.go "$@"
126 changes: 126 additions & 0 deletions pkg/docker/config/testdata/docker-credential-mock-helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
///usr/bin/true; exec /usr/bin/env go run "$0" "$@"

package main

import (
"encoding/json"
"fmt"
"github.com/pkg/errors"
"io/ioutil"
"os"
"strings"
"syscall"
)

type userPasswordPair struct {
User, Password string
}

type serverUserSecretTriple struct {
ServerURL, Username, Secret string
}

func load() (map[string]userPasswordPair, error) {
credentials := make(map[string]userPasswordPair)
fname, ok := os.LookupEnv("CRED_HELPER_STORE_FILE")
if !ok {
return credentials, fmt.Errorf("the CRED_HELPER_STORE_FILE envvar not set")
}
f, err := os.OpenFile(fname, os.O_RDONLY, 0644)
if err != nil {
return credentials, err
}
defer f.Close()
dec := json.NewDecoder(f)
err = dec.Decode(&credentials)
return credentials, err
}

func store(credentials map[string]userPasswordPair) error {
fname, ok := os.LookupEnv("CRED_HELPER_STORE_FILE")
if !ok {
return fmt.Errorf("the CRED_HELPER_STORE_FILE envvar not set")
}
f, err := os.OpenFile(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return err
}
defer f.Close()
enc := json.NewEncoder(f)
return enc.Encode(&credentials)

}

func fatal(err error) {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
var se syscall.Errno
if ok := errors.As(err, &se); ok {
os.Exit(int(se))
}
os.Exit(-1)
}

func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "exactly one argument expected\n")
os.Exit(int(syscall.EINVAL))
}

credentials, err := load()
if err != nil {
fatal(err)
}

switch os.Args[1] {
case "list":
outData := make(map[string]string)
for server, up := range credentials {
outData[server] = up.User
}
enc := json.NewEncoder(os.Stdout)
err = enc.Encode(&outData)
if err != nil {
fatal(err)
}
case "get":
inData, err := ioutil.ReadAll(os.Stdin)
if err != nil {
fatal(err)
}
serverURL := string(inData)
serverURL = strings.Trim(serverURL, "\r\n")
up := credentials[serverURL]
outData := serverUserSecretTriple{ServerURL: serverURL, Username: up.User, Secret: up.Password}
enc := json.NewEncoder(os.Stdout)
if err = enc.Encode(outData); err != nil {
fatal(err)
}
case "erase":
inData, err := ioutil.ReadAll(os.Stdin)
if err != nil {
fatal(err)
}
serverURL := string(inData)
serverURL = strings.Trim(serverURL, "\r\n")
delete(credentials, serverURL)
err = store(credentials)
if err != nil {
fatal(err)
}
case "store":
inData := serverUserSecretTriple{}
enc := json.NewDecoder(os.Stdin)
err = enc.Decode(&inData)
if err != nil {
fatal(err)
}
credentials[inData.ServerURL] = userPasswordPair{User: inData.Username, Password: inData.Secret}
err = store(credentials)
if err != nil {
fatal(err)
}
default:
fmt.Fprintf(os.Stderr, "unknown sub-command %s\n", os.Args[1])
os.Exit(int(syscall.EINVAL))
}
}