mirror of
				https://github.com/containers/podman.git
				synced 2025-10-31 10:00:01 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			422 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			422 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| 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 caller must 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 _, v := range headers.Values(xRegistryConfigHeader) {
 | |
| 			req.Header.Add(xRegistryConfigHeader, 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 _, v := range headers.Values(xRegistryAuthHeader) {
 | |
| 			req.Header.Set(xRegistryAuthHeader, 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[0])
 | |
| 				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[0])
 | |
| 				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
 | |
| 		server           string
 | |
| 		shouldErr        bool
 | |
| 		expectedContains string
 | |
| 	}{
 | |
| 		{
 | |
| 			name:             "empty auth configs",
 | |
| 			server:           "",
 | |
| 			shouldErr:        false,
 | |
| 			expectedContains: "{}",
 | |
| 		},
 | |
| 		{
 | |
| 			name:             "registry with a namespace prefix",
 | |
| 			server:           "my-registry.local/username",
 | |
| 			shouldErr:        false,
 | |
| 			expectedContains: `"my-registry.local/username":`,
 | |
| 		},
 | |
| 		{
 | |
| 			name:             "URLs are interpreted as full registries",
 | |
| 			server:           "http://my-registry.local/username",
 | |
| 			shouldErr:        false,
 | |
| 			expectedContains: `"my-registry.local":`,
 | |
| 		},
 | |
| 		{
 | |
| 			name:             "the old-style docker registry URL is normalized",
 | |
| 			server:           "http://index.docker.io/v1/",
 | |
| 			shouldErr:        false,
 | |
| 			expectedContains: `"docker.io":`,
 | |
| 		},
 | |
| 		{
 | |
| 			name:             "docker.io vendor namespace",
 | |
| 			server:           "docker.io/vendor",
 | |
| 			shouldErr:        false,
 | |
| 			expectedContains: `"docker.io/vendor":`,
 | |
| 		},
 | |
| 	} {
 | |
| 		configs := map[string]types.DockerAuthConfig{}
 | |
| 		if tc.server != "" {
 | |
| 			configs[tc.server] = types.DockerAuthConfig{}
 | |
| 		}
 | |
| 
 | |
| 		filePath, err := authConfigsToAuthFile(configs)
 | |
| 
 | |
| 		if tc.shouldErr {
 | |
| 			assert.Error(t, err)
 | |
| 			assert.Empty(t, filePath)
 | |
| 		} else {
 | |
| 			assert.NoError(t, err)
 | |
| 			content, err := ioutil.ReadFile(filePath)
 | |
| 			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)
 | |
| 		}
 | |
| 	}
 | |
| }
 | 
