File: auth.go

package info (click to toggle)
golang-github-containers-common 0.33.4%2Bds1-1
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 856 kB
  • sloc: makefile: 118; sh: 25
file content (269 lines) | stat: -rw-r--r-- 9,315 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
package auth

import (
	"bufio"
	"context"
	"fmt"
	"os"
	"strings"

	"github.com/containers/image/v5/docker"
	"github.com/containers/image/v5/pkg/docker/config"
	"github.com/containers/image/v5/pkg/sysregistriesv2"
	"github.com/containers/image/v5/types"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"golang.org/x/crypto/ssh/terminal"
)

// GetDefaultAuthFile returns env value REGISTRY_AUTH_FILE as default
// --authfile path used in multiple --authfile flag definitions
// Will fail over to DOCKER_CONFIG if REGISTRY_AUTH_FILE environment is not set
func GetDefaultAuthFile() string {
	authfile := os.Getenv("REGISTRY_AUTH_FILE")
	if authfile == "" {
		if authfile, ok := os.LookupEnv("DOCKER_CONFIG"); ok {
			logrus.Infof("Using DOCKER_CONFIG environment variable for authfile path %s", authfile)
		}
	}
	return authfile
}

// CheckAuthFile validates filepath given by --authfile
// used by command has --authfile flag
func CheckAuthFile(authfile string) error {
	if authfile == "" {
		return nil
	}
	if _, err := os.Stat(authfile); err != nil {
		return errors.Wrapf(err, "error checking authfile path %s", authfile)
	}
	return nil
}

// systemContextWithOptions returns a version of sys
// updated with authFile and certDir values (if they are not "").
// NOTE: this is a shallow copy that can be used and updated, but may share
// data with the original parameter.
func systemContextWithOptions(sys *types.SystemContext, authFile, certDir string) *types.SystemContext {
	if sys != nil {
		sysCopy := *sys
		sys = &sysCopy
	} else {
		sys = &types.SystemContext{}
	}

	if authFile != "" {
		sys.AuthFilePath = authFile
	}
	if certDir != "" {
		sys.DockerCertPath = certDir
	}
	return sys
}

// Login implements a “log in” command with the provided opts and args
// reading the password from opts.Stdin or the options in opts.
func Login(ctx context.Context, systemContext *types.SystemContext, opts *LoginOptions, args []string) error {
	systemContext = systemContextWithOptions(systemContext, opts.AuthFile, opts.CertDir)

	var (
		server string
		err    error
	)
	if len(args) > 1 {
		return errors.Errorf("login accepts only one registry to login to")
	}
	if len(args) == 0 {
		if !opts.AcceptUnspecifiedRegistry {
			return errors.Errorf("please provide a registry to login to")
		}
		if server, err = defaultRegistryWhenUnspecified(systemContext); err != nil {
			return err
		}
		logrus.Debugf("registry not specified, default to the first registry %q from registries.conf", server)
	} else {
		server = getRegistryName(args[0])
	}
	authConfig, err := config.GetCredentials(systemContext, server)
	if err != nil {
		return errors.Wrapf(err, "error reading auth file")
	}
	if opts.GetLoginSet {
		if authConfig.Username == "" {
			return errors.Errorf("not logged into %s", server)
		}
		fmt.Fprintf(opts.Stdout, "%s\n", authConfig.Username)
		return nil
	}
	if authConfig.IdentityToken != "" {
		return errors.Errorf("currently logged in, auth file contains an Identity token")
	}

	password := opts.Password
	if opts.StdinPassword {
		var stdinPasswordStrBuilder strings.Builder
		if opts.Password != "" {
			return errors.Errorf("Can't specify both --password-stdin and --password")
		}
		if opts.Username == "" {
			return errors.Errorf("Must provide --username with --password-stdin")
		}
		scanner := bufio.NewScanner(opts.Stdin)
		for scanner.Scan() {
			fmt.Fprint(&stdinPasswordStrBuilder, scanner.Text())
		}
		password = stdinPasswordStrBuilder.String()
	}

	// If no username and no password is specified, try to use existing ones.
	if opts.Username == "" && password == "" && authConfig.Username != "" && authConfig.Password != "" {
		fmt.Println("Authenticating with existing credentials...")
		if err := docker.CheckAuth(ctx, systemContext, authConfig.Username, authConfig.Password, server); err == nil {
			fmt.Fprintln(opts.Stdout, "Existing credentials are valid. Already logged in to", server)
			return nil
		}
		fmt.Fprintln(opts.Stdout, "Existing credentials are invalid, please enter valid username and password")
	}

	username, password, err := getUserAndPass(opts, password, authConfig.Username)
	if err != nil {
		return errors.Wrapf(err, "error getting username and password")
	}

	if err = docker.CheckAuth(ctx, systemContext, username, password, server); err == nil {
		// Write the new credentials to the authfile
		if err := config.SetAuthentication(systemContext, server, username, password); err != nil {
			return err
		}
	}
	if err == nil {
		fmt.Fprintln(opts.Stdout, "Login Succeeded!")
		return nil
	}
	if unauthorized, ok := err.(docker.ErrUnauthorizedForCredentials); ok {
		logrus.Debugf("error logging into %q: %v", server, unauthorized)
		return errors.Errorf("error logging into %q: invalid username/password", server)
	}
	return errors.Wrapf(err, "error authenticating creds for %q", server)
}

// getRegistryName scrubs and parses the input to get the server name
func getRegistryName(server string) string {
	// removes 'http://' or 'https://' from the front of the
	// server/registry string if either is there.  This will be mostly used
	// for user input from 'Buildah login' and 'Buildah logout'.
	server = strings.TrimPrefix(strings.TrimPrefix(server, "https://"), "http://")
	// gets the registry from the input. If the input is of the form
	// quay.io/myuser/myimage, it will parse it and just return quay.io
	split := strings.Split(server, "/")
	return split[0]
}

// getUserAndPass gets the username and password from STDIN if not given
// using the -u and -p flags.  If the username prompt is left empty, the
// displayed userFromAuthFile will be used instead.
func getUserAndPass(opts *LoginOptions, password, userFromAuthFile string) (user, pass string, err error) {
	reader := bufio.NewReader(opts.Stdin)
	username := opts.Username
	if username == "" {
		if userFromAuthFile != "" {
			fmt.Fprintf(opts.Stdout, "Username (%s): ", userFromAuthFile)
		} else {
			fmt.Fprint(opts.Stdout, "Username: ")
		}
		username, err = reader.ReadString('\n')
		if err != nil {
			return "", "", errors.Wrapf(err, "error reading username")
		}
		// If the user just hit enter, use the displayed user from the
		// the authentication file.  This allows to do a lazy
		// `$ buildah login -p $NEW_PASSWORD` without specifying the
		// user.
		if strings.TrimSpace(username) == "" {
			username = userFromAuthFile
		}
	}
	if password == "" {
		fmt.Fprint(opts.Stdout, "Password: ")
		pass, err := terminal.ReadPassword(0)
		if err != nil {
			return "", "", errors.Wrapf(err, "error reading password")
		}
		password = string(pass)
		fmt.Fprintln(opts.Stdout)
	}
	return strings.TrimSpace(username), password, err
}

// Logout implements a “log out” command with the provided opts and args
func Logout(systemContext *types.SystemContext, opts *LogoutOptions, args []string) error {
	if err := CheckAuthFile(opts.AuthFile); err != nil {
		return err
	}
	systemContext = systemContextWithOptions(systemContext, opts.AuthFile, "")

	var (
		server string
		err    error
	)
	if len(args) > 1 {
		return errors.Errorf("logout accepts only one registry to logout from")
	}
	if len(args) == 0 && !opts.All {
		if !opts.AcceptUnspecifiedRegistry {
			return errors.Errorf("please provide a registry to logout from")
		}
		if server, err = defaultRegistryWhenUnspecified(systemContext); err != nil {
			return err
		}
		logrus.Debugf("registry not specified, default to the first registry %q from registries.conf", server)
	}
	if len(args) != 0 {
		if opts.All {
			return errors.Errorf("--all takes no arguments")
		}
		server = getRegistryName(args[0])
	}

	if opts.All {
		if err := config.RemoveAllAuthentication(systemContext); err != nil {
			return err
		}
		fmt.Fprintln(opts.Stdout, "Removed login credentials for all registries")
		return nil
	}

	err = config.RemoveAuthentication(systemContext, server)
	switch errors.Cause(err) {
	case nil:
		fmt.Fprintf(opts.Stdout, "Removed login credentials for %s\n", server)
		return nil
	case config.ErrNotLoggedIn:
		authConfig, err := config.GetCredentials(systemContext, server)
		if err != nil {
			return errors.Wrapf(err, "error reading auth file")
		}
		authInvalid := docker.CheckAuth(context.Background(), systemContext, authConfig.Username, authConfig.Password, server)
		if authConfig.Username != "" && authConfig.Password != "" && authInvalid == nil {
			fmt.Printf("Not logged into %s with current tool. Existing credentials were established via docker login. Please use docker logout instead.\n", server)
			return nil
		}
		return errors.Errorf("Not logged into %s\n", server)
	default:
		return errors.Wrapf(err, "error logging out of %q", server)
	}
}

// defaultRegistryWhenUnspecified returns first registry from search list of registry.conf
// used by login/logout when registry argument is not specified
func defaultRegistryWhenUnspecified(systemContext *types.SystemContext) (string, error) {
	registriesFromFile, err := sysregistriesv2.UnqualifiedSearchRegistries(systemContext)
	if err != nil {
		return "", errors.Wrapf(err, "error getting registry from registry.conf, please specify a registry")
	}
	if len(registriesFromFile) == 0 {
		return "", errors.Errorf("no registries found in registries.conf, a registry must be provided")
	}
	return registriesFromFile[0], nil
}