diff options
author | Valentin Rothberg <rothberg@redhat.com> | 2020-05-13 13:44:29 +0200 |
---|---|---|
committer | Valentin Rothberg <rothberg@redhat.com> | 2020-05-29 15:39:37 +0200 |
commit | dc80267b594e41cf7e223821dc1446683f0cae36 (patch) | |
tree | 8ca8f81cdf302b1905d7a56f7c5c76ba5468c6f1 /pkg/auth | |
parent | 78c38460eb8ba9190d414f2da6a1414990cc6cfd (diff) | |
download | podman-dc80267b594e41cf7e223821dc1446683f0cae36.tar.gz podman-dc80267b594e41cf7e223821dc1446683f0cae36.tar.bz2 podman-dc80267b594e41cf7e223821dc1446683f0cae36.zip |
compat handlers: add X-Registry-Auth header support
* Support the `X-Registry-Auth` http-request header.
* The content of the header is a base64 encoded JSON payload which can
either be a single auth config or a map of auth configs (user+pw or
token) with the corresponding registries being the keys. Vanilla
Docker, projectatomic Docker and the bindings are transparantly
supported.
* Add a hidden `--registries-conf` flag. Buildah exposes the same
flag, mostly for testing purposes.
* Do all credential parsing in the client (i.e., `cmd/podman`) pass
the username and password in the backend instead of unparsed
credentials.
* Add a `pkg/auth` which handles most of the heavy lifting.
* Go through the authentication-handling code of most commands, bindings
and endpoints. Migrate them to the new code and fix issues as seen.
A final evaluation and more tests is still required *after* this
change.
* The manifest-push endpoint is missing certain parameters and should
use the ABI function instead. Adding auth-support isn't really
possible without these parts working.
* The container commands and endpoints (i.e., create and run) have not
been changed yet. The APIs don't yet account for the authfile.
* Add authentication tests to `pkg/bindings`.
Fixes: #6384
Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
Diffstat (limited to 'pkg/auth')
-rw-r--r-- | pkg/auth/auth.go | 216 |
1 files changed, 216 insertions, 0 deletions
diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go new file mode 100644 index 000000000..ffa65f7e5 --- /dev/null +++ b/pkg/auth/auth.go @@ -0,0 +1,216 @@ +package auth + +import ( + "encoding/base64" + "encoding/json" + "io/ioutil" + "net/http" + "os" + "strings" + + imageAuth "github.com/containers/image/v5/pkg/docker/config" + "github.com/containers/image/v5/types" + dockerAPITypes "github.com/docker/docker/api/types" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// XRegistryAuthHeader is the key to the encoded registry authentication +// configuration in an http-request header. +const XRegistryAuthHeader = "X-Registry-Auth" + +// GetCredentials extracts one or more DockerAuthConfigs from the request's +// header. The header could specify a single-auth config in which case the +// first return value is set. In case of a multi-auth header, the contents are +// stored in a temporary auth file (2nd return value). Note that the auth file +// should be removed after usage. +func GetCredentials(r *http.Request) (*types.DockerAuthConfig, string, error) { + authHeader := r.Header.Get(XRegistryAuthHeader) + if len(authHeader) == 0 { + return nil, "", nil + } + + // First look for a multi-auth header (i.e., a map). + authConfigs, err := multiAuthHeader(r) + if err == nil { + authfile, err := authConfigsToAuthFile(authConfigs) + return nil, authfile, err + } + + // Fallback to looking for a single-auth header (i.e., one config). + authConfigs, err = singleAuthHeader(r) + if err != nil { + return nil, "", err + } + var conf *types.DockerAuthConfig + for k := range authConfigs { + c := authConfigs[k] + conf = &c + break + } + return conf, "", nil +} + +// Header returns a map with the XRegistryAuthHeader set which can +// conveniently be used in the http stack. +func Header(sys *types.SystemContext, authfile, username, password string) (map[string]string, error) { + var content string + var err error + + if username != "" { + content, err = encodeSingleAuthConfig(types.DockerAuthConfig{Username: username, Password: password}) + if err != nil { + return nil, err + } + } else { + if sys == nil { + sys = &types.SystemContext{} + } + if authfile != "" { + sys.AuthFilePath = authfile + } + authConfigs, err := imageAuth.GetAllCredentials(sys) + if err != nil { + return nil, err + } + content, err = encodeMultiAuthConfigs(authConfigs) + if err != nil { + return nil, err + } + } + + header := make(map[string]string) + header[XRegistryAuthHeader] = content + + return header, nil +} + +// RemoveAuthfile is a convenience function that is meant to be called in a +// deferred statement. If non-empty, it removes the specified authfile and log +// errors. It's meant to reduce boilerplate code at call sites of +// `GetCredentials`. +func RemoveAuthfile(authfile string) { + if authfile == "" { + return + } + if err := os.Remove(authfile); err != nil { + logrus.Errorf("Error removing temporary auth file %q: %v", authfile, err) + } +} + +// encodeSingleAuthConfig serializes the auth configuration as a base64 encoded JSON payload. +func encodeSingleAuthConfig(authConfig types.DockerAuthConfig) (string, error) { + conf := imageAuthToDockerAuth(authConfig) + buf, err := json.Marshal(conf) + if err != nil { + return "", err + } + return base64.URLEncoding.EncodeToString(buf), nil +} + +// encodeMultiAuthConfigs serializes the auth configurations as a base64 encoded JSON payload. +func encodeMultiAuthConfigs(authConfigs map[string]types.DockerAuthConfig) (string, error) { + confs := make(map[string]dockerAPITypes.AuthConfig) + for registry, authConf := range authConfigs { + confs[registry] = imageAuthToDockerAuth(authConf) + } + buf, err := json.Marshal(confs) + if err != nil { + return "", err + } + return base64.URLEncoding.EncodeToString(buf), nil +} + +// authConfigsToAuthFile stores the specified auth configs in a temporary files +// and returns its path. The file can later be used an auth file for contacting +// one or more container registries. If tmpDir is empty, the system's default +// TMPDIR will be used. +func authConfigsToAuthFile(authConfigs map[string]types.DockerAuthConfig) (string, error) { + // Intitialize an empty temporary JSON file. + tmpFile, err := ioutil.TempFile("", "auth.json.") + if err != nil { + return "", err + } + if _, err := tmpFile.Write([]byte{'{', '}'}); err != nil { + return "", errors.Wrap(err, "error initializing temporary auth file") + } + if err := tmpFile.Close(); err != nil { + return "", errors.Wrap(err, "error closing temporary auth file") + } + authFilePath := tmpFile.Name() + + // TODO: It would be nice if c/image could dump the map at once. + // + // Now use the c/image packages to store the credentials. It's battle + // tested, and we make sure to use the same code as the image backend. + sys := types.SystemContext{AuthFilePath: authFilePath} + for server, config := range authConfigs { + // Note that we do not validate the credentials here. Wassume + // that all credentials are valid. They'll be used on demand + // later. + if err := imageAuth.SetAuthentication(&sys, server, config.Username, config.Password); err != nil { + return "", errors.Wrapf(err, "error storing credentials in temporary auth file (server: %q, user: %q)", server, config.Username) + } + } + + return authFilePath, nil +} + +// dockerAuthToImageAuth converts a docker auth config to one we're using +// internally from c/image. Note that the Docker types look slightly +// different, so we need to convert to be extra sure we're not running into +// undesired side-effects when unmarhalling directly to our types. +func dockerAuthToImageAuth(authConfig dockerAPITypes.AuthConfig) types.DockerAuthConfig { + return types.DockerAuthConfig{ + Username: authConfig.Username, + Password: authConfig.Password, + IdentityToken: authConfig.IdentityToken, + } +} + +// reverse conversion of `dockerAuthToImageAuth`. +func imageAuthToDockerAuth(authConfig types.DockerAuthConfig) dockerAPITypes.AuthConfig { + return dockerAPITypes.AuthConfig{ + Username: authConfig.Username, + Password: authConfig.Password, + IdentityToken: authConfig.IdentityToken, + } +} + +// singleAuthHeader extracts a DockerAuthConfig from the request's header. +// The header content is a single DockerAuthConfig. +func singleAuthHeader(r *http.Request) (map[string]types.DockerAuthConfig, error) { + authHeader := r.Header.Get(XRegistryAuthHeader) + authConfig := dockerAPITypes.AuthConfig{} + if len(authHeader) > 0 { + authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authHeader)) + if err := json.NewDecoder(authJSON).Decode(&authConfig); err != nil { + return nil, err + } + } + authConfigs := make(map[string]types.DockerAuthConfig) + authConfigs["0"] = dockerAuthToImageAuth(authConfig) + return authConfigs, nil +} + +// multiAuthHeader extracts a DockerAuthConfig from the request's header. +// The header content is a map[string]DockerAuthConfigs. +func multiAuthHeader(r *http.Request) (map[string]types.DockerAuthConfig, error) { + authHeader := r.Header.Get(XRegistryAuthHeader) + if len(authHeader) == 0 { + return nil, nil + } + + dockerAuthConfigs := make(map[string]dockerAPITypes.AuthConfig) + authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authHeader)) + if err := json.NewDecoder(authJSON).Decode(&dockerAuthConfigs); err != nil { + return nil, err + } + + // Now convert to the internal types. + authConfigs := make(map[string]types.DockerAuthConfig) + for server := range dockerAuthConfigs { + authConfigs[server] = dockerAuthToImageAuth(dockerAuthConfigs[server]) + } + return authConfigs, nil +} |