11
imageAuth "github.com/containers/image/v5/pkg/docker/config"
12
"github.com/containers/image/v5/types"
13
dockerAPITypes "github.com/docker/docker/api/types/registry"
14
"github.com/sirupsen/logrus"
17
// xRegistryAuthHeader is the key to the encoded registry authentication configuration in an http-request header.
18
// This header supports one registry per header occurrence. To support N registries provide N headers, one per registry.
19
// As of Docker API 1.40 and Libpod API 1.0.0, this header is supported by all endpoints.
20
const xRegistryAuthHeader = "X-Registry-Auth"
22
// xRegistryConfigHeader is the key to the encoded registry authentication configuration in an http-request header.
23
// This header supports N registries in one header via a Base64 encoded, JSON map.
24
// As of Docker API 1.40 and Libpod API 2.0.0, this header is supported by build endpoints.
25
const xRegistryConfigHeader = "X-Registry-Config"
27
// GetCredentials queries the http.Request for X-Registry-.* headers and extracts
28
// the necessary authentication information for libpod operations, possibly
29
// creating a config file. If that is the case, the caller must call RemoveAuthFile.
30
func GetCredentials(r *http.Request) (*types.DockerAuthConfig, string, error) {
31
nonemptyHeaderValue := func(key string) ([]string, bool) {
32
hdr := r.Header.Values(key)
33
return hdr, len(hdr) > 0
35
var override *types.DockerAuthConfig
36
var fileContents map[string]types.DockerAuthConfig
39
if hdr, ok := nonemptyHeaderValue(xRegistryConfigHeader); ok {
40
headerName = xRegistryConfigHeader
41
override, fileContents, err = getConfigCredentials(r, hdr)
42
} else if hdr, ok := nonemptyHeaderValue(xRegistryAuthHeader); ok {
43
headerName = xRegistryAuthHeader
44
override, fileContents, err = getAuthCredentials(hdr)
49
return nil, "", fmt.Errorf("failed to parse %q header for %s: %w", headerName, r.URL.String(), err)
53
if fileContents == nil {
56
authFile, err = authConfigsToAuthFile(fileContents)
58
return nil, "", fmt.Errorf("failed to parse %q header for %s: %w", headerName, r.URL.String(), err)
61
return override, authFile, nil
64
// getConfigCredentials extracts one or more docker.AuthConfig from a request and its
65
// xRegistryConfigHeader value. An empty key will be used as default while a named registry will be
66
// returned as types.DockerAuthConfig
67
func getConfigCredentials(r *http.Request, headers []string) (*types.DockerAuthConfig, map[string]types.DockerAuthConfig, error) {
68
var auth *types.DockerAuthConfig
69
configs := make(map[string]types.DockerAuthConfig)
71
for _, h := range headers {
72
param, err := base64.URLEncoding.DecodeString(h)
74
return nil, nil, fmt.Errorf("failed to decode %q: %w", xRegistryConfigHeader, err)
77
ac := make(map[string]dockerAPITypes.AuthConfig)
78
err = json.Unmarshal(param, &ac)
80
return nil, nil, fmt.Errorf("failed to unmarshal %q: %w", xRegistryConfigHeader, err)
83
for k, v := range ac {
84
configs[k] = dockerAuthToImageAuth(v)
88
// Empty key implies no registry given in API
89
if c, found := configs[""]; found {
93
// Override any default given above if specialized credentials provided
94
if registries, found := r.URL.Query()["registry"]; found {
95
for _, r := range registries {
96
for k, v := range configs {
97
if strings.Contains(k, r) {
109
logrus.Debugf("%q header found in request, but \"registry=%v\" query parameter not provided",
110
xRegistryConfigHeader, registries)
112
logrus.Debugf("%q header found in request for username %q", xRegistryConfigHeader, auth.Username)
116
return auth, configs, nil
119
// getAuthCredentials extracts one or more DockerAuthConfigs from an xRegistryAuthHeader
120
// value. The header could specify a single-auth config in which case the
121
// first return value is set. In case of a multi-auth header, the contents are
122
// returned in the second return value.
123
func getAuthCredentials(headers []string) (*types.DockerAuthConfig, map[string]types.DockerAuthConfig, error) {
124
authHeader := headers[0]
126
// First look for a multi-auth header (i.e., a map).
127
authConfigs, err := parseMultiAuthHeader(authHeader)
129
return nil, authConfigs, nil
132
// Fallback to looking for a single-auth header (i.e., one config).
133
authConfig, err := parseSingleAuthHeader(authHeader)
137
return &authConfig, nil, nil
140
// MakeXRegistryConfigHeader returns a map with the "X-Registry-Config" header set, which can
141
// conveniently be used in the http stack.
142
func MakeXRegistryConfigHeader(sys *types.SystemContext, username, password string) (http.Header, error) {
144
sys = &types.SystemContext{}
146
authConfigs, err := imageAuth.GetAllCredentials(sys)
152
authConfigs[""] = types.DockerAuthConfig{
158
if len(authConfigs) == 0 {
161
content, err := encodeMultiAuthConfigs(authConfigs)
165
return http.Header{xRegistryConfigHeader: []string{content}}, nil
168
// MakeXRegistryAuthHeader returns a map with the "X-Registry-Auth" header set, which can
169
// conveniently be used in the http stack.
170
func MakeXRegistryAuthHeader(sys *types.SystemContext, username, password string) (http.Header, error) {
172
content, err := encodeSingleAuthConfig(types.DockerAuthConfig{Username: username, Password: password})
176
return http.Header{xRegistryAuthHeader: []string{content}}, nil
180
sys = &types.SystemContext{}
182
authConfigs, err := imageAuth.GetAllCredentials(sys)
186
content, err := encodeMultiAuthConfigs(authConfigs)
190
return http.Header{xRegistryAuthHeader: []string{content}}, nil
193
// RemoveAuthfile is a convenience function that is meant to be called in a
194
// deferred statement. If non-empty, it removes the specified authfile and log
195
// errors. It's meant to reduce boilerplate code at call sites of
197
func RemoveAuthfile(authfile string) {
201
if err := os.Remove(authfile); err != nil {
202
logrus.Errorf("Removing temporary auth file %q: %v", authfile, err)
206
// encodeSingleAuthConfig serializes the auth configuration as a base64 encoded JSON payload.
207
func encodeSingleAuthConfig(authConfig types.DockerAuthConfig) (string, error) {
208
conf := imageAuthToDockerAuth(authConfig)
209
buf, err := json.Marshal(conf)
213
return base64.URLEncoding.EncodeToString(buf), nil
216
// encodeMultiAuthConfigs serializes the auth configurations as a base64 encoded JSON payload.
217
func encodeMultiAuthConfigs(authConfigs map[string]types.DockerAuthConfig) (string, error) {
218
confs := make(map[string]dockerAPITypes.AuthConfig)
219
for registry, authConf := range authConfigs {
220
confs[registry] = imageAuthToDockerAuth(authConf)
222
buf, err := json.Marshal(confs)
226
return base64.URLEncoding.EncodeToString(buf), nil
229
// authConfigsToAuthFile stores the specified auth configs in a temporary files
230
// and returns its path. The file can later be used as an auth file for contacting
231
// one or more container registries. If tmpDir is empty, the system's default
232
// TMPDIR will be used.
233
func authConfigsToAuthFile(authConfigs map[string]types.DockerAuthConfig) (string, error) {
234
// Initialize an empty temporary JSON file.
235
tmpFile, err := os.CreateTemp("", "auth.json.")
239
if _, err := tmpFile.Write([]byte{'{', '}'}); err != nil {
240
return "", fmt.Errorf("initializing temporary auth file: %w", err)
242
if err := tmpFile.Close(); err != nil {
243
return "", fmt.Errorf("closing temporary auth file: %w", err)
245
authFilePath := tmpFile.Name()
247
// Now use the c/image packages to store the credentials. It's battle
248
// tested, and we make sure to use the same code as the image backend.
249
sys := types.SystemContext{AuthFilePath: authFilePath}
250
for authFileKey, config := range authConfigs {
251
key := normalizeAuthFileKey(authFileKey)
253
// Note that we do not validate the credentials here. We assume
254
// that all credentials are valid. They'll be used on demand
256
if err := imageAuth.SetAuthentication(&sys, key, config.Username, config.Password); err != nil {
257
return "", fmt.Errorf("storing credentials in temporary auth file (key: %q / %q, user: %q): %w", authFileKey, key, config.Username, err)
261
return authFilePath, nil
264
// normalizeAuthFileKey takes an auth file key and converts it into a new-style credential key
265
// in the canonical format, as interpreted by c/image/pkg/docker/config.
266
func normalizeAuthFileKey(authFileKey string) string {
267
stripped := strings.TrimPrefix(authFileKey, "http://")
268
stripped = strings.TrimPrefix(stripped, "https://")
270
if stripped != authFileKey { // URLs are interpreted to mean complete registries
271
stripped, _, _ = strings.Cut(stripped, "/")
274
// Only non-namespaced registry names (or URLs) need to be normalized; repo namespaces
275
// always use the simple format.
277
case "registry-1.docker.io", "index.docker.io":
284
// dockerAuthToImageAuth converts a docker auth config to one we're using
285
// internally from c/image. Note that the Docker types look slightly
286
// different, so we need to convert to be extra sure we're not running into
287
// undesired side-effects when unmarshalling directly to our types.
288
func dockerAuthToImageAuth(authConfig dockerAPITypes.AuthConfig) types.DockerAuthConfig {
289
return types.DockerAuthConfig{
290
Username: authConfig.Username,
291
Password: authConfig.Password,
292
IdentityToken: authConfig.IdentityToken,
296
// reverse conversion of `dockerAuthToImageAuth`.
297
func imageAuthToDockerAuth(authConfig types.DockerAuthConfig) dockerAPITypes.AuthConfig {
298
return dockerAPITypes.AuthConfig{
299
Username: authConfig.Username,
300
Password: authConfig.Password,
301
IdentityToken: authConfig.IdentityToken,
305
// parseSingleAuthHeader extracts a DockerAuthConfig from an xRegistryAuthHeader value.
306
// The header content is a single DockerAuthConfig.
307
func parseSingleAuthHeader(authHeader string) (types.DockerAuthConfig, error) {
308
// Accept "null" and handle it as empty value for compatibility reason with Docker.
309
// Some java docker clients pass this value, e.g. this one used in Eclipse.
310
if len(authHeader) == 0 || authHeader == "null" {
311
return types.DockerAuthConfig{}, nil
314
authConfig := dockerAPITypes.AuthConfig{}
315
authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authHeader))
316
if err := json.NewDecoder(authJSON).Decode(&authConfig); err != nil {
317
return types.DockerAuthConfig{}, err
319
return dockerAuthToImageAuth(authConfig), nil
322
// parseMultiAuthHeader extracts a DockerAuthConfig from an xRegistryAuthHeader value.
323
// The header content is a map[string]DockerAuthConfigs.
324
func parseMultiAuthHeader(authHeader string) (map[string]types.DockerAuthConfig, error) {
325
// Accept "null" and handle it as empty value for compatibility reason with Docker.
326
// Some java docker clients pass this value, e.g. this one used in Eclipse.
327
if len(authHeader) == 0 || authHeader == "null" {
331
dockerAuthConfigs := make(map[string]dockerAPITypes.AuthConfig)
332
authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authHeader))
333
if err := json.NewDecoder(authJSON).Decode(&dockerAuthConfigs); err != nil {
337
// Now convert to the internal types.
338
authConfigs := make(map[string]types.DockerAuthConfig)
339
for server := range dockerAuthConfigs {
340
authConfigs[server] = dockerAuthToImageAuth(dockerAuthConfigs[server])
342
return authConfigs, nil