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 }