github.com/cspotcode/docker-cli@v20.10.0-rc1.0.20201201121459-3faad7acc5b8+incompatible/cli/command/registry/login.go (about)

     1  package registry
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"strings"
     8  
     9  	"github.com/docker/cli/cli"
    10  	"github.com/docker/cli/cli/command"
    11  	configtypes "github.com/docker/cli/cli/config/types"
    12  	"github.com/docker/docker/api/types"
    13  	registrytypes "github.com/docker/docker/api/types/registry"
    14  	"github.com/docker/docker/client"
    15  	"github.com/docker/docker/registry"
    16  	"github.com/pkg/errors"
    17  	"github.com/spf13/cobra"
    18  )
    19  
    20  const unencryptedWarning = `WARNING! Your password will be stored unencrypted in %s.
    21  Configure a credential helper to remove this warning. See
    22  https://docs.docker.com/engine/reference/commandline/login/#credentials-store
    23  `
    24  
    25  type loginOptions struct {
    26  	serverAddress string
    27  	user          string
    28  	password      string
    29  	passwordStdin bool
    30  }
    31  
    32  // NewLoginCommand creates a new `docker login` command
    33  func NewLoginCommand(dockerCli command.Cli) *cobra.Command {
    34  	var opts loginOptions
    35  
    36  	cmd := &cobra.Command{
    37  		Use:   "login [OPTIONS] [SERVER]",
    38  		Short: "Log in to a Docker registry",
    39  		Long:  "Log in to a Docker registry.\nIf no server is specified, the default is defined by the daemon.",
    40  		Args:  cli.RequiresMaxArgs(1),
    41  		RunE: func(cmd *cobra.Command, args []string) error {
    42  			if len(args) > 0 {
    43  				opts.serverAddress = args[0]
    44  			}
    45  			return runLogin(dockerCli, opts)
    46  		},
    47  	}
    48  
    49  	flags := cmd.Flags()
    50  
    51  	flags.StringVarP(&opts.user, "username", "u", "", "Username")
    52  	flags.StringVarP(&opts.password, "password", "p", "", "Password")
    53  	flags.BoolVarP(&opts.passwordStdin, "password-stdin", "", false, "Take the password from stdin")
    54  
    55  	return cmd
    56  }
    57  
    58  // displayUnencryptedWarning warns the user when using an insecure credential storage.
    59  // After a deprecation period, user will get prompted if stdin and stderr are a terminal.
    60  // Otherwise, we'll assume they want it (sadly), because people may have been scripting
    61  // insecure logins and we don't want to break them. Maybe they'll see the warning in their
    62  // logs and fix things.
    63  func displayUnencryptedWarning(dockerCli command.Streams, filename string) error {
    64  	_, err := fmt.Fprintln(dockerCli.Err(), fmt.Sprintf(unencryptedWarning, filename))
    65  
    66  	return err
    67  }
    68  
    69  type isFileStore interface {
    70  	IsFileStore() bool
    71  	GetFilename() string
    72  }
    73  
    74  func verifyloginOptions(dockerCli command.Cli, opts *loginOptions) error {
    75  	if opts.password != "" {
    76  		fmt.Fprintln(dockerCli.Err(), "WARNING! Using --password via the CLI is insecure. Use --password-stdin.")
    77  		if opts.passwordStdin {
    78  			return errors.New("--password and --password-stdin are mutually exclusive")
    79  		}
    80  	}
    81  
    82  	if opts.passwordStdin {
    83  		if opts.user == "" {
    84  			return errors.New("Must provide --username with --password-stdin")
    85  		}
    86  
    87  		contents, err := ioutil.ReadAll(dockerCli.In())
    88  		if err != nil {
    89  			return err
    90  		}
    91  
    92  		opts.password = strings.TrimSuffix(string(contents), "\n")
    93  		opts.password = strings.TrimSuffix(opts.password, "\r")
    94  	}
    95  	return nil
    96  }
    97  
    98  func runLogin(dockerCli command.Cli, opts loginOptions) error { //nolint: gocyclo
    99  	ctx := context.Background()
   100  	clnt := dockerCli.Client()
   101  	if err := verifyloginOptions(dockerCli, &opts); err != nil {
   102  		return err
   103  	}
   104  	var (
   105  		serverAddress string
   106  		authServer    = command.ElectAuthServer(ctx, dockerCli)
   107  	)
   108  	if opts.serverAddress != "" && opts.serverAddress != registry.DefaultNamespace {
   109  		serverAddress = opts.serverAddress
   110  	} else {
   111  		serverAddress = authServer
   112  	}
   113  
   114  	var err error
   115  	var authConfig *types.AuthConfig
   116  	var response registrytypes.AuthenticateOKBody
   117  	isDefaultRegistry := serverAddress == authServer
   118  	authConfig, err = command.GetDefaultAuthConfig(dockerCli, opts.user == "" && opts.password == "", serverAddress, isDefaultRegistry)
   119  	if err == nil && authConfig.Username != "" && authConfig.Password != "" {
   120  		response, err = loginWithCredStoreCreds(ctx, dockerCli, authConfig)
   121  	}
   122  	if err != nil || authConfig.Username == "" || authConfig.Password == "" {
   123  		err = command.ConfigureAuth(dockerCli, opts.user, opts.password, authConfig, isDefaultRegistry)
   124  		if err != nil {
   125  			return err
   126  		}
   127  
   128  		response, err = clnt.RegistryLogin(ctx, *authConfig)
   129  		if err != nil && client.IsErrConnectionFailed(err) {
   130  			// If the server isn't responding (yet) attempt to login purely client side
   131  			response, err = loginClientSide(ctx, *authConfig)
   132  		}
   133  		// If we (still) have an error, give up
   134  		if err != nil {
   135  			return err
   136  		}
   137  	}
   138  	if response.IdentityToken != "" {
   139  		authConfig.Password = ""
   140  		authConfig.IdentityToken = response.IdentityToken
   141  	}
   142  
   143  	creds := dockerCli.ConfigFile().GetCredentialsStore(serverAddress)
   144  
   145  	store, isDefault := creds.(isFileStore)
   146  	// Display a warning if we're storing the users password (not a token)
   147  	if isDefault && authConfig.Password != "" {
   148  		err = displayUnencryptedWarning(dockerCli, store.GetFilename())
   149  		if err != nil {
   150  			return err
   151  		}
   152  	}
   153  
   154  	if err := creds.Store(configtypes.AuthConfig(*authConfig)); err != nil {
   155  		return errors.Errorf("Error saving credentials: %v", err)
   156  	}
   157  
   158  	if response.Status != "" {
   159  		fmt.Fprintln(dockerCli.Out(), response.Status)
   160  	}
   161  	return nil
   162  }
   163  
   164  func loginWithCredStoreCreds(ctx context.Context, dockerCli command.Cli, authConfig *types.AuthConfig) (registrytypes.AuthenticateOKBody, error) {
   165  	fmt.Fprintf(dockerCli.Out(), "Authenticating with existing credentials...\n")
   166  	cliClient := dockerCli.Client()
   167  	response, err := cliClient.RegistryLogin(ctx, *authConfig)
   168  	if err != nil {
   169  		if client.IsErrUnauthorized(err) {
   170  			fmt.Fprintf(dockerCli.Err(), "Stored credentials invalid or expired\n")
   171  		} else {
   172  			fmt.Fprintf(dockerCli.Err(), "Login did not succeed, error: %s\n", err)
   173  		}
   174  	}
   175  	return response, err
   176  }
   177  
   178  func loginClientSide(ctx context.Context, auth types.AuthConfig) (registrytypes.AuthenticateOKBody, error) {
   179  	svc, err := registry.NewService(registry.ServiceOptions{})
   180  	if err != nil {
   181  		return registrytypes.AuthenticateOKBody{}, err
   182  	}
   183  
   184  	status, token, err := svc.Auth(ctx, &auth, command.UserAgent())
   185  
   186  	return registrytypes.AuthenticateOKBody{
   187  		Status:        status,
   188  		IdentityToken: token,
   189  	}, err
   190  }