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

cli/config/credentials: refactor DetectDefaultStore and add tests #5568

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

thaJeztah
Copy link
Member

Refactor the DetectDefaultStore to allow testing it cross-platform, and without the actual helpers installed.

This also makes a small change in the logic for detecting the preferred helper. Previously, it only detected the "helper" binary ("pass"), but would fall back to using plain-text if the pass credentials-helper was not installed.

With this patch, it falls back to the platform default (secretservice), before falling back to using no credentials helper (and storing unencrypted).

- Description for the changelog

- A picture of a cute animal (not mandatory but encouraged)

@thaJeztah thaJeztah added this to the 28.0.0 milestone Oct 21, 2024
@thaJeztah thaJeztah self-assigned this Oct 21, 2024
@codecov-commenter
Copy link

codecov-commenter commented Oct 21, 2024

Codecov Report

Attention: Patch coverage is 87.50000% with 3 lines in your changes missing coverage. Please review.

Project coverage is 59.67%. Comparing base (32ff200) to head (555aba2).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #5568      +/-   ##
==========================================
+ Coverage   59.60%   59.67%   +0.07%     
==========================================
  Files         345      343       -2     
  Lines       29103    29113      +10     
==========================================
+ Hits        17346    17373      +27     
+ Misses      10788    10770      -18     
- Partials      969      970       +1     

@thaJeztah thaJeztah requested a review from laurazard October 24, 2024 16:54
Copy link
Member

@laurazard laurazard left a comment

Choose a reason for hiding this comment

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

Left some nits/comments. I'm not super convinced about having the whole pass logic (check for both the pass binary and docker-credential-pass) outside of _linux.go.

Maybe we could have a separate checkForPreferredHelper func that we can override instead, or some other solution that allows to keep this code separate while still making it possible to test cross-platform?

cli/config/credentials/default_store.go Outdated Show resolved Hide resolved
Comment on lines +14 to 18
func DetectDefaultStore(customStore string) string {
if customStore != "" {
// use user-defined
return store
return customStore
}
Copy link
Member

Choose a reason for hiding this comment

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

Sidenote, but

func bork(foo string) string {
	if foo != "" {
		return foo
	}
	...
}

is definitely something 😅 we should see if we could change this for the next major.

Copy link
Member Author

Choose a reason for hiding this comment

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

Right, so I the logic here was that if the config-file has a credentials-store configured, we accept it whatever it is, and consider it an option set by the user; when used, it may error, but that's intentional (you configured a helper, we tried using it, and it failed! intervention is needed).

Copy link
Member Author

Choose a reason for hiding this comment

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

But now the fun bit;

cli/cli/config/config.go

Lines 162 to 172 in 32ff200

func LoadDefaultConfigFile(stderr io.Writer) *configfile.ConfigFile {
configFile, err := load(Dir())
if err != nil {
// FIXME(thaJeztah): we should not proceed here to prevent overwriting existing (but malformed) config files; see https://github.com/docker/cli/issues/5075
_, _ = fmt.Fprintln(stderr, "WARNING: Error", err)
}
if !configFile.ContainsAuth() {
configFile.CredentialsStore = credentials.DetectDefaultStore(configFile.CredentialsStore)
}
return configFile
}

See what's wrong in the logic there? Hint:

if !configFile.ContainsAuth() {

That nice porcelain ContainsAuth() function checks if either a credentials-store is configured or per-registry helpers, or contains credentials;

func (configFile *ConfigFile) ContainsAuth() bool {
return configFile.CredentialsStore != "" ||
len(configFile.CredentialHelpers) > 0 ||
len(configFile.AuthConfigs) > 0
}

I guess we could consider it an extra line of defence to never overwrite what's there, but definitely a bit iffy, and looks like it was there from the start moby/moby@cf721c2

Comment on lines +19 to +29
if preferred := findPreferredHelper(); preferred != "" {
return preferred
}
if defaultHelper == "" {
return ""
}
if _, err := exec.LookPath(remoteCredentialsPrefix + defaultHelper); err != nil {
return ""
}
return defaultHelper
}
Copy link
Member

Choose a reason for hiding this comment

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

Why are we testing for defaultHelper's existence, but not preferred's?

Copy link
Member

Choose a reason for hiding this comment

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

Nevermind, I see that the test for the preferred helper is inside findPreferredHelper. I find this a bit hard to read, maybe because it mixes levels of abstraction. Maybe instead we could do something like

Suggested change
if preferred := findPreferredHelper(); preferred != "" {
return preferred
}
if defaultHelper == "" {
return ""
}
if _, err := exec.LookPath(remoteCredentialsPrefix + defaultHelper); err != nil {
return ""
}
return defaultHelper
}
if preferred := findPreferredHelper(); preferred != "" {
return preferred
}
return findDefaultHelper()
}
func findDefaultHelper() string {
if defaultHelper == "" {
return ""
}
if _, err := exec.LookPath(remoteCredentialsPrefix + defaultHelper); err != nil {
return ""
}
return defaultHelper
}

Comment on lines +52 to +55
// Note that the logic below is specific to detection needed for the
// "pass" credentials-helper on Linux (which is the only platform with
// a "preferred" and "default" helper. This logic may change if a similar
// order of preference is needed on other platforms.
Copy link
Member

Choose a reason for hiding this comment

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

The "detection" logic below is generic, right? It's only used for linux (because linux is the only platform with a preferred helper), but would work for any other platform all the same?

Copy link
Member

Choose a reason for hiding this comment

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

// a "preferred" and "default" helper. This logic may change if a similar
// order of preference is needed on other platforms.

If this comment as a whole is about the preferred vs default helper order of preference, then I'd suggest placing it above, inside DetectDefaultStore near the operations regarding both the preferred and default helpers.

Copy link
Member

Choose a reason for hiding this comment

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

The "detection" logic below is generic, right? It's only used for linux (because linux is the only platform with a preferred helper), but would work for any other platform all the same?

AAAHH, this is about checking for both pass and docker-credential-pass. I wonder if we can make this clearer. Maybe separate this comment from the comment about the order (preferred vs default)? It was somewhat hard to figure out what this comment was about.

Copy link
Member

Choose a reason for hiding this comment

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

I realize you moved this here in order to make this easier to test cross-platform, but this is exactly the type of thing that would make sense to put in a _linux.go file and not leak to the rest of the code – otherwise, we need to have it here with a number of comments explaining why, while it's unnecessary for anyone worrying about any other platform to look at).

Refactor the DetectDefaultStore to allow testing it cross-platform, and
without the actual helpers installed.

This also makes a small change in the logic for detecting the preferred
helper. Previously, it only detected the "helper" binary ("pass"), but
would fall back to using plain-text if the pass credentials-helper was
not installed.

With this patch, it falls back to the platform default (secretservice),
before falling back to using no credentials helper (and storing unencrypted).

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
@thaJeztah thaJeztah force-pushed the refactor_DetectDefaultStore branch from e8e05fd to 555aba2 Compare October 28, 2024 22:26
@thaJeztah
Copy link
Member Author

Thanks for reviewing! I fixed the typo; will have a look at the other bits tomorrow.

I guess we could generalize the for a credentials helper to have a "prerequisites check" (for docker-credentials-pass that would be "check if the pass binary is found". Other helpers would then have an empty func for that.

Perhaps ultimately, the credentials-helpers themselves could have some check (docker-credential-foo --check), but not sure if we want to go that route 😅

@laurazard
Copy link
Member

laurazard commented Oct 30, 2024

Perhaps ultimately, the credentials-helpers themselves could have some check (docker-credential-foo --check), but not sure if we want to go that route

Honestly that sounds like the most sane idea to me – the CLI can't be expected to account for all the different credential helpers and their requirements, they should do it themselves. Maybe even something like the metadata we have for plugins, credential-helpers could have a similar thing that would include if they are "ready" to be used or not, and some error message.

Or the API could just be exit_code == 0 means it's fine, otherwise it's not and it prints some error message.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants