mirror of
https://github.com/containers/podman.git
synced 2025-07-03 17:27:18 +08:00
Merge pull request #11538 from mtrmac/http-credentials
Fix HTTP credentials passing
This commit is contained in:
@ -270,9 +270,9 @@ func CreateImageFromImage(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
authConf, authfile, key, err := auth.GetCredentials(r)
|
||||
authConf, authfile, err := auth.GetCredentials(r)
|
||||
if err != nil {
|
||||
utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, errors.Wrapf(err, "failed to parse %q header for %s", key, r.URL.String()))
|
||||
utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
defer auth.RemoveAuthfile(authfile)
|
||||
|
@ -453,10 +453,10 @@ func BuildImage(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
creds, authfile, key, err := auth.GetCredentials(r)
|
||||
creds, authfile, err := auth.GetCredentials(r)
|
||||
if err != nil {
|
||||
// Credential value(s) not returned as their value is not human readable
|
||||
utils.BadRequest(w, key.String(), "n/a", err)
|
||||
utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
defer auth.RemoveAuthfile(authfile)
|
||||
|
@ -85,9 +85,9 @@ func PushImage(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
authconf, authfile, key, err := auth.GetCredentials(r)
|
||||
authconf, authfile, err := auth.GetCredentials(r)
|
||||
if err != nil {
|
||||
utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "failed to parse %q header for %s", key, r.URL.String()))
|
||||
utils.Error(w, "Something went wrong.", http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
defer auth.RemoveAuthfile(authfile)
|
||||
|
@ -34,9 +34,9 @@ func SearchImages(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
_, authfile, key, err := auth.GetCredentials(r)
|
||||
_, authfile, err := auth.GetCredentials(r)
|
||||
if err != nil {
|
||||
utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, errors.Wrapf(err, "failed to parse %q header for %s", key, r.URL.String()))
|
||||
utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
defer auth.RemoveAuthfile(authfile)
|
||||
|
@ -497,9 +497,9 @@ func PushImage(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
authconf, authfile, key, err := auth.GetCredentials(r)
|
||||
authconf, authfile, err := auth.GetCredentials(r)
|
||||
if err != nil {
|
||||
utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, errors.Wrapf(err, "failed to parse %q header for %s", key, r.URL.String()))
|
||||
utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
defer auth.RemoveAuthfile(authfile)
|
||||
|
@ -68,9 +68,9 @@ func ImagesPull(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Do the auth dance.
|
||||
authConf, authfile, key, err := auth.GetCredentials(r)
|
||||
authConf, authfile, err := auth.GetCredentials(r)
|
||||
if err != nil {
|
||||
utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, errors.Wrapf(err, "failed to parse %q header for %s", key, r.URL.String()))
|
||||
utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
defer auth.RemoveAuthfile(authfile)
|
||||
|
@ -176,9 +176,9 @@ func ManifestPush(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
source := utils.GetName(r)
|
||||
authconf, authfile, key, err := auth.GetCredentials(r)
|
||||
authconf, authfile, err := auth.GetCredentials(r)
|
||||
if err != nil {
|
||||
utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, errors.Wrapf(err, "failed to parse %q header for %s", key, r.URL.String()))
|
||||
utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
defer auth.RemoveAuthfile(authfile)
|
||||
|
@ -86,9 +86,9 @@ func PlayKube(w http.ResponseWriter, r *http.Request) {
|
||||
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error closing temporary file"))
|
||||
return
|
||||
}
|
||||
authConf, authfile, key, err := auth.GetCredentials(r)
|
||||
authConf, authfile, err := auth.GetCredentials(r)
|
||||
if err != nil {
|
||||
utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, errors.Wrapf(err, "failed to parse %q header for %s", key, r.URL.String()))
|
||||
utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
defer auth.RemoveAuthfile(authfile)
|
||||
|
226
pkg/auth/auth.go
226
pkg/auth/auth.go
@ -3,7 +3,6 @@ package auth
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
@ -16,52 +15,70 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type HeaderAuthName string
|
||||
|
||||
func (h HeaderAuthName) String() string { return string(h) }
|
||||
|
||||
// XRegistryAuthHeader is the key to the encoded registry authentication configuration in an http-request header.
|
||||
// This header supports one registry per header occurrence. To support N registries provided N headers, one per registry.
|
||||
// xRegistryAuthHeader is the key to the encoded registry authentication configuration in an http-request header.
|
||||
// This header supports one registry per header occurrence. To support N registries provide N headers, one per registry.
|
||||
// As of Docker API 1.40 and Libpod API 1.0.0, this header is supported by all endpoints.
|
||||
const XRegistryAuthHeader HeaderAuthName = "X-Registry-Auth"
|
||||
const xRegistryAuthHeader = "X-Registry-Auth"
|
||||
|
||||
// XRegistryConfigHeader is the key to the encoded registry authentication configuration in an http-request header.
|
||||
// xRegistryConfigHeader is the key to the encoded registry authentication configuration in an http-request header.
|
||||
// This header supports N registries in one header via a Base64 encoded, JSON map.
|
||||
// As of Docker API 1.40 and Libpod API 2.0.0, this header is supported by build endpoints.
|
||||
const XRegistryConfigHeader HeaderAuthName = "X-Registry-Config"
|
||||
const xRegistryConfigHeader = "X-Registry-Config"
|
||||
|
||||
// GetCredentials queries the http.Request for X-Registry-.* headers and extracts
|
||||
// the necessary authentication information for libpod operations
|
||||
func GetCredentials(r *http.Request) (*types.DockerAuthConfig, string, HeaderAuthName, error) {
|
||||
has := func(key HeaderAuthName) bool { hdr, found := r.Header[string(key)]; return found && len(hdr) > 0 }
|
||||
switch {
|
||||
case has(XRegistryConfigHeader):
|
||||
c, f, err := getConfigCredentials(r)
|
||||
return c, f, XRegistryConfigHeader, err
|
||||
case has(XRegistryAuthHeader):
|
||||
c, f, err := getAuthCredentials(r)
|
||||
return c, f, XRegistryAuthHeader, err
|
||||
// the necessary authentication information for libpod operations, possibly
|
||||
// creating a config file. If that is the case, the caller must call RemoveAuthFile.
|
||||
func GetCredentials(r *http.Request) (*types.DockerAuthConfig, string, error) {
|
||||
nonemptyHeaderValue := func(key string) ([]string, bool) {
|
||||
hdr := r.Header.Values(key)
|
||||
return hdr, len(hdr) > 0
|
||||
}
|
||||
return nil, "", "", nil
|
||||
var override *types.DockerAuthConfig
|
||||
var fileContents map[string]types.DockerAuthConfig
|
||||
var headerName string
|
||||
var err error
|
||||
if hdr, ok := nonemptyHeaderValue(xRegistryConfigHeader); ok {
|
||||
headerName = xRegistryConfigHeader
|
||||
override, fileContents, err = getConfigCredentials(r, hdr)
|
||||
} else if hdr, ok := nonemptyHeaderValue(xRegistryAuthHeader); ok {
|
||||
headerName = xRegistryAuthHeader
|
||||
override, fileContents, err = getAuthCredentials(hdr)
|
||||
} else {
|
||||
return nil, "", nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, "", errors.Wrapf(err, "failed to parse %q header for %s", headerName, r.URL.String())
|
||||
}
|
||||
|
||||
var authFile string
|
||||
if fileContents == nil {
|
||||
authFile = ""
|
||||
} else {
|
||||
authFile, err = authConfigsToAuthFile(fileContents)
|
||||
if err != nil {
|
||||
return nil, "", errors.Wrapf(err, "failed to parse %q header for %s", headerName, r.URL.String())
|
||||
}
|
||||
}
|
||||
return override, authFile, nil
|
||||
}
|
||||
|
||||
// getConfigCredentials extracts one or more docker.AuthConfig from the request's
|
||||
// header. An empty key will be used as default while a named registry will be
|
||||
// getConfigCredentials extracts one or more docker.AuthConfig from a request and its
|
||||
// xRegistryConfigHeader value. An empty key will be used as default while a named registry will be
|
||||
// returned as types.DockerAuthConfig
|
||||
func getConfigCredentials(r *http.Request) (*types.DockerAuthConfig, string, error) {
|
||||
func getConfigCredentials(r *http.Request, headers []string) (*types.DockerAuthConfig, map[string]types.DockerAuthConfig, error) {
|
||||
var auth *types.DockerAuthConfig
|
||||
configs := make(map[string]types.DockerAuthConfig)
|
||||
|
||||
for _, h := range r.Header[string(XRegistryConfigHeader)] {
|
||||
for _, h := range headers {
|
||||
param, err := base64.URLEncoding.DecodeString(h)
|
||||
if err != nil {
|
||||
return nil, "", errors.Wrapf(err, "failed to decode %q", XRegistryConfigHeader)
|
||||
return nil, nil, errors.Wrapf(err, "failed to decode %q", xRegistryConfigHeader)
|
||||
}
|
||||
|
||||
ac := make(map[string]dockerAPITypes.AuthConfig)
|
||||
err = json.Unmarshal(param, &ac)
|
||||
if err != nil {
|
||||
return nil, "", errors.Wrapf(err, "failed to unmarshal %q", XRegistryConfigHeader)
|
||||
return nil, nil, errors.Wrapf(err, "failed to unmarshal %q", xRegistryConfigHeader)
|
||||
}
|
||||
|
||||
for k, v := range ac {
|
||||
@ -91,79 +108,45 @@ func getConfigCredentials(r *http.Request) (*types.DockerAuthConfig, string, err
|
||||
|
||||
if auth == nil {
|
||||
logrus.Debugf("%q header found in request, but \"registry=%v\" query parameter not provided",
|
||||
XRegistryConfigHeader, registries)
|
||||
xRegistryConfigHeader, registries)
|
||||
} else {
|
||||
logrus.Debugf("%q header found in request for username %q", XRegistryConfigHeader, auth.Username)
|
||||
logrus.Debugf("%q header found in request for username %q", xRegistryConfigHeader, auth.Username)
|
||||
}
|
||||
}
|
||||
|
||||
authfile, err := authConfigsToAuthFile(configs)
|
||||
return auth, authfile, err
|
||||
return auth, configs, nil
|
||||
}
|
||||
|
||||
// getAuthCredentials extracts one or more DockerAuthConfigs from the request's
|
||||
// header. The header could specify a single-auth config in which case the
|
||||
// getAuthCredentials extracts one or more DockerAuthConfigs from an xRegistryAuthHeader
|
||||
// value. 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 getAuthCredentials(r *http.Request) (*types.DockerAuthConfig, string, error) {
|
||||
// returned in the second return value.
|
||||
func getAuthCredentials(headers []string) (*types.DockerAuthConfig, map[string]types.DockerAuthConfig, error) {
|
||||
authHeader := headers[0]
|
||||
|
||||
// First look for a multi-auth header (i.e., a map).
|
||||
authConfigs, err := multiAuthHeader(r)
|
||||
authConfigs, err := parseMultiAuthHeader(authHeader)
|
||||
if err == nil {
|
||||
authfile, err := authConfigsToAuthFile(authConfigs)
|
||||
return nil, authfile, err
|
||||
return nil, authConfigs, nil
|
||||
}
|
||||
|
||||
// Fallback to looking for a single-auth header (i.e., one config).
|
||||
authConfigs, err = singleAuthHeader(r)
|
||||
authConfig, err := parseSingleAuthHeader(authHeader)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, nil, err
|
||||
}
|
||||
var conf *types.DockerAuthConfig
|
||||
for k := range authConfigs {
|
||||
c := authConfigs[k]
|
||||
conf = &c
|
||||
break
|
||||
}
|
||||
return conf, "", nil
|
||||
return &authConfig, nil, nil
|
||||
}
|
||||
|
||||
// Header builds the requested Authentication Header
|
||||
func Header(sys *types.SystemContext, headerName HeaderAuthName, authfile, username, password string) (map[string]string, error) {
|
||||
var (
|
||||
content string
|
||||
err error
|
||||
)
|
||||
switch headerName {
|
||||
case XRegistryAuthHeader:
|
||||
content, err = headerAuth(sys, authfile, username, password)
|
||||
case XRegistryConfigHeader:
|
||||
content, err = headerConfig(sys, authfile, username, password)
|
||||
default:
|
||||
err = fmt.Errorf("unsupported authentication header: %q", headerName)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(content) > 0 {
|
||||
return map[string]string{string(headerName): content}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// headerConfig returns a map with the XRegistryConfigHeader set which can
|
||||
// MakeXRegistryConfigHeader returns a map with the "X-Registry-Config" header set, which can
|
||||
// conveniently be used in the http stack.
|
||||
func headerConfig(sys *types.SystemContext, authfile, username, password string) (string, error) {
|
||||
func MakeXRegistryConfigHeader(sys *types.SystemContext, username, password string) (map[string]string, error) {
|
||||
if sys == nil {
|
||||
sys = &types.SystemContext{}
|
||||
}
|
||||
if authfile != "" {
|
||||
sys.AuthFilePath = authfile
|
||||
}
|
||||
authConfigs, err := imageAuth.GetAllCredentials(sys)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if username != "" {
|
||||
@ -174,29 +157,38 @@ func headerConfig(sys *types.SystemContext, authfile, username, password string)
|
||||
}
|
||||
|
||||
if len(authConfigs) == 0 {
|
||||
return "", nil
|
||||
return nil, nil
|
||||
}
|
||||
return encodeMultiAuthConfigs(authConfigs)
|
||||
content, err := encodeMultiAuthConfigs(authConfigs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]string{xRegistryConfigHeader: content}, nil
|
||||
}
|
||||
|
||||
// headerAuth returns a base64 encoded map with the XRegistryAuthHeader set which can
|
||||
// MakeXRegistryAuthHeader returns a map with the "X-Registry-Auth" header set, which can
|
||||
// conveniently be used in the http stack.
|
||||
func headerAuth(sys *types.SystemContext, authfile, username, password string) (string, error) {
|
||||
func MakeXRegistryAuthHeader(sys *types.SystemContext, username, password string) (map[string]string, error) {
|
||||
if username != "" {
|
||||
return encodeSingleAuthConfig(types.DockerAuthConfig{Username: username, Password: password})
|
||||
content, err := encodeSingleAuthConfig(types.DockerAuthConfig{Username: username, Password: password})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]string{xRegistryAuthHeader: content}, nil
|
||||
}
|
||||
|
||||
if sys == nil {
|
||||
sys = &types.SystemContext{}
|
||||
}
|
||||
if authfile != "" {
|
||||
sys.AuthFilePath = authfile
|
||||
}
|
||||
authConfigs, err := imageAuth.GetAllCredentials(sys)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
return encodeMultiAuthConfigs(authConfigs)
|
||||
content, err := encodeMultiAuthConfigs(authConfigs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]string{xRegistryAuthHeader: content}, nil
|
||||
}
|
||||
|
||||
// RemoveAuthfile is a convenience function that is meant to be called in a
|
||||
@ -258,34 +250,38 @@ func authConfigsToAuthFile(authConfigs map[string]types.DockerAuthConfig) (strin
|
||||
// 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 {
|
||||
server = normalize(server)
|
||||
for authFileKey, config := range authConfigs {
|
||||
key := normalizeAuthFileKey(authFileKey)
|
||||
|
||||
// Note that we do not validate the credentials here. We assume
|
||||
// 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)
|
||||
if err := imageAuth.SetAuthentication(&sys, key, config.Username, config.Password); err != nil {
|
||||
return "", errors.Wrapf(err, "error storing credentials in temporary auth file (key: %q / %q, user: %q)", authFileKey, key, config.Username)
|
||||
}
|
||||
}
|
||||
|
||||
return authFilePath, nil
|
||||
}
|
||||
|
||||
// normalize takes a server and removes the leading "http[s]://" prefix as well
|
||||
// as removes path suffixes from docker registries.
|
||||
func normalize(server string) string {
|
||||
stripped := strings.TrimPrefix(server, "http://")
|
||||
// normalizeAuthFileKey takes an auth file key and converts it into a new-style credential key
|
||||
// in the canonical format, as interpreted by c/image/pkg/docker/config.
|
||||
func normalizeAuthFileKey(authFileKey string) string {
|
||||
stripped := strings.TrimPrefix(authFileKey, "http://")
|
||||
stripped = strings.TrimPrefix(stripped, "https://")
|
||||
|
||||
/// Normalize docker registries
|
||||
if strings.HasPrefix(stripped, "index.docker.io/") ||
|
||||
strings.HasPrefix(stripped, "registry-1.docker.io/") ||
|
||||
strings.HasPrefix(stripped, "docker.io/") {
|
||||
if stripped != authFileKey { // URLs are interpreted to mean complete registries
|
||||
stripped = strings.SplitN(stripped, "/", 2)[0]
|
||||
}
|
||||
|
||||
return stripped
|
||||
// Only non-namespaced registry names (or URLs) need to be normalized; repo namespaces
|
||||
// always use the simple format.
|
||||
switch stripped {
|
||||
case "registry-1.docker.io", "index.docker.io":
|
||||
return "docker.io"
|
||||
default:
|
||||
return stripped
|
||||
}
|
||||
}
|
||||
|
||||
// dockerAuthToImageAuth converts a docker auth config to one we're using
|
||||
@ -309,28 +305,26 @@ func imageAuthToDockerAuth(authConfig types.DockerAuthConfig) dockerAPITypes.Aut
|
||||
}
|
||||
}
|
||||
|
||||
// singleAuthHeader extracts a DockerAuthConfig from the request's header.
|
||||
// parseSingleAuthHeader extracts a DockerAuthConfig from an xRegistryAuthHeader value.
|
||||
// The header content is a single DockerAuthConfig.
|
||||
func singleAuthHeader(r *http.Request) (map[string]types.DockerAuthConfig, error) {
|
||||
authHeader := r.Header.Get(string(XRegistryAuthHeader))
|
||||
authConfig := dockerAPITypes.AuthConfig{}
|
||||
func parseSingleAuthHeader(authHeader string) (types.DockerAuthConfig, error) {
|
||||
// Accept "null" and handle it as empty value for compatibility reason with Docker.
|
||||
// Some java docker clients pass this value, e.g. this one used in Eclipse.
|
||||
if len(authHeader) > 0 && authHeader != "null" {
|
||||
authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authHeader))
|
||||
if err := json.NewDecoder(authJSON).Decode(&authConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(authHeader) == 0 || authHeader == "null" {
|
||||
return types.DockerAuthConfig{}, nil
|
||||
}
|
||||
authConfigs := make(map[string]types.DockerAuthConfig)
|
||||
authConfigs["0"] = dockerAuthToImageAuth(authConfig)
|
||||
return authConfigs, nil
|
||||
|
||||
authConfig := dockerAPITypes.AuthConfig{}
|
||||
authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authHeader))
|
||||
if err := json.NewDecoder(authJSON).Decode(&authConfig); err != nil {
|
||||
return types.DockerAuthConfig{}, err
|
||||
}
|
||||
return dockerAuthToImageAuth(authConfig), nil
|
||||
}
|
||||
|
||||
// multiAuthHeader extracts a DockerAuthConfig from the request's header.
|
||||
// parseMultiAuthHeader extracts a DockerAuthConfig from an xRegistryAuthHeader value.
|
||||
// The header content is a map[string]DockerAuthConfigs.
|
||||
func multiAuthHeader(r *http.Request) (map[string]types.DockerAuthConfig, error) {
|
||||
authHeader := r.Header.Get(string(XRegistryAuthHeader))
|
||||
func parseMultiAuthHeader(authHeader string) (map[string]types.DockerAuthConfig, error) {
|
||||
// Accept "null" and handle it as empty value for compatibility reason with Docker.
|
||||
// Some java docker clients pass this value, e.g. this one used in Eclipse.
|
||||
if len(authHeader) == 0 || authHeader == "null" {
|
||||
|
@ -1,13 +1,302 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/containers/image/v5/pkg/docker/config"
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const largeAuthFile = `{"auths":{
|
||||
"docker.io/vendor": {"auth": "ZG9ja2VyOnZlbmRvcg=="},
|
||||
"https://index.docker.io/v1": {"auth": "ZG9ja2VyOnRvcA=="},
|
||||
"quay.io/libpod": {"auth": "cXVheTpsaWJwb2Q="},
|
||||
"quay.io": {"auth": "cXVheTp0b3A="}
|
||||
}}`
|
||||
|
||||
// Semantics of largeAuthFile
|
||||
var largeAuthFileValues = map[string]types.DockerAuthConfig{
|
||||
"docker.io/vendor": {Username: "docker", Password: "vendor"},
|
||||
"docker.io": {Username: "docker", Password: "top"},
|
||||
"quay.io/libpod": {Username: "quay", Password: "libpod"},
|
||||
"quay.io": {Username: "quay", Password: "top"},
|
||||
}
|
||||
|
||||
// systemContextForAuthFile returns a types.SystemContext with AuthFilePath pointing
|
||||
// to a temporary file with fileContents, or nil if fileContents is empty; and a cleanup
|
||||
// function the calle rmust arrange to call.
|
||||
func systemContextForAuthFile(t *testing.T, fileContents string) (*types.SystemContext, func()) {
|
||||
if fileContents == "" {
|
||||
return nil, func() {}
|
||||
}
|
||||
|
||||
f, err := ioutil.TempFile("", "auth.json")
|
||||
require.NoError(t, err)
|
||||
path := f.Name()
|
||||
err = ioutil.WriteFile(path, []byte(fileContents), 0700)
|
||||
require.NoError(t, err)
|
||||
return &types.SystemContext{AuthFilePath: path}, func() { os.Remove(path) }
|
||||
}
|
||||
|
||||
// Test that GetCredentials() correctly parses what MakeXRegistryConfigHeader() produces
|
||||
func TestMakeXRegistryConfigHeaderGetCredentialsRoundtrip(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
fileContents string
|
||||
username, password string
|
||||
expectedOverride *types.DockerAuthConfig
|
||||
expectedFileValues map[string]types.DockerAuthConfig
|
||||
}{
|
||||
{
|
||||
name: "no data",
|
||||
fileContents: "",
|
||||
username: "",
|
||||
password: "",
|
||||
expectedOverride: nil,
|
||||
expectedFileValues: nil,
|
||||
},
|
||||
{
|
||||
name: "file data",
|
||||
fileContents: largeAuthFile,
|
||||
username: "",
|
||||
password: "",
|
||||
expectedOverride: nil,
|
||||
expectedFileValues: largeAuthFileValues,
|
||||
},
|
||||
{
|
||||
name: "file data + override",
|
||||
fileContents: largeAuthFile,
|
||||
username: "override-user",
|
||||
password: "override-pass",
|
||||
expectedOverride: &types.DockerAuthConfig{Username: "override-user", Password: "override-pass"},
|
||||
expectedFileValues: largeAuthFileValues,
|
||||
},
|
||||
} {
|
||||
sys, cleanup := systemContextForAuthFile(t, tc.fileContents)
|
||||
defer cleanup()
|
||||
headers, err := MakeXRegistryConfigHeader(sys, tc.username, tc.password)
|
||||
require.NoError(t, err)
|
||||
req, err := http.NewRequest(http.MethodPost, "/", nil)
|
||||
require.NoError(t, err, tc.name)
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
override, resPath, err := GetCredentials(req)
|
||||
require.NoError(t, err, tc.name)
|
||||
defer RemoveAuthfile(resPath)
|
||||
if tc.expectedOverride == nil {
|
||||
assert.Nil(t, override, tc.name)
|
||||
} else {
|
||||
require.NotNil(t, override, tc.name)
|
||||
assert.Equal(t, *tc.expectedOverride, *override, tc.name)
|
||||
}
|
||||
for key, expectedAuth := range tc.expectedFileValues {
|
||||
auth, err := config.GetCredentials(&types.SystemContext{AuthFilePath: resPath}, key)
|
||||
require.NoError(t, err, tc.name)
|
||||
assert.Equal(t, expectedAuth, auth, "%s, key %s", tc.name, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test that GetCredentials() correctly parses what MakeXRegistryAuthHeader() produces
|
||||
func TestMakeXRegistryAuthHeaderGetCredentialsRoundtrip(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
fileContents string
|
||||
username, password string
|
||||
expectedOverride *types.DockerAuthConfig
|
||||
expectedFileValues map[string]types.DockerAuthConfig
|
||||
}{
|
||||
{
|
||||
name: "override",
|
||||
fileContents: "",
|
||||
username: "override-user",
|
||||
password: "override-pass",
|
||||
expectedOverride: &types.DockerAuthConfig{Username: "override-user", Password: "override-pass"},
|
||||
expectedFileValues: nil,
|
||||
},
|
||||
{
|
||||
name: "file data",
|
||||
fileContents: largeAuthFile,
|
||||
username: "",
|
||||
password: "",
|
||||
expectedFileValues: largeAuthFileValues,
|
||||
},
|
||||
} {
|
||||
sys, cleanup := systemContextForAuthFile(t, tc.fileContents)
|
||||
defer cleanup()
|
||||
headers, err := MakeXRegistryAuthHeader(sys, tc.username, tc.password)
|
||||
require.NoError(t, err)
|
||||
req, err := http.NewRequest(http.MethodPost, "/", nil)
|
||||
require.NoError(t, err, tc.name)
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
override, resPath, err := GetCredentials(req)
|
||||
require.NoError(t, err, tc.name)
|
||||
defer RemoveAuthfile(resPath)
|
||||
if tc.expectedOverride == nil {
|
||||
assert.Nil(t, override, tc.name)
|
||||
} else {
|
||||
require.NotNil(t, override, tc.name)
|
||||
assert.Equal(t, *tc.expectedOverride, *override, tc.name)
|
||||
}
|
||||
for key, expectedAuth := range tc.expectedFileValues {
|
||||
auth, err := config.GetCredentials(&types.SystemContext{AuthFilePath: resPath}, key)
|
||||
require.NoError(t, err, tc.name)
|
||||
assert.Equal(t, expectedAuth, auth, "%s, key %s", tc.name, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeXRegistryConfigHeader(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
fileContents string
|
||||
username, password string
|
||||
shouldErr bool
|
||||
expectedContents string
|
||||
}{
|
||||
{
|
||||
name: "no data",
|
||||
fileContents: "",
|
||||
username: "",
|
||||
password: "",
|
||||
expectedContents: "",
|
||||
},
|
||||
{
|
||||
name: "invalid JSON",
|
||||
fileContents: "@invalid JSON",
|
||||
username: "",
|
||||
password: "",
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "file data",
|
||||
fileContents: largeAuthFile,
|
||||
username: "",
|
||||
password: "",
|
||||
expectedContents: `{
|
||||
"docker.io/vendor": {"username": "docker", "password": "vendor"},
|
||||
"docker.io": {"username": "docker", "password": "top"},
|
||||
"quay.io/libpod": {"username": "quay", "password": "libpod"},
|
||||
"quay.io": {"username": "quay", "password": "top"}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "file data + override",
|
||||
fileContents: largeAuthFile,
|
||||
username: "override-user",
|
||||
password: "override-pass",
|
||||
expectedContents: `{
|
||||
"docker.io/vendor": {"username": "docker", "password": "vendor"},
|
||||
"docker.io": {"username": "docker", "password": "top"},
|
||||
"quay.io/libpod": {"username": "quay", "password": "libpod"},
|
||||
"quay.io": {"username": "quay", "password": "top"},
|
||||
"": {"username": "override-user", "password": "override-pass"}
|
||||
}`,
|
||||
},
|
||||
} {
|
||||
sys, cleanup := systemContextForAuthFile(t, tc.fileContents)
|
||||
defer cleanup()
|
||||
res, err := MakeXRegistryConfigHeader(sys, tc.username, tc.password)
|
||||
if tc.shouldErr {
|
||||
assert.Error(t, err, tc.name)
|
||||
} else {
|
||||
require.NoError(t, err, tc.name)
|
||||
if tc.expectedContents == "" {
|
||||
assert.Empty(t, res, tc.name)
|
||||
} else {
|
||||
require.Len(t, res, 1, tc.name)
|
||||
header, ok := res[xRegistryConfigHeader]
|
||||
require.True(t, ok, tc.name)
|
||||
decodedHeader, err := base64.URLEncoding.DecodeString(header)
|
||||
require.NoError(t, err, tc.name)
|
||||
// Don't test for a specific JSON representation, just for the expected contents.
|
||||
expected := map[string]interface{}{}
|
||||
actual := map[string]interface{}{}
|
||||
err = json.Unmarshal([]byte(tc.expectedContents), &expected)
|
||||
require.NoError(t, err, tc.name)
|
||||
err = json.Unmarshal(decodedHeader, &actual)
|
||||
require.NoError(t, err, tc.name)
|
||||
assert.Equal(t, expected, actual, tc.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeXRegistryAuthHeader(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
fileContents string
|
||||
username, password string
|
||||
shouldErr bool
|
||||
expectedContents string
|
||||
}{
|
||||
{
|
||||
name: "override",
|
||||
fileContents: "",
|
||||
username: "override-user",
|
||||
password: "override-pass",
|
||||
expectedContents: `{"username": "override-user", "password": "override-pass"}`,
|
||||
},
|
||||
{
|
||||
name: "invalid JSON",
|
||||
fileContents: "@invalid JSON",
|
||||
username: "",
|
||||
password: "",
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "file data",
|
||||
fileContents: largeAuthFile,
|
||||
username: "",
|
||||
password: "",
|
||||
expectedContents: `{
|
||||
"docker.io/vendor": {"username": "docker", "password": "vendor"},
|
||||
"docker.io": {"username": "docker", "password": "top"},
|
||||
"quay.io/libpod": {"username": "quay", "password": "libpod"},
|
||||
"quay.io": {"username": "quay", "password": "top"}
|
||||
}`,
|
||||
},
|
||||
} {
|
||||
sys, cleanup := systemContextForAuthFile(t, tc.fileContents)
|
||||
defer cleanup()
|
||||
res, err := MakeXRegistryAuthHeader(sys, tc.username, tc.password)
|
||||
if tc.shouldErr {
|
||||
assert.Error(t, err, tc.name)
|
||||
} else {
|
||||
require.NoError(t, err, tc.name)
|
||||
if tc.expectedContents == "" {
|
||||
assert.Empty(t, res, tc.name)
|
||||
} else {
|
||||
require.Len(t, res, 1, tc.name)
|
||||
header, ok := res[xRegistryAuthHeader]
|
||||
require.True(t, ok, tc.name)
|
||||
decodedHeader, err := base64.URLEncoding.DecodeString(header)
|
||||
require.NoError(t, err, tc.name)
|
||||
// Don't test for a specific JSON representation, just for the expected contents.
|
||||
expected := map[string]interface{}{}
|
||||
actual := map[string]interface{}{}
|
||||
err = json.Unmarshal([]byte(tc.expectedContents), &expected)
|
||||
require.NoError(t, err, tc.name)
|
||||
err = json.Unmarshal(decodedHeader, &actual)
|
||||
require.NoError(t, err, tc.name)
|
||||
assert.Equal(t, expected, actual, tc.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthConfigsToAuthFile(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
@ -22,28 +311,28 @@ func TestAuthConfigsToAuthFile(t *testing.T) {
|
||||
expectedContains: "{}",
|
||||
},
|
||||
{
|
||||
name: "registry with prefix",
|
||||
name: "registry with a namespace prefix",
|
||||
server: "my-registry.local/username",
|
||||
shouldErr: false,
|
||||
expectedContains: `"my-registry.local/username":`,
|
||||
},
|
||||
{
|
||||
name: "normalize https:// prefix",
|
||||
name: "URLs are interpreted as full registries",
|
||||
server: "http://my-registry.local/username",
|
||||
shouldErr: false,
|
||||
expectedContains: `"my-registry.local/username":`,
|
||||
expectedContains: `"my-registry.local":`,
|
||||
},
|
||||
{
|
||||
name: "normalize docker registry with https prefix",
|
||||
name: "the old-style docker registry URL is normalized",
|
||||
server: "http://index.docker.io/v1/",
|
||||
shouldErr: false,
|
||||
expectedContains: `"index.docker.io":`,
|
||||
expectedContains: `"docker.io":`,
|
||||
},
|
||||
{
|
||||
name: "normalize docker registry without https prefix",
|
||||
server: "docker.io/v2/",
|
||||
name: "docker.io vendor namespace",
|
||||
server: "docker.io/vendor",
|
||||
shouldErr: false,
|
||||
expectedContains: `"docker.io":`,
|
||||
expectedContains: `"docker.io/vendor":`,
|
||||
},
|
||||
} {
|
||||
configs := map[string]types.DockerAuthConfig{}
|
||||
@ -54,13 +343,79 @@ func TestAuthConfigsToAuthFile(t *testing.T) {
|
||||
filePath, err := authConfigsToAuthFile(configs)
|
||||
|
||||
if tc.shouldErr {
|
||||
assert.NotNil(t, err)
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, filePath)
|
||||
} else {
|
||||
assert.Nil(t, err)
|
||||
assert.NoError(t, err)
|
||||
content, err := ioutil.ReadFile(filePath)
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(content), tc.expectedContains)
|
||||
os.Remove(filePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSingleAuthHeader(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
expected types.DockerAuthConfig
|
||||
}{
|
||||
{
|
||||
input: "", // An empty (or missing) header
|
||||
expected: types.DockerAuthConfig{},
|
||||
},
|
||||
{
|
||||
input: "null",
|
||||
expected: types.DockerAuthConfig{},
|
||||
},
|
||||
// Invalid JSON
|
||||
{input: "@", shouldErr: true},
|
||||
// Success
|
||||
{
|
||||
input: base64.URLEncoding.EncodeToString([]byte(`{"username":"u1","password":"p1"}`)),
|
||||
expected: types.DockerAuthConfig{Username: "u1", Password: "p1"},
|
||||
},
|
||||
} {
|
||||
res, err := parseSingleAuthHeader(tc.input)
|
||||
if tc.shouldErr {
|
||||
assert.Error(t, err, tc.input)
|
||||
} else {
|
||||
require.NoError(t, err, tc.input)
|
||||
assert.Equal(t, tc.expected, res, tc.input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMultiAuthHeader(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
expected map[string]types.DockerAuthConfig
|
||||
}{
|
||||
// Empty header
|
||||
{input: "", expected: nil},
|
||||
// "null"
|
||||
{input: "null", expected: nil},
|
||||
// Invalid JSON
|
||||
{input: "@", shouldErr: true},
|
||||
// Success
|
||||
{
|
||||
input: base64.URLEncoding.EncodeToString([]byte(
|
||||
`{"https://index.docker.io/v1/":{"username":"u1","password":"p1"},` +
|
||||
`"quay.io/libpod":{"username":"u2","password":"p2"}}`)),
|
||||
expected: map[string]types.DockerAuthConfig{
|
||||
"https://index.docker.io/v1/": {Username: "u1", Password: "p1"},
|
||||
"quay.io/libpod": {Username: "u2", Password: "p2"},
|
||||
},
|
||||
},
|
||||
} {
|
||||
res, err := parseMultiAuthHeader(tc.input)
|
||||
if tc.shouldErr {
|
||||
assert.Error(t, err, tc.input)
|
||||
} else {
|
||||
require.NoError(t, err, tc.input)
|
||||
assert.Equal(t, tc.expected, res, tc.input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -293,14 +293,10 @@ func Build(ctx context.Context, containerFiles []string, options entities.BuildO
|
||||
headers map[string]string
|
||||
err error
|
||||
)
|
||||
if options.SystemContext == nil {
|
||||
headers, err = auth.Header(options.SystemContext, auth.XRegistryConfigHeader, "", "", "")
|
||||
if options.SystemContext != nil && options.SystemContext.DockerAuthConfig != nil {
|
||||
headers, err = auth.MakeXRegistryAuthHeader(options.SystemContext, options.SystemContext.DockerAuthConfig.Username, options.SystemContext.DockerAuthConfig.Password)
|
||||
} else {
|
||||
if options.SystemContext.DockerAuthConfig != nil {
|
||||
headers, err = auth.Header(options.SystemContext, auth.XRegistryAuthHeader, options.SystemContext.AuthFilePath, options.SystemContext.DockerAuthConfig.Username, options.SystemContext.DockerAuthConfig.Password)
|
||||
} else {
|
||||
headers, err = auth.Header(options.SystemContext, auth.XRegistryConfigHeader, options.SystemContext.AuthFilePath, "", "")
|
||||
}
|
||||
headers, err = auth.MakeXRegistryConfigHeader(options.SystemContext, "", "")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
imageTypes "github.com/containers/image/v5/types"
|
||||
"github.com/containers/podman/v3/pkg/api/handlers/types"
|
||||
"github.com/containers/podman/v3/pkg/auth"
|
||||
"github.com/containers/podman/v3/pkg/bindings"
|
||||
@ -280,7 +281,7 @@ func Push(ctx context.Context, source string, destination string, options *PushO
|
||||
return err
|
||||
}
|
||||
// TODO: have a global system context we can pass around (1st argument)
|
||||
header, err := auth.Header(nil, auth.XRegistryAuthHeader, options.GetAuthfile(), options.GetUsername(), options.GetPassword())
|
||||
header, err := auth.MakeXRegistryAuthHeader(&imageTypes.SystemContext{AuthFilePath: options.GetAuthfile()}, options.GetUsername(), options.GetPassword())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -329,7 +330,7 @@ func Search(ctx context.Context, term string, options *SearchOptions) ([]entitie
|
||||
}
|
||||
|
||||
// TODO: have a global system context we can pass around (1st argument)
|
||||
header, err := auth.Header(nil, auth.XRegistryAuthHeader, options.GetAuthfile(), "", "")
|
||||
header, err := auth.MakeXRegistryAuthHeader(&imageTypes.SystemContext{AuthFilePath: options.GetAuthfile()}, "", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/containers/podman/v3/pkg/auth"
|
||||
"github.com/containers/podman/v3/pkg/bindings"
|
||||
"github.com/containers/podman/v3/pkg/domain/entities"
|
||||
@ -42,7 +43,7 @@ func Pull(ctx context.Context, rawImage string, options *PullOptions) ([]string,
|
||||
}
|
||||
|
||||
// TODO: have a global system context we can pass around (1st argument)
|
||||
header, err := auth.Header(nil, auth.XRegistryAuthHeader, options.GetAuthfile(), options.GetUsername(), options.GetPassword())
|
||||
header, err := auth.MakeXRegistryAuthHeader(&types.SystemContext{AuthFilePath: options.GetAuthfile()}, options.GetUsername(), options.GetPassword())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/containers/podman/v3/pkg/auth"
|
||||
"github.com/containers/podman/v3/pkg/bindings"
|
||||
"github.com/containers/podman/v3/pkg/domain/entities"
|
||||
@ -40,7 +41,7 @@ func Kube(ctx context.Context, path string, options *KubeOptions) (*entities.Pla
|
||||
}
|
||||
|
||||
// TODO: have a global system context we can pass around (1st argument)
|
||||
header, err := auth.Header(nil, auth.XRegistryAuthHeader, options.GetAuthfile(), options.GetUsername(), options.GetPassword())
|
||||
header, err := auth.MakeXRegistryAuthHeader(&types.SystemContext{AuthFilePath: options.GetAuthfile()}, options.GetUsername(), options.GetPassword())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
Reference in New Issue
Block a user