github.com/thajeztah/cli@v0.0.0-20240223162942-dc6bfac81a8b/cli/command/registry/login.go (about)

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