github.com/thajeztah/cli@v0.0.0-20240223162942-dc6bfac81a8b/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/cli/cli/config/configfile" 13 configtypes "github.com/docker/cli/cli/config/types" 14 "github.com/docker/cli/cli/hints" 15 "github.com/docker/cli/cli/streams" 16 "github.com/docker/docker/api/types" 17 registrytypes "github.com/docker/docker/api/types/registry" 18 "github.com/docker/docker/registry" 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 }