Refactor `cli/command/registry`

Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit 6e4818e7d6)
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
This commit is contained in:
Laura Brehm 2024-07-08 23:48:13 +01:00
parent 0c29d6bac1
commit b8a38fd22d
No known key found for this signature in database
GPG Key ID: 08EC1B0491948487
3 changed files with 150 additions and 70 deletions

View File

@ -41,7 +41,7 @@ func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInf
default: default:
} }
err = ConfigureAuth(ctx, cli, "", "", &authConfig, isDefaultRegistry) authConfig, err = PromptUserForCredentials(ctx, cli, "", "", authConfig.Username, indexServer)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -86,8 +86,32 @@ func GetDefaultAuthConfig(cfg *configfile.ConfigFile, checkCredStore bool, serve
return registrytypes.AuthConfig(authconfig), nil return registrytypes.AuthConfig(authconfig), nil
} }
// ConfigureAuth handles prompting of user's username and password if needed // ConfigureAuth handles prompting of user's username and password if needed.
func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, authconfig *registrytypes.AuthConfig, isDefaultRegistry bool) error { // Deprecated: use PromptUserForCredentials instead.
func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, authConfig *registrytypes.AuthConfig, _ bool) error {
defaultUsername := authConfig.Username
serverAddress := authConfig.ServerAddress
newAuthConfig, err := PromptUserForCredentials(ctx, cli, flUser, flPassword, defaultUsername, serverAddress)
if err != nil {
return err
}
authConfig.Username = newAuthConfig.Username
authConfig.Password = newAuthConfig.Password
return nil
}
// PromptUserForCredentials handles the CLI prompt for the user to input
// credentials.
// If argUser is not empty, then the user is only prompted for their password.
// If argPassword is not empty, then the user is only prompted for their username
// If neither argUser nor argPassword are empty, then the user is not prompted and
// an AuthConfig is returned with those values.
// If defaultUsername is not empty, the username prompt includes that username
// and the user can hit enter without inputting a username to use that default
// username.
func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword, defaultUsername, serverAddress string) (authConfig registrytypes.AuthConfig, err error) {
// On Windows, force the use of the regular OS stdin stream. // On Windows, force the use of the regular OS stdin stream.
// //
// See: // See:
@ -107,13 +131,14 @@ func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, auth
// Linux will hit this if you attempt `cat | docker login`, and Windows // Linux will hit this if you attempt `cat | docker login`, and Windows
// will hit this if you attempt docker login from mintty where stdin // will hit this if you attempt docker login from mintty where stdin
// is a pipe, not a character based console. // is a pipe, not a character based console.
if flPassword == "" && !cli.In().IsTerminal() { if argPassword == "" && !cli.In().IsTerminal() {
return errors.Errorf("Error: Cannot perform an interactive login from a non TTY device") return authConfig, errors.Errorf("Error: Cannot perform an interactive login from a non TTY device")
} }
authconfig.Username = strings.TrimSpace(authconfig.Username) isDefaultRegistry := serverAddress == registry.IndexServer
defaultUsername = strings.TrimSpace(defaultUsername)
if flUser = strings.TrimSpace(flUser); flUser == "" { if argUser = strings.TrimSpace(argUser); argUser == "" {
if isDefaultRegistry { if isDefaultRegistry {
// if this is a default registry (docker hub), then display the following message. // if this is a default registry (docker hub), then display the following message.
fmt.Fprintln(cli.Out(), "Log in with your Docker ID or email address to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com/ to create one.") fmt.Fprintln(cli.Out(), "Log in with your Docker ID or email address to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com/ to create one.")
@ -124,44 +149,43 @@ func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, auth
} }
var prompt string var prompt string
if authconfig.Username == "" { if defaultUsername == "" {
prompt = "Username: " prompt = "Username: "
} else { } else {
prompt = fmt.Sprintf("Username (%s): ", authconfig.Username) prompt = fmt.Sprintf("Username (%s): ", defaultUsername)
} }
var err error argUser, err = PromptForInput(ctx, cli.In(), cli.Out(), prompt)
flUser, err = PromptForInput(ctx, cli.In(), cli.Out(), prompt)
if err != nil { if err != nil {
return err return authConfig, err
} }
if flUser == "" { if argUser == "" {
flUser = authconfig.Username argUser = defaultUsername
} }
} }
if flUser == "" { if argUser == "" {
return errors.Errorf("Error: Non-null Username Required") return authConfig, errors.Errorf("Error: Non-null Username Required")
} }
if flPassword == "" { if argPassword == "" {
restoreInput, err := DisableInputEcho(cli.In()) restoreInput, err := DisableInputEcho(cli.In())
if err != nil { if err != nil {
return err return authConfig, err
} }
defer restoreInput() defer restoreInput()
flPassword, err = PromptForInput(ctx, cli.In(), cli.Out(), "Password: ") argPassword, err = PromptForInput(ctx, cli.In(), cli.Out(), "Password: ")
if err != nil { if err != nil {
return err return authConfig, err
} }
fmt.Fprint(cli.Out(), "\n") fmt.Fprint(cli.Out(), "\n")
if flPassword == "" { if argPassword == "" {
return errors.Errorf("Error: Password Required") return authConfig, errors.Errorf("Error: Password Required")
} }
} }
authconfig.Username = flUser authConfig.Username = argUser
authconfig.Password = flPassword authConfig.Password = argPassword
authConfig.ServerAddress = serverAddress
return nil return authConfig, nil
} }
// RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete // RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete

View File

@ -101,90 +101,146 @@ func verifyloginOptions(dockerCli command.Cli, opts *loginOptions) error {
return nil return nil
} }
func runLogin(ctx context.Context, dockerCli command.Cli, opts loginOptions) error { //nolint:gocyclo func runLogin(ctx context.Context, dockerCli command.Cli, opts loginOptions) error {
clnt := dockerCli.Client()
if err := verifyloginOptions(dockerCli, &opts); err != nil { if err := verifyloginOptions(dockerCli, &opts); err != nil {
return err return err
} }
var ( var (
serverAddress string serverAddress string
response registrytypes.AuthenticateOKBody response *registrytypes.AuthenticateOKBody
) )
if opts.serverAddress != "" && opts.serverAddress != registry.DefaultNamespace { if opts.serverAddress != "" && opts.serverAddress != registry.DefaultNamespace {
serverAddress = opts.serverAddress serverAddress = opts.serverAddress
} else { } else {
serverAddress = registry.IndexServer serverAddress = registry.IndexServer
} }
isDefaultRegistry := serverAddress == registry.IndexServer isDefaultRegistry := serverAddress == registry.IndexServer
// attempt login with current (stored) credentials
authConfig, err := command.GetDefaultAuthConfig(dockerCli.ConfigFile(), opts.user == "" && opts.password == "", serverAddress, isDefaultRegistry) authConfig, err := command.GetDefaultAuthConfig(dockerCli.ConfigFile(), opts.user == "" && opts.password == "", serverAddress, isDefaultRegistry)
if err == nil && authConfig.Username != "" && authConfig.Password != "" { if err == nil && authConfig.Username != "" && authConfig.Password != "" {
response, err = loginWithCredStoreCreds(ctx, dockerCli, &authConfig) response, err = loginWithStoredCredentials(ctx, dockerCli, authConfig)
} }
if err != nil || authConfig.Username == "" || authConfig.Password == "" {
if isDefaultRegistry && opts.user == "" && opts.password == "" {
// todo(laurazard: clean this up
store := dockerCli.ConfigFile().GetCredentialsStore(serverAddress)
oauthAuthConfig, err := manager.NewManager(store).LoginDevice(ctx, dockerCli.Err())
if err != nil {
return err
}
authConfig = registrytypes.AuthConfig(*oauthAuthConfig)
} else {
err = command.ConfigureAuth(ctx, dockerCli, opts.user, opts.password, &authConfig, isDefaultRegistry)
if err != nil {
return err
}
}
response, err = clnt.RegistryLogin(ctx, authConfig) // if we failed to authenticate with stored credentials (or didn't have stored credentials),
if err != nil && client.IsErrConnectionFailed(err) { // prompt the user for new credentials
// If the server isn't responding (yet) attempt to login purely client side if err != nil || authConfig.Username == "" || authConfig.Password == "" {
response, err = loginClientSide(ctx, authConfig) response, err = loginUser(ctx, dockerCli, opts, authConfig.Username, serverAddress)
}
// If we (still) have an error, give up
if err != nil { if err != nil {
return err return err
} }
} }
if response != nil && response.Status != "" {
_, _ = fmt.Fprintln(dockerCli.Out(), response.Status)
}
return nil
}
func loginWithStoredCredentials(ctx context.Context, dockerCli command.Cli, authConfig registrytypes.AuthConfig) (*registrytypes.AuthenticateOKBody, error) {
_, _ = fmt.Fprintf(dockerCli.Out(), "Authenticating with existing credentials...\n")
response, err := dockerCli.Client().RegistryLogin(ctx, authConfig)
if err != nil {
if errdefs.IsUnauthorized(err) {
_, _ = fmt.Fprintf(dockerCli.Err(), "Stored credentials invalid or expired\n")
} else {
_, _ = fmt.Fprintf(dockerCli.Err(), "Login did not succeed, error: %s\n", err)
}
}
if response.IdentityToken != "" { if response.IdentityToken != "" {
authConfig.Password = "" authConfig.Password = ""
authConfig.IdentityToken = response.IdentityToken authConfig.IdentityToken = response.IdentityToken
} }
creds := dockerCli.ConfigFile().GetCredentialsStore(serverAddress) if err := storeCredentials(dockerCli, authConfig); err != nil {
return nil, err
}
return &response, err
}
func loginUser(ctx context.Context, dockerCli command.Cli, opts loginOptions, defaultUsername, serverAddress string) (*registrytypes.AuthenticateOKBody, error) {
// If we're logging into the index server and the user didn't provide a username or password, use the device flow
if serverAddress == registry.IndexServer && opts.user == "" && opts.password == "" {
return loginWithDeviceCodeFlow(ctx, dockerCli)
} else {
return loginWithUsernameAndPassword(ctx, dockerCli, opts, defaultUsername, serverAddress)
}
}
func loginWithUsernameAndPassword(ctx context.Context, dockerCli command.Cli, opts loginOptions, defaultUsername, serverAddress string) (*registrytypes.AuthenticateOKBody, error) {
// Prompt user for credentials
authConfig, err := command.PromptUserForCredentials(ctx, dockerCli, opts.user, opts.password, defaultUsername, serverAddress)
if err != nil {
return nil, err
}
response, err := loginWithRegistry(ctx, dockerCli, authConfig)
if err != nil {
return nil, err
}
if response.IdentityToken != "" {
authConfig.Password = ""
authConfig.IdentityToken = response.IdentityToken
}
if err = storeCredentials(dockerCli, authConfig); err != nil {
return nil, err
}
return &response, nil
}
func loginWithDeviceCodeFlow(ctx context.Context, dockerCli command.Cli) (*registrytypes.AuthenticateOKBody, error) {
store := dockerCli.ConfigFile().GetCredentialsStore(registry.IndexServer)
authConfig, err := manager.NewManager(store).LoginDevice(ctx, dockerCli.Err())
if err != nil {
return nil, err
}
response, err := loginWithRegistry(ctx, dockerCli, registrytypes.AuthConfig(*authConfig))
if err != nil {
return nil, err
}
if err = storeCredentials(dockerCli, registrytypes.AuthConfig(*authConfig)); err != nil {
return nil, err
}
return &response, nil
}
func storeCredentials(dockerCli command.Cli, authConfig registrytypes.AuthConfig) error {
creds := dockerCli.ConfigFile().GetCredentialsStore(authConfig.ServerAddress)
store, isDefault := creds.(isFileStore) store, isDefault := creds.(isFileStore)
// Display a warning if we're storing the users password (not a token) // Display a warning if we're storing the users password (not a token)
if isDefault && authConfig.Password != "" { if isDefault && authConfig.Password != "" {
err = displayUnencryptedWarning(dockerCli, store.GetFilename()) err := displayUnencryptedWarning(dockerCli, store.GetFilename())
if err != nil { if err != nil {
return err return err
} }
} }
if err := creds.Store(configtypes.AuthConfig(authConfig)); err != nil { if err := creds.Store(configtypes.AuthConfig(authConfig)); err != nil {
return errors.Errorf("Error saving credentials: %v", err) return errors.Errorf("Error saving credentials: %v", err)
} }
if response.Status != "" {
fmt.Fprintln(dockerCli.Out(), response.Status)
}
return nil return nil
} }
func loginWithCredStoreCreds(ctx context.Context, dockerCli command.Cli, authConfig *registrytypes.AuthConfig) (registrytypes.AuthenticateOKBody, error) { func loginWithRegistry(ctx context.Context, dockerCli command.Cli, authConfig registrytypes.AuthConfig) (registrytypes.AuthenticateOKBody, error) {
fmt.Fprintf(dockerCli.Out(), "Authenticating with existing credentials...\n") response, err := dockerCli.Client().RegistryLogin(ctx, authConfig)
cliClient := dockerCli.Client() if err != nil && client.IsErrConnectionFailed(err) {
response, err := cliClient.RegistryLogin(ctx, *authConfig) // If the server isn't responding (yet) attempt to login purely client side
if err != nil { response, err = loginClientSide(ctx, authConfig)
if errdefs.IsUnauthorized(err) {
fmt.Fprintf(dockerCli.Err(), "Stored credentials invalid or expired\n")
} else {
fmt.Fprintf(dockerCli.Err(), "Login did not succeed, error: %s\n", err)
}
} }
return response, err // If we (still) have an error, give up
if err != nil {
return registrytypes.AuthenticateOKBody{}, err
}
return response, nil
} }
func loginClientSide(ctx context.Context, auth registrytypes.AuthConfig) (registrytypes.AuthenticateOKBody, error) { func loginClientSide(ctx context.Context, auth registrytypes.AuthConfig) (registrytypes.AuthenticateOKBody, error) {

View File

@ -74,7 +74,7 @@ func TestLoginWithCredStoreCreds(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{}) cli := test.NewFakeCli(&fakeClient{})
errBuf := new(bytes.Buffer) errBuf := new(bytes.Buffer)
cli.SetErr(streams.NewOut(errBuf)) cli.SetErr(streams.NewOut(errBuf))
loginWithCredStoreCreds(ctx, cli, &tc.inputAuthConfig) loginWithStoredCredentials(ctx, cli, tc.inputAuthConfig)
outputString := cli.OutBuffer().String() outputString := cli.OutBuffer().String()
assert.Check(t, is.Equal(tc.expectedMsg, outputString)) assert.Check(t, is.Equal(tc.expectedMsg, outputString))
errorString := errBuf.String() errorString := errBuf.String()