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 }