-
Notifications
You must be signed in to change notification settings - Fork 40
/
auth.go
223 lines (190 loc) · 6.19 KB
/
auth.go
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
package docker // import "https://github.com/cpuguy83/dockercfg"
import (
"encoding/base64"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
// Errors from credential helpers
var (
errCredentialsNotFound = errors.New("credentials not found in native keychain")
errCredentialsMissingServerURL = errors.New("no credentials server URL")
)
// This is used by the docker CLI in casses where an oauth identity token is used.
// In that case the username is stored literally as `<token>`
// When fetching the credentials we check for this value to determine if
const tokenUsername = "<token>"
// GetRegistryCredentials gets registry credentials for the passed in registry host.
//
// This will use `LoadDefaultConfig` to read registry auth details from the config.
// If the config doesn't exist, it will attempt to load registry credentials using the default credential helper for the platform.
func getRegistryCredentials(configFile string, hostname string) (string, string, error) {
cfg, err := loadConfig(configFile)
if err != nil {
if !os.IsNotExist(err) {
return "", "", err
}
return getCredentialsFromHelper("", hostname)
}
return cfg.getRegistryCredentials(hostname)
}
// GetRegistryCredentials gets credentials, if any, for the provided hostname
//
// Hostnames should already be resolved using `ResolveRegistryAuth`
//
// If the returned username string is empty, the password is an identity token.
func (c *config) getRegistryCredentials(hostname string) (string, string, error) {
h, ok := c.CredentialHelpers[hostname]
if ok {
return getCredentialsFromHelper(h, hostname)
}
if c.CredentialsStore != "" {
return getCredentialsFromHelper(c.CredentialsStore, hostname)
}
auth, ok := c.AuthConfigs[hostname]
if !ok {
return getCredentialsFromHelper("", hostname)
}
if auth.IdentityToken != "" {
return "", auth.IdentityToken, nil
}
if auth.Username != "" && auth.Password != "" {
return auth.Username, auth.Password, nil
}
return decodeBase64Auth(auth)
}
// DecodeBase64Auth decodes the legacy file-based auth storage from the docker CLI.
// It takes the "Auth" filed from AuthConfig and decodes that into a username and password.
//
// If "Auth" is empty, an empty user/pass will be returned, but not an error.
func decodeBase64Auth(auth authConfig) (string, string, error) {
if auth.Auth == "" {
return "", "", nil
}
decLen := base64.StdEncoding.DecodedLen(len(auth.Auth))
decoded := make([]byte, decLen)
n, err := base64.StdEncoding.Decode(decoded, []byte(auth.Auth))
if err != nil {
return "", "", fmt.Errorf("error decoding auth from file: %w", err)
}
if n > decLen {
return "", "", fmt.Errorf("decoded value is longer than expected length, expected: %d, actual: %d", decLen, n)
}
split := strings.SplitN(string(decoded), ":", 2)
if len(split) != 2 {
return "", "", errors.New("invalid auth string")
}
return split[0], strings.Trim(split[1], "\x00"), nil
}
// GetCredentialsFromHelper attempts to lookup credentials from the passed in docker credential helper.
//
// The credential helpoer should just be the suffix name (no "docker-credential-").
// If the passed in helper program is empty this will look up the default helper for the platform.
//
// If the credentials are not found, no error is returned, only empty credentials.
//
// Hostnames should already be resolved using `ResolveRegistryAuth`
//
// If the username string is empty, the password string is an identity token.
func getCredentialsFromHelper(helper, hostname string) (string, string, error) {
if helper == "" {
helper = getCredentialHelper()
}
if helper == "" {
return "", "", nil
}
p, err := exec.LookPath("docker-credential-" + helper)
if err != nil {
return "", "", nil
}
cmd := exec.Command(p, "get")
cmd.Stdin = strings.NewReader(hostname)
b, err := cmd.Output()
if err != nil {
s := strings.TrimSpace(string(b))
switch s {
case errCredentialsNotFound.Error():
return "", "", nil
case errCredentialsMissingServerURL.Error():
return "", "", errors.New(s)
default:
}
return "", "", err
}
var creds struct {
Username string
Secret string
}
if err := json.Unmarshal(b, &creds); err != nil {
return "", "", err
}
// When tokenUsername is used, the output is an identity token and the username is garbage
if creds.Username == tokenUsername {
creds.Username = ""
}
return creds.Username, creds.Secret, nil
}
// getCredentialHelper gets the default credential helper name for the current platform.
func getCredentialHelper() string {
switch runtime.GOOS {
case "linux":
if _, err := exec.LookPath("pass"); err == nil {
return "pass"
}
return "secretservice"
case "darwin":
return "osxkeychain"
case "windows":
return "wincred"
default:
return ""
}
}
// UserHomeConfigPath returns the path to the docker config in the current user's home dir.
func userHomeConfigPath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("error looking up user home dir: %w", err)
}
return filepath.Join(home, ".docker", "config.json"), nil
}
// ConfigPath returns the path to the docker cli config.
//
// It will either use the DOCKER_CONFIG env var if set, or the value from `UserHomeConfigPath`
// DOCKER_CONFIG would be the dir path where `config.json` is stored, this returns the path to config.json.
func configPath(configFile string) (string, error) {
if configFile != "" {
return configFile, nil
}
return userHomeConfigPath()
}
// LoadDefaultConfig loads the docker cli config from the path returned from `ConfigPath`
func loadConfig(configFile string) (config, error) {
var cfg config
p, err := configPath(configFile)
if err != nil {
return cfg, err
}
return cfg, fromFile(p, &cfg)
}
// FromFile loads config from the specified path into cfg
func fromFile(configPath string, cfg *config) error {
log.Debug().Msgf("Loading docker config: %s", configPath)
f, err := os.Open(configPath)
if err != nil {
return err
}
if err := json.NewDecoder(f).Decode(&cfg); err != nil {
return errors.Wrapf(err, "error decoding docker config")
}
if err := f.Close(); err != nil {
return errors.Wrapf(err, "error closing docker config file")
}
return nil
}