From b1d9e8adef7b42194a9e6c178d04aed643f9b524 Mon Sep 17 00:00:00 2001 From: Laura Brehm Date: Tue, 15 Oct 2024 11:38:46 +0100 Subject: [PATCH] Support plaintext credentials as multi-call binary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Docker CLI supports storing/managing credentials without a credential-helper, in which case credentials are fetched from/saved to the CLI config file (`~/.docker/config.json`). This is all managed entirely by the CLI itself, without resort to a separate binary. There are a few issues with this approach – for one, saving the credentials together with all the configurations make it impossible to share one without the other, so one can't for example bind mount the config file into a container without also including all configured credentials. Another issue is that this has made it so that any other clients accessing registry credentials (such as https://github.com/google/go-containerregistry) all have to both: - read/parse the CLI `config.json`, to check for credentials there, which also means they're dependent on this type and might break if the type changes/we need to be careful not to break other codebases parsing this file, and can't change the location where plaintext credentials are stored. - support the credential helper protocol, so that they can access credentials when users do have configured credential helpers. This means that if we want to do something like support oauth credentials by having credential-helpers refresh oauth tokens before returning them, we have to both implement that in each credential-helper and in the CLI itself, and any client directly reading `config.json` will also need to implement this logic. This commit turns the Docker CLI binary into a multicall binary, acting as a standalone credentials helper when invoked as `docker-credential-file`, while still storing/fetching credentials from the configuration file (`~/.docker/config.json`), and without any further changes. This represents a first step into aligning the "no credhelper"/plaintext flow with the "credhelper" flow, meaning that instead of this being an exception where credentials must be read directly from the config file, credentials can now be accessed in the exact same way as with other credential helpers – by invoking `docker-credential-[credhelper name]`, such as `docker-credential-pass`, `docker-credential-osxkeychain` or `docker-credential-wincred`. This would also make it possible for any other clients accessing credentials to untangle themselves from things like the location of the credentials, parsing credentials from `config.json`, etc. and instead simply support the credential-helper protocol, and call the `docker-credential-file` binary as they do others. Signed-off-by: Laura Brehm --- cmd/docker/docker.go | 7 +++++ cmd/docker/file_helper.go | 64 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 cmd/docker/file_helper.go diff --git a/cmd/docker/docker.go b/cmd/docker/docker.go index 8b15b76f8ec7..1269f42734a1 100644 --- a/cmd/docker/docker.go +++ b/cmd/docker/docker.go @@ -29,6 +29,13 @@ import ( ) func main() { + // multi-call binary, if called as file credentials-helper + // then we exec the credhelper flow and exit + if os.Args[0] == fileCredsHelperBinary { + serveFileCredHelper() + return + } + err := dockerMain(context.Background()) if err != nil && !errdefs.IsCancelled(err) { _, _ = fmt.Fprintln(os.Stderr, err) diff --git a/cmd/docker/file_helper.go b/cmd/docker/file_helper.go new file mode 100644 index 000000000000..c44a47a882ee --- /dev/null +++ b/cmd/docker/file_helper.go @@ -0,0 +1,64 @@ +package main + +import ( + "os" + + credhelpers "github.com/docker/docker-credential-helpers/credentials" + + "github.com/docker/cli/cli/config" + "github.com/docker/cli/cli/config/credentials" + "github.com/docker/cli/cli/config/types" +) + +//nolint:gosec // ignore G101: Potential hardcoded credentials +const fileCredsHelperBinary = "docker-credential-file" + +func serveFileCredHelper() { + configfile := config.LoadDefaultConfigFile(os.Stderr) + store := credentials.NewFileStore(configfile) + credhelpers.Serve(&FileHelper{ + fileStore: store, + }) +} + +var _ credhelpers.Helper = &FileHelper{} + +type FileHelper struct { + fileStore credentials.Store +} + +func (f *FileHelper) Add(creds *credhelpers.Credentials) error { + return f.fileStore.Store(types.AuthConfig{ + Username: creds.Username, + Password: creds.Secret, + ServerAddress: creds.ServerURL, + }) +} + +func (f *FileHelper) Delete(serverAddress string) error { + return f.fileStore.Erase(serverAddress) +} + +func (f *FileHelper) Get(serverAddress string) (string, string, error) { + authConfig, err := f.fileStore.Get(serverAddress) + if err != nil { + return "", "", err + } + + return authConfig.Username, authConfig.Password, nil +} + +func (f *FileHelper) List() (map[string]string, error) { + creds := make(map[string]string) + + authConfig, err := f.fileStore.GetAll() + if err != nil { + return nil, err + } + + for k, v := range authConfig { + creds[k] = v.Username + } + + return creds, nil +}