github.com/khulnasoft/cli@v0.0.0-20240402070845-01bcad7beefa/cli/command/registry.go (about)

     1  package command
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"runtime"
     9  	"strings"
    10  
    11  	"github.com/distribution/reference"
    12  	"github.com/docker/docker/api/types"
    13  	registrytypes "github.com/docker/docker/api/types/registry"
    14  	"github.com/docker/docker/registry"
    15  	"github.com/khulnasoft/cli/cli/config/configfile"
    16  	configtypes "github.com/khulnasoft/cli/cli/config/types"
    17  	"github.com/khulnasoft/cli/cli/hints"
    18  	"github.com/khulnasoft/cli/cli/streams"
    19  	"github.com/moby/term"
    20  	"github.com/pkg/errors"
    21  )
    22  
    23  const patSuggest = "You can log in with your password or a Personal Access " +
    24  	"Token (PAT). Using a limited-scope PAT grants better security and is required " +
    25  	"for organizations using SSO. Learn more at https://docs.docker.com/go/access-tokens/"
    26  
    27  // RegistryAuthenticationPrivilegedFunc returns a RequestPrivilegeFunc from the specified registry index info
    28  // for the given command.
    29  func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInfo, cmdName string) types.RequestPrivilegeFunc {
    30  	return func() (string, error) {
    31  		fmt.Fprintf(cli.Out(), "\nPlease login prior to %s:\n", cmdName)
    32  		indexServer := registry.GetAuthConfigKey(index)
    33  		isDefaultRegistry := indexServer == registry.IndexServer
    34  		authConfig, err := GetDefaultAuthConfig(cli.ConfigFile(), true, indexServer, isDefaultRegistry)
    35  		if err != nil {
    36  			fmt.Fprintf(cli.Err(), "Unable to retrieve stored credentials for %s, error: %s.\n", indexServer, err)
    37  		}
    38  		err = ConfigureAuth(cli, "", "", &authConfig, isDefaultRegistry)
    39  		if err != nil {
    40  			return "", err
    41  		}
    42  		return registrytypes.EncodeAuthConfig(authConfig)
    43  	}
    44  }
    45  
    46  // ResolveAuthConfig returns auth-config for the given registry from the
    47  // credential-store. It returns an empty AuthConfig if no credentials were
    48  // found.
    49  //
    50  // It is similar to [registry.ResolveAuthConfig], but uses the credentials-
    51  // store, instead of looking up credentials from a map.
    52  func ResolveAuthConfig(cfg *configfile.ConfigFile, index *registrytypes.IndexInfo) registrytypes.AuthConfig {
    53  	configKey := index.Name
    54  	if index.Official {
    55  		configKey = registry.IndexServer
    56  	}
    57  
    58  	a, _ := cfg.GetAuthConfig(configKey)
    59  	return registrytypes.AuthConfig(a)
    60  }
    61  
    62  // GetDefaultAuthConfig gets the default auth config given a serverAddress
    63  // If credentials for given serverAddress exists in the credential store, the configuration will be populated with values in it
    64  func GetDefaultAuthConfig(cfg *configfile.ConfigFile, checkCredStore bool, serverAddress string, isDefaultRegistry bool) (registrytypes.AuthConfig, error) {
    65  	if !isDefaultRegistry {
    66  		serverAddress = registry.ConvertToHostname(serverAddress)
    67  	}
    68  	authconfig := configtypes.AuthConfig{}
    69  	var err error
    70  	if checkCredStore {
    71  		authconfig, err = cfg.GetAuthConfig(serverAddress)
    72  		if err != nil {
    73  			return registrytypes.AuthConfig{
    74  				ServerAddress: serverAddress,
    75  			}, err
    76  		}
    77  	}
    78  	authconfig.ServerAddress = serverAddress
    79  	authconfig.IdentityToken = ""
    80  	return registrytypes.AuthConfig(authconfig), nil
    81  }
    82  
    83  // ConfigureAuth handles prompting of user's username and password if needed
    84  func ConfigureAuth(cli Cli, flUser, flPassword string, authconfig *registrytypes.AuthConfig, isDefaultRegistry bool) error {
    85  	// On Windows, force the use of the regular OS stdin stream.
    86  	//
    87  	// See:
    88  	// - https://github.com/moby/moby/issues/14336
    89  	// - https://github.com/moby/moby/issues/14210
    90  	// - https://github.com/moby/moby/pull/17738
    91  	//
    92  	// TODO(thaJeztah): we need to confirm if this special handling is still needed, as we may not be doing this in other places.
    93  	if runtime.GOOS == "windows" {
    94  		cli.SetIn(streams.NewIn(os.Stdin))
    95  	}
    96  
    97  	// Some links documenting this:
    98  	// - https://code.google.com/archive/p/mintty/issues/56
    99  	// - https://github.com/docker/docker/issues/15272
   100  	// - https://mintty.github.io/ (compatibility)
   101  	// Linux will hit this if you attempt `cat | docker login`, and Windows
   102  	// will hit this if you attempt docker login from mintty where stdin
   103  	// is a pipe, not a character based console.
   104  	if flPassword == "" && !cli.In().IsTerminal() {
   105  		return errors.Errorf("Error: Cannot perform an interactive login from a non TTY device")
   106  	}
   107  
   108  	authconfig.Username = strings.TrimSpace(authconfig.Username)
   109  
   110  	if flUser = strings.TrimSpace(flUser); flUser == "" {
   111  		if isDefaultRegistry {
   112  			// if this is a default registry (docker hub), then display the following message.
   113  			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.")
   114  			if hints.Enabled() {
   115  				fmt.Fprintln(cli.Out(), patSuggest)
   116  				fmt.Fprintln(cli.Out())
   117  			}
   118  		}
   119  		promptWithDefault(cli.Out(), "Username", authconfig.Username)
   120  		var err error
   121  		flUser, err = readInput(cli.In())
   122  		if err != nil {
   123  			return err
   124  		}
   125  		if flUser == "" {
   126  			flUser = authconfig.Username
   127  		}
   128  	}
   129  	if flUser == "" {
   130  		return errors.Errorf("Error: Non-null Username Required")
   131  	}
   132  	if flPassword == "" {
   133  		oldState, err := term.SaveState(cli.In().FD())
   134  		if err != nil {
   135  			return err
   136  		}
   137  		fmt.Fprintf(cli.Out(), "Password: ")
   138  		_ = term.DisableEcho(cli.In().FD(), oldState)
   139  		defer func() {
   140  			_ = term.RestoreTerminal(cli.In().FD(), oldState)
   141  		}()
   142  		flPassword, err = readInput(cli.In())
   143  		if err != nil {
   144  			return err
   145  		}
   146  		fmt.Fprint(cli.Out(), "\n")
   147  		if flPassword == "" {
   148  			return errors.Errorf("Error: Password Required")
   149  		}
   150  	}
   151  
   152  	authconfig.Username = flUser
   153  	authconfig.Password = flPassword
   154  
   155  	return nil
   156  }
   157  
   158  // readInput reads, and returns user input from in. It tries to return a
   159  // single line, not including the end-of-line bytes, and trims leading
   160  // and trailing whitespace.
   161  func readInput(in io.Reader) (string, error) {
   162  	line, _, err := bufio.NewReader(in).ReadLine()
   163  	if err != nil {
   164  		return "", errors.Wrap(err, "error while reading input")
   165  	}
   166  	return strings.TrimSpace(string(line)), nil
   167  }
   168  
   169  func promptWithDefault(out io.Writer, prompt string, configDefault string) {
   170  	if configDefault == "" {
   171  		fmt.Fprintf(out, "%s: ", prompt)
   172  	} else {
   173  		fmt.Fprintf(out, "%s (%s): ", prompt, configDefault)
   174  	}
   175  }
   176  
   177  // RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete
   178  // image. The auth configuration is serialized as a base64url encoded RFC4648,
   179  // section 5) JSON string for sending through the X-Registry-Auth header.
   180  //
   181  // For details on base64url encoding, see:
   182  // - RFC4648, section 5:   https://tools.ietf.org/html/rfc4648#section-5
   183  func RetrieveAuthTokenFromImage(cfg *configfile.ConfigFile, image string) (string, error) {
   184  	// Retrieve encoded auth token from the image reference
   185  	authConfig, err := resolveAuthConfigFromImage(cfg, image)
   186  	if err != nil {
   187  		return "", err
   188  	}
   189  	encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig)
   190  	if err != nil {
   191  		return "", err
   192  	}
   193  	return encodedAuth, nil
   194  }
   195  
   196  // resolveAuthConfigFromImage retrieves that AuthConfig using the image string
   197  func resolveAuthConfigFromImage(cfg *configfile.ConfigFile, image string) (registrytypes.AuthConfig, error) {
   198  	registryRef, err := reference.ParseNormalizedNamed(image)
   199  	if err != nil {
   200  		return registrytypes.AuthConfig{}, err
   201  	}
   202  	repoInfo, err := registry.ParseRepositoryInfo(registryRef)
   203  	if err != nil {
   204  		return registrytypes.AuthConfig{}, err
   205  	}
   206  	return ResolveAuthConfig(cfg, repoInfo.Index), nil
   207  }