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

     1  package auth
     2  
     3  import (
     4  	"encoding/base64"
     5  	"encoding/json"
     6  	"io/ioutil"
     7  	"net/http"
     8  	"os"
     9  	"testing"
    10  
    11  	"github.com/containers/image/v5/pkg/docker/config"
    12  	"github.com/containers/image/v5/types"
    13  	"github.com/stretchr/testify/assert"
    14  	"github.com/stretchr/testify/require"
    15  )
    16  
    17  const largeAuthFile = `{"auths":{
    18  	"docker.io/vendor": {"auth": "ZG9ja2VyOnZlbmRvcg=="},
    19  	"https://index.docker.io/v1": {"auth": "ZG9ja2VyOnRvcA=="},
    20  	"quay.io/libpod": {"auth": "cXVheTpsaWJwb2Q="},
    21  	"quay.io": {"auth": "cXVheTp0b3A="}
    22  }}`
    23  
    24  // Semantics of largeAuthFile
    25  var largeAuthFileValues = map[string]types.DockerAuthConfig{
    26  	"docker.io/vendor": {Username: "docker", Password: "vendor"},
    27  	"docker.io":        {Username: "docker", Password: "top"},
    28  	"quay.io/libpod":   {Username: "quay", Password: "libpod"},
    29  	"quay.io":          {Username: "quay", Password: "top"},
    30  }
    31  
    32  // systemContextForAuthFile returns a types.SystemContext with AuthFilePath pointing
    33  // to a temporary file with fileContents, or nil if fileContents is empty; and a cleanup
    34  // function the caller must arrange to call.
    35  func systemContextForAuthFile(t *testing.T, fileContents string) (*types.SystemContext, func()) {
    36  	if fileContents == "" {
    37  		return nil, func() {}
    38  	}
    39  
    40  	f, err := ioutil.TempFile("", "auth.json")
    41  	require.NoError(t, err)
    42  	path := f.Name()
    43  	err = ioutil.WriteFile(path, []byte(fileContents), 0700)
    44  	require.NoError(t, err)
    45  	return &types.SystemContext{AuthFilePath: path}, func() { os.Remove(path) }
    46  }
    47  
    48  // Test that GetCredentials() correctly parses what MakeXRegistryConfigHeader() produces
    49  func TestMakeXRegistryConfigHeaderGetCredentialsRoundtrip(t *testing.T) {
    50  	for _, tc := range []struct {
    51  		name               string
    52  		fileContents       string
    53  		username, password string
    54  		expectedOverride   *types.DockerAuthConfig
    55  		expectedFileValues map[string]types.DockerAuthConfig
    56  	}{
    57  		{
    58  			name:               "no data",
    59  			fileContents:       "",
    60  			username:           "",
    61  			password:           "",
    62  			expectedOverride:   nil,
    63  			expectedFileValues: nil,
    64  		},
    65  		{
    66  			name:               "file data",
    67  			fileContents:       largeAuthFile,
    68  			username:           "",
    69  			password:           "",
    70  			expectedOverride:   nil,
    71  			expectedFileValues: largeAuthFileValues,
    72  		},
    73  		{
    74  			name:               "file data + override",
    75  			fileContents:       largeAuthFile,
    76  			username:           "override-user",
    77  			password:           "override-pass",
    78  			expectedOverride:   &types.DockerAuthConfig{Username: "override-user", Password: "override-pass"},
    79  			expectedFileValues: largeAuthFileValues,
    80  		},
    81  	} {
    82  		sys, cleanup := systemContextForAuthFile(t, tc.fileContents)
    83  		defer cleanup()
    84  		headers, err := MakeXRegistryConfigHeader(sys, tc.username, tc.password)
    85  		require.NoError(t, err)
    86  		req, err := http.NewRequest(http.MethodPost, "/", nil)
    87  		require.NoError(t, err, tc.name)
    88  		for _, v := range headers.Values(xRegistryConfigHeader) {
    89  			req.Header.Add(xRegistryConfigHeader, v)
    90  		}
    91  
    92  		override, resPath, err := GetCredentials(req)
    93  		require.NoError(t, err, tc.name)
    94  		defer RemoveAuthfile(resPath)
    95  		if tc.expectedOverride == nil {
    96  			assert.Nil(t, override, tc.name)
    97  		} else {
    98  			require.NotNil(t, override, tc.name)
    99  			assert.Equal(t, *tc.expectedOverride, *override, tc.name)
   100  		}
   101  		for key, expectedAuth := range tc.expectedFileValues {
   102  			auth, err := config.GetCredentials(&types.SystemContext{AuthFilePath: resPath}, key)
   103  			require.NoError(t, err, tc.name)
   104  			assert.Equal(t, expectedAuth, auth, "%s, key %s", tc.name, key)
   105  		}
   106  	}
   107  }
   108  
   109  // Test that GetCredentials() correctly parses what MakeXRegistryAuthHeader() produces
   110  func TestMakeXRegistryAuthHeaderGetCredentialsRoundtrip(t *testing.T) {
   111  	for _, tc := range []struct {
   112  		name               string
   113  		fileContents       string
   114  		username, password string
   115  		expectedOverride   *types.DockerAuthConfig
   116  		expectedFileValues map[string]types.DockerAuthConfig
   117  	}{
   118  		{
   119  			name:               "override",
   120  			fileContents:       "",
   121  			username:           "override-user",
   122  			password:           "override-pass",
   123  			expectedOverride:   &types.DockerAuthConfig{Username: "override-user", Password: "override-pass"},
   124  			expectedFileValues: nil,
   125  		},
   126  		{
   127  			name:               "file data",
   128  			fileContents:       largeAuthFile,
   129  			username:           "",
   130  			password:           "",
   131  			expectedFileValues: largeAuthFileValues,
   132  		},
   133  	} {
   134  		sys, cleanup := systemContextForAuthFile(t, tc.fileContents)
   135  		defer cleanup()
   136  		headers, err := MakeXRegistryAuthHeader(sys, tc.username, tc.password)
   137  		require.NoError(t, err)
   138  		req, err := http.NewRequest(http.MethodPost, "/", nil)
   139  		require.NoError(t, err, tc.name)
   140  		for _, v := range headers.Values(xRegistryAuthHeader) {
   141  			req.Header.Set(xRegistryAuthHeader, v)
   142  		}
   143  
   144  		override, resPath, err := GetCredentials(req)
   145  		require.NoError(t, err, tc.name)
   146  		defer RemoveAuthfile(resPath)
   147  		if tc.expectedOverride == nil {
   148  			assert.Nil(t, override, tc.name)
   149  		} else {
   150  			require.NotNil(t, override, tc.name)
   151  			assert.Equal(t, *tc.expectedOverride, *override, tc.name)
   152  		}
   153  		for key, expectedAuth := range tc.expectedFileValues {
   154  			auth, err := config.GetCredentials(&types.SystemContext{AuthFilePath: resPath}, key)
   155  			require.NoError(t, err, tc.name)
   156  			assert.Equal(t, expectedAuth, auth, "%s, key %s", tc.name, key)
   157  		}
   158  	}
   159  }
   160  
   161  func TestMakeXRegistryConfigHeader(t *testing.T) {
   162  	for _, tc := range []struct {
   163  		name               string
   164  		fileContents       string
   165  		username, password string
   166  		shouldErr          bool
   167  		expectedContents   string
   168  	}{
   169  		{
   170  			name:             "no data",
   171  			fileContents:     "",
   172  			username:         "",
   173  			password:         "",
   174  			expectedContents: "",
   175  		},
   176  		{
   177  			name:         "invalid JSON",
   178  			fileContents: "@invalid JSON",
   179  			username:     "",
   180  			password:     "",
   181  			shouldErr:    true,
   182  		},
   183  		{
   184  			name:         "file data",
   185  			fileContents: largeAuthFile,
   186  			username:     "",
   187  			password:     "",
   188  			expectedContents: `{
   189  			"docker.io/vendor": {"username": "docker", "password": "vendor"},
   190  			"docker.io": {"username": "docker", "password": "top"},
   191  			"quay.io/libpod": {"username": "quay", "password": "libpod"},
   192  			"quay.io": {"username": "quay", "password": "top"}
   193  			}`,
   194  		},
   195  		{
   196  			name:         "file data + override",
   197  			fileContents: largeAuthFile,
   198  			username:     "override-user",
   199  			password:     "override-pass",
   200  			expectedContents: `{
   201  				"docker.io/vendor": {"username": "docker", "password": "vendor"},
   202  				"docker.io": {"username": "docker", "password": "top"},
   203  				"quay.io/libpod": {"username": "quay", "password": "libpod"},
   204  				"quay.io": {"username": "quay", "password": "top"},
   205  				"": {"username": "override-user", "password": "override-pass"}
   206  				}`,
   207  		},
   208  	} {
   209  		sys, cleanup := systemContextForAuthFile(t, tc.fileContents)
   210  		defer cleanup()
   211  		res, err := MakeXRegistryConfigHeader(sys, tc.username, tc.password)
   212  		if tc.shouldErr {
   213  			assert.Error(t, err, tc.name)
   214  		} else {
   215  			require.NoError(t, err, tc.name)
   216  			if tc.expectedContents == "" {
   217  				assert.Empty(t, res, tc.name)
   218  			} else {
   219  				require.Len(t, res, 1, tc.name)
   220  				header, ok := res[xRegistryConfigHeader]
   221  				require.True(t, ok, tc.name)
   222  				decodedHeader, err := base64.URLEncoding.DecodeString(header[0])
   223  				require.NoError(t, err, tc.name)
   224  				// Don't test for a specific JSON representation, just for the expected contents.
   225  				expected := map[string]interface{}{}
   226  				actual := map[string]interface{}{}
   227  				err = json.Unmarshal([]byte(tc.expectedContents), &expected)
   228  				require.NoError(t, err, tc.name)
   229  				err = json.Unmarshal(decodedHeader, &actual)
   230  				require.NoError(t, err, tc.name)
   231  				assert.Equal(t, expected, actual, tc.name)
   232  			}
   233  		}
   234  	}
   235  }
   236  
   237  func TestMakeXRegistryAuthHeader(t *testing.T) {
   238  	for _, tc := range []struct {
   239  		name               string
   240  		fileContents       string
   241  		username, password string
   242  		shouldErr          bool
   243  		expectedContents   string
   244  	}{
   245  		{
   246  			name:             "override",
   247  			fileContents:     "",
   248  			username:         "override-user",
   249  			password:         "override-pass",
   250  			expectedContents: `{"username": "override-user", "password": "override-pass"}`,
   251  		},
   252  		{
   253  			name:         "invalid JSON",
   254  			fileContents: "@invalid JSON",
   255  			username:     "",
   256  			password:     "",
   257  			shouldErr:    true,
   258  		},
   259  		{
   260  			name:         "file data",
   261  			fileContents: largeAuthFile,
   262  			username:     "",
   263  			password:     "",
   264  			expectedContents: `{
   265  			"docker.io/vendor": {"username": "docker", "password": "vendor"},
   266  			"docker.io": {"username": "docker", "password": "top"},
   267  			"quay.io/libpod": {"username": "quay", "password": "libpod"},
   268  			"quay.io": {"username": "quay", "password": "top"}
   269  			}`,
   270  		},
   271  	} {
   272  		sys, cleanup := systemContextForAuthFile(t, tc.fileContents)
   273  		defer cleanup()
   274  		res, err := MakeXRegistryAuthHeader(sys, tc.username, tc.password)
   275  		if tc.shouldErr {
   276  			assert.Error(t, err, tc.name)
   277  		} else {
   278  			require.NoError(t, err, tc.name)
   279  			if tc.expectedContents == "" {
   280  				assert.Empty(t, res, tc.name)
   281  			} else {
   282  				require.Len(t, res, 1, tc.name)
   283  				header, ok := res[xRegistryAuthHeader]
   284  				require.True(t, ok, tc.name)
   285  				decodedHeader, err := base64.URLEncoding.DecodeString(header[0])
   286  				require.NoError(t, err, tc.name)
   287  				// Don't test for a specific JSON representation, just for the expected contents.
   288  				expected := map[string]interface{}{}
   289  				actual := map[string]interface{}{}
   290  				err = json.Unmarshal([]byte(tc.expectedContents), &expected)
   291  				require.NoError(t, err, tc.name)
   292  				err = json.Unmarshal(decodedHeader, &actual)
   293  				require.NoError(t, err, tc.name)
   294  				assert.Equal(t, expected, actual, tc.name)
   295  			}
   296  		}
   297  	}
   298  }
   299  
   300  func TestAuthConfigsToAuthFile(t *testing.T) {
   301  	for _, tc := range []struct {
   302  		name             string
   303  		server           string
   304  		shouldErr        bool
   305  		expectedContains string
   306  	}{
   307  		{
   308  			name:             "empty auth configs",
   309  			server:           "",
   310  			shouldErr:        false,
   311  			expectedContains: "{}",
   312  		},
   313  		{
   314  			name:             "registry with a namespace prefix",
   315  			server:           "my-registry.local/username",
   316  			shouldErr:        false,
   317  			expectedContains: `"my-registry.local/username":`,
   318  		},
   319  		{
   320  			name:             "URLs are interpreted as full registries",
   321  			server:           "http://my-registry.local/username",
   322  			shouldErr:        false,
   323  			expectedContains: `"my-registry.local":`,
   324  		},
   325  		{
   326  			name:             "the old-style docker registry URL is normalized",
   327  			server:           "http://index.docker.io/v1/",
   328  			shouldErr:        false,
   329  			expectedContains: `"docker.io":`,
   330  		},
   331  		{
   332  			name:             "docker.io vendor namespace",
   333  			server:           "docker.io/vendor",
   334  			shouldErr:        false,
   335  			expectedContains: `"docker.io/vendor":`,
   336  		},
   337  	} {
   338  		configs := map[string]types.DockerAuthConfig{}
   339  		if tc.server != "" {
   340  			configs[tc.server] = types.DockerAuthConfig{}
   341  		}
   342  
   343  		filePath, err := authConfigsToAuthFile(configs)
   344  
   345  		if tc.shouldErr {
   346  			assert.Error(t, err)
   347  			assert.Empty(t, filePath)
   348  		} else {
   349  			assert.NoError(t, err)
   350  			content, err := ioutil.ReadFile(filePath)
   351  			require.NoError(t, err)
   352  			assert.Contains(t, string(content), tc.expectedContains)
   353  			os.Remove(filePath)
   354  		}
   355  	}
   356  }
   357  
   358  func TestParseSingleAuthHeader(t *testing.T) {
   359  	for _, tc := range []struct {
   360  		input     string
   361  		shouldErr bool
   362  		expected  types.DockerAuthConfig
   363  	}{
   364  		{
   365  			input:    "", // An empty (or missing) header
   366  			expected: types.DockerAuthConfig{},
   367  		},
   368  		{
   369  			input:    "null",
   370  			expected: types.DockerAuthConfig{},
   371  		},
   372  		// Invalid JSON
   373  		{input: "@", shouldErr: true},
   374  		// Success
   375  		{
   376  			input:    base64.URLEncoding.EncodeToString([]byte(`{"username":"u1","password":"p1"}`)),
   377  			expected: types.DockerAuthConfig{Username: "u1", Password: "p1"},
   378  		},
   379  	} {
   380  		res, err := parseSingleAuthHeader(tc.input)
   381  		if tc.shouldErr {
   382  			assert.Error(t, err, tc.input)
   383  		} else {
   384  			require.NoError(t, err, tc.input)
   385  			assert.Equal(t, tc.expected, res, tc.input)
   386  		}
   387  	}
   388  }
   389  
   390  func TestParseMultiAuthHeader(t *testing.T) {
   391  	for _, tc := range []struct {
   392  		input     string
   393  		shouldErr bool
   394  		expected  map[string]types.DockerAuthConfig
   395  	}{
   396  		// Empty header
   397  		{input: "", expected: nil},
   398  		// "null"
   399  		{input: "null", expected: nil},
   400  		// Invalid JSON
   401  		{input: "@", shouldErr: true},
   402  		// Success
   403  		{
   404  			input: base64.URLEncoding.EncodeToString([]byte(
   405  				`{"https://index.docker.io/v1/":{"username":"u1","password":"p1"},` +
   406  					`"quay.io/libpod":{"username":"u2","password":"p2"}}`)),
   407  			expected: map[string]types.DockerAuthConfig{
   408  				"https://index.docker.io/v1/": {Username: "u1", Password: "p1"},
   409  				"quay.io/libpod":              {Username: "u2", Password: "p2"},
   410  			},
   411  		},
   412  	} {
   413  		res, err := parseMultiAuthHeader(tc.input)
   414  		if tc.shouldErr {
   415  			assert.Error(t, err, tc.input)
   416  		} else {
   417  			require.NoError(t, err, tc.input)
   418  			assert.Equal(t, tc.expected, res, tc.input)
   419  		}
   420  	}
   421  }