github.com/hanks177/podman/v4@v4.1.3-0.20220613032544-16d90015bc83/pkg/auth/auth.go (about)

     1  package auth
     2  
     3  import (
     4  	"encoding/base64"
     5  	"encoding/json"
     6  	"io/ioutil"
     7  	"net/http"
     8  	"os"
     9  	"strings"
    10  
    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"
    14  	"github.com/pkg/errors"
    15  	"github.com/sirupsen/logrus"
    16  )
    17  
    18  // xRegistryAuthHeader is the key to the encoded registry authentication configuration in an http-request header.
    19  // This header supports one registry per header occurrence. To support N registries provide N headers, one per registry.
    20  // As of Docker API 1.40 and Libpod API 1.0.0, this header is supported by all endpoints.
    21  const xRegistryAuthHeader = "X-Registry-Auth"
    22  
    23  // xRegistryConfigHeader is the key to the encoded registry authentication configuration in an http-request header.
    24  // This header supports N registries in one header via a Base64 encoded, JSON map.
    25  // As of Docker API 1.40 and Libpod API 2.0.0, this header is supported by build endpoints.
    26  const xRegistryConfigHeader = "X-Registry-Config"
    27  
    28  // GetCredentials queries the http.Request for X-Registry-.* headers and extracts
    29  // the necessary authentication information for libpod operations, possibly
    30  // creating a config file. If that is the case, the caller must call RemoveAuthFile.
    31  func GetCredentials(r *http.Request) (*types.DockerAuthConfig, string, error) {
    32  	nonemptyHeaderValue := func(key string) ([]string, bool) {
    33  		hdr := r.Header.Values(key)
    34  		return hdr, len(hdr) > 0
    35  	}
    36  	var override *types.DockerAuthConfig
    37  	var fileContents map[string]types.DockerAuthConfig
    38  	var headerName string
    39  	var err error
    40  	if hdr, ok := nonemptyHeaderValue(xRegistryConfigHeader); ok {
    41  		headerName = xRegistryConfigHeader
    42  		override, fileContents, err = getConfigCredentials(r, hdr)
    43  	} else if hdr, ok := nonemptyHeaderValue(xRegistryAuthHeader); ok {
    44  		headerName = xRegistryAuthHeader
    45  		override, fileContents, err = getAuthCredentials(hdr)
    46  	} else {
    47  		return nil, "", nil
    48  	}
    49  	if err != nil {
    50  		return nil, "", errors.Wrapf(err, "failed to parse %q header for %s", headerName, r.URL.String())
    51  	}
    52  
    53  	var authFile string
    54  	if fileContents == nil {
    55  		authFile = ""
    56  	} else {
    57  		authFile, err = authConfigsToAuthFile(fileContents)
    58  		if err != nil {
    59  			return nil, "", errors.Wrapf(err, "failed to parse %q header for %s", headerName, r.URL.String())
    60  		}
    61  	}
    62  	return override, authFile, nil
    63  }
    64  
    65  // getConfigCredentials extracts one or more docker.AuthConfig from a request and its
    66  // xRegistryConfigHeader value.  An empty key will be used as default while a named registry will be
    67  // returned as types.DockerAuthConfig
    68  func getConfigCredentials(r *http.Request, headers []string) (*types.DockerAuthConfig, map[string]types.DockerAuthConfig, error) {
    69  	var auth *types.DockerAuthConfig
    70  	configs := make(map[string]types.DockerAuthConfig)
    71  
    72  	for _, h := range headers {
    73  		param, err := base64.URLEncoding.DecodeString(h)
    74  		if err != nil {
    75  			return nil, nil, errors.Wrapf(err, "failed to decode %q", xRegistryConfigHeader)
    76  		}
    77  
    78  		ac := make(map[string]dockerAPITypes.AuthConfig)
    79  		err = json.Unmarshal(param, &ac)
    80  		if err != nil {
    81  			return nil, nil, errors.Wrapf(err, "failed to unmarshal %q", xRegistryConfigHeader)
    82  		}
    83  
    84  		for k, v := range ac {
    85  			configs[k] = dockerAuthToImageAuth(v)
    86  		}
    87  	}
    88  
    89  	// Empty key implies no registry given in API
    90  	if c, found := configs[""]; found {
    91  		auth = &c
    92  	}
    93  
    94  	// Override any default given above if specialized credentials provided
    95  	if registries, found := r.URL.Query()["registry"]; found {
    96  		for _, r := range registries {
    97  			for k, v := range configs {
    98  				if strings.Contains(k, r) {
    99  					v := v
   100  					auth = &v
   101  					break
   102  				}
   103  			}
   104  			if auth != nil {
   105  				break
   106  			}
   107  		}
   108  
   109  		if auth == nil {
   110  			logrus.Debugf("%q header found in request, but \"registry=%v\" query parameter not provided",
   111  				xRegistryConfigHeader, registries)
   112  		} else {
   113  			logrus.Debugf("%q header found in request for username %q", xRegistryConfigHeader, auth.Username)
   114  		}
   115  	}
   116  
   117  	return auth, configs, nil
   118  }
   119  
   120  // getAuthCredentials extracts one or more DockerAuthConfigs from an xRegistryAuthHeader
   121  // value.  The header could specify a single-auth config in which case the
   122  // first return value is set.  In case of a multi-auth header, the contents are
   123  // returned in the second return value.
   124  func getAuthCredentials(headers []string) (*types.DockerAuthConfig, map[string]types.DockerAuthConfig, error) {
   125  	authHeader := headers[0]
   126  
   127  	// First look for a multi-auth header (i.e., a map).
   128  	authConfigs, err := parseMultiAuthHeader(authHeader)
   129  	if err == nil {
   130  		return nil, authConfigs, nil
   131  	}
   132  
   133  	// Fallback to looking for a single-auth header (i.e., one config).
   134  	authConfig, err := parseSingleAuthHeader(authHeader)
   135  	if err != nil {
   136  		return nil, nil, err
   137  	}
   138  	return &authConfig, nil, nil
   139  }
   140  
   141  // MakeXRegistryConfigHeader returns a map with the "X-Registry-Config" header set, which can
   142  // conveniently be used in the http stack.
   143  func MakeXRegistryConfigHeader(sys *types.SystemContext, username, password string) (http.Header, error) {
   144  	if sys == nil {
   145  		sys = &types.SystemContext{}
   146  	}
   147  	authConfigs, err := imageAuth.GetAllCredentials(sys)
   148  	if err != nil {
   149  		return nil, err
   150  	}
   151  
   152  	if username != "" {
   153  		authConfigs[""] = types.DockerAuthConfig{
   154  			Username: username,
   155  			Password: password,
   156  		}
   157  	}
   158  
   159  	if len(authConfigs) == 0 {
   160  		return nil, nil
   161  	}
   162  	content, err := encodeMultiAuthConfigs(authConfigs)
   163  	if err != nil {
   164  		return nil, err
   165  	}
   166  	return http.Header{xRegistryConfigHeader: []string{content}}, nil
   167  }
   168  
   169  // MakeXRegistryAuthHeader returns a map with the "X-Registry-Auth" header set, which can
   170  // conveniently be used in the http stack.
   171  func MakeXRegistryAuthHeader(sys *types.SystemContext, username, password string) (http.Header, error) {
   172  	if username != "" {
   173  		content, err := encodeSingleAuthConfig(types.DockerAuthConfig{Username: username, Password: password})
   174  		if err != nil {
   175  			return nil, err
   176  		}
   177  		return http.Header{xRegistryAuthHeader: []string{content}}, nil
   178  	}
   179  
   180  	if sys == nil {
   181  		sys = &types.SystemContext{}
   182  	}
   183  	authConfigs, err := imageAuth.GetAllCredentials(sys)
   184  	if err != nil {
   185  		return nil, err
   186  	}
   187  	content, err := encodeMultiAuthConfigs(authConfigs)
   188  	if err != nil {
   189  		return nil, err
   190  	}
   191  	return http.Header{xRegistryAuthHeader: []string{content}}, nil
   192  }
   193  
   194  // RemoveAuthfile is a convenience function that is meant to be called in a
   195  // deferred statement. If non-empty, it removes the specified authfile and log
   196  // errors.  It's meant to reduce boilerplate code at call sites of
   197  // `GetCredentials`.
   198  func RemoveAuthfile(authfile string) {
   199  	if authfile == "" {
   200  		return
   201  	}
   202  	if err := os.Remove(authfile); err != nil {
   203  		logrus.Errorf("Removing temporary auth file %q: %v", authfile, err)
   204  	}
   205  }
   206  
   207  // encodeSingleAuthConfig serializes the auth configuration as a base64 encoded JSON payload.
   208  func encodeSingleAuthConfig(authConfig types.DockerAuthConfig) (string, error) {
   209  	conf := imageAuthToDockerAuth(authConfig)
   210  	buf, err := json.Marshal(conf)
   211  	if err != nil {
   212  		return "", err
   213  	}
   214  	return base64.URLEncoding.EncodeToString(buf), nil
   215  }
   216  
   217  // encodeMultiAuthConfigs serializes the auth configurations as a base64 encoded JSON payload.
   218  func encodeMultiAuthConfigs(authConfigs map[string]types.DockerAuthConfig) (string, error) {
   219  	confs := make(map[string]dockerAPITypes.AuthConfig)
   220  	for registry, authConf := range authConfigs {
   221  		confs[registry] = imageAuthToDockerAuth(authConf)
   222  	}
   223  	buf, err := json.Marshal(confs)
   224  	if err != nil {
   225  		return "", err
   226  	}
   227  	return base64.URLEncoding.EncodeToString(buf), nil
   228  }
   229  
   230  // authConfigsToAuthFile stores the specified auth configs in a temporary files
   231  // and returns its path. The file can later be used an auth file for contacting
   232  // one or more container registries.  If tmpDir is empty, the system's default
   233  // TMPDIR will be used.
   234  func authConfigsToAuthFile(authConfigs map[string]types.DockerAuthConfig) (string, error) {
   235  	// Initialize an empty temporary JSON file.
   236  	tmpFile, err := ioutil.TempFile("", "auth.json.")
   237  	if err != nil {
   238  		return "", err
   239  	}
   240  	if _, err := tmpFile.Write([]byte{'{', '}'}); err != nil {
   241  		return "", errors.Wrap(err, "error initializing temporary auth file")
   242  	}
   243  	if err := tmpFile.Close(); err != nil {
   244  		return "", errors.Wrap(err, "error closing temporary auth file")
   245  	}
   246  	authFilePath := tmpFile.Name()
   247  
   248  	// Now use the c/image packages to store the credentials. It's battle
   249  	// tested, and we make sure to use the same code as the image backend.
   250  	sys := types.SystemContext{AuthFilePath: authFilePath}
   251  	for authFileKey, config := range authConfigs {
   252  		key := normalizeAuthFileKey(authFileKey)
   253  
   254  		// Note that we do not validate the credentials here. We assume
   255  		// that all credentials are valid. They'll be used on demand
   256  		// later.
   257  		if err := imageAuth.SetAuthentication(&sys, key, config.Username, config.Password); err != nil {
   258  			return "", errors.Wrapf(err, "error storing credentials in temporary auth file (key: %q / %q, user: %q)", authFileKey, key, config.Username)
   259  		}
   260  	}
   261  
   262  	return authFilePath, nil
   263  }
   264  
   265  // normalizeAuthFileKey takes an auth file key and converts it into a new-style credential key
   266  // in the canonical format, as interpreted by c/image/pkg/docker/config.
   267  func normalizeAuthFileKey(authFileKey string) string {
   268  	stripped := strings.TrimPrefix(authFileKey, "http://")
   269  	stripped = strings.TrimPrefix(stripped, "https://")
   270  
   271  	if stripped != authFileKey { // URLs are interpreted to mean complete registries
   272  		stripped = strings.SplitN(stripped, "/", 2)[0]
   273  	}
   274  
   275  	// Only non-namespaced registry names (or URLs) need to be normalized; repo namespaces
   276  	// always use the simple format.
   277  	switch stripped {
   278  	case "registry-1.docker.io", "index.docker.io":
   279  		return "docker.io"
   280  	default:
   281  		return stripped
   282  	}
   283  }
   284  
   285  // dockerAuthToImageAuth converts a docker auth config to one we're using
   286  // internally from c/image.  Note that the Docker types look slightly
   287  // different, so we need to convert to be extra sure we're not running into
   288  // undesired side-effects when unmarhalling directly to our types.
   289  func dockerAuthToImageAuth(authConfig dockerAPITypes.AuthConfig) types.DockerAuthConfig {
   290  	return types.DockerAuthConfig{
   291  		Username:      authConfig.Username,
   292  		Password:      authConfig.Password,
   293  		IdentityToken: authConfig.IdentityToken,
   294  	}
   295  }
   296  
   297  // reverse conversion of `dockerAuthToImageAuth`.
   298  func imageAuthToDockerAuth(authConfig types.DockerAuthConfig) dockerAPITypes.AuthConfig {
   299  	return dockerAPITypes.AuthConfig{
   300  		Username:      authConfig.Username,
   301  		Password:      authConfig.Password,
   302  		IdentityToken: authConfig.IdentityToken,
   303  	}
   304  }
   305  
   306  // parseSingleAuthHeader extracts a DockerAuthConfig from an xRegistryAuthHeader value.
   307  // The header content is a single DockerAuthConfig.
   308  func parseSingleAuthHeader(authHeader string) (types.DockerAuthConfig, error) {
   309  	// Accept "null" and handle it as empty value for compatibility reason with Docker.
   310  	// Some java docker clients pass this value, e.g. this one used in Eclipse.
   311  	if len(authHeader) == 0 || authHeader == "null" {
   312  		return types.DockerAuthConfig{}, nil
   313  	}
   314  
   315  	authConfig := dockerAPITypes.AuthConfig{}
   316  	authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authHeader))
   317  	if err := json.NewDecoder(authJSON).Decode(&authConfig); err != nil {
   318  		return types.DockerAuthConfig{}, err
   319  	}
   320  	return dockerAuthToImageAuth(authConfig), nil
   321  }
   322  
   323  // parseMultiAuthHeader extracts a DockerAuthConfig from an xRegistryAuthHeader value.
   324  // The header content is a map[string]DockerAuthConfigs.
   325  func parseMultiAuthHeader(authHeader string) (map[string]types.DockerAuthConfig, error) {
   326  	// Accept "null" and handle it as empty value for compatibility reason with Docker.
   327  	// Some java docker clients pass this value, e.g. this one used in Eclipse.
   328  	if len(authHeader) == 0 || authHeader == "null" {
   329  		return nil, nil
   330  	}
   331  
   332  	dockerAuthConfigs := make(map[string]dockerAPITypes.AuthConfig)
   333  	authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authHeader))
   334  	if err := json.NewDecoder(authJSON).Decode(&dockerAuthConfigs); err != nil {
   335  		return nil, err
   336  	}
   337  
   338  	// Now convert to the internal types.
   339  	authConfigs := make(map[string]types.DockerAuthConfig)
   340  	for server := range dockerAuthConfigs {
   341  		authConfigs[server] = dockerAuthToImageAuth(dockerAuthConfigs[server])
   342  	}
   343  	return authConfigs, nil
   344  }