github.com/containers/podman/v2@v2.2.2-0.20210501105131-c1e07d070c4c/pkg/auth/auth.go (about)

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