fix(security): scope auth headers to registry origin
Replace setAuthHeader with setScopedAuth that verifies the request URL matches the registry origin before attaching credentials. Defense-in-depth against credential leakage even if origin pinning on Location is bypassed. Thread baseURL through uploadBlobChunks, uploadBlobChunk, and finalizeBlobUpload so the origin check has access to the registry host.
- commit
- 92dab3814b5eaf3f437fb92d1ab15a655ab87c26
- author
- brice · · 2026-03-14 22:12 UTC
- parent
- aa2bfce
- signature
- unsigned
2 files · +86 -14
M pkg/registrypush/push.go +25 -14
| @@ -147,12 +147,12 @@ func (p *Pusher) UploadBlob(ctx context.Context, baseURL, repo, digest string, s | |||
| 147 | 147 | } | |
| 148 | 148 | } | |
| 149 | 149 | ||
| 150 | - | uploadURL, err = p.uploadBlobChunks(ctx, uploadURL, content, auth) | |
| 150 | + | uploadURL, err = p.uploadBlobChunks(ctx, baseURL, uploadURL, content, auth) | |
| 151 | 151 | if err != nil { | |
| 152 | 152 | return err | |
| 153 | 153 | } | |
| 154 | 154 | ||
| 155 | - | return p.finalizeBlobUpload(ctx, uploadURL, digest, auth) | |
| 155 | + | return p.finalizeBlobUpload(ctx, baseURL, uploadURL, digest, auth) | |
| 156 | 156 | } | |
| 157 | 157 | ||
| 158 | 158 | // defaultImageSource reads an image from the local Docker/Podman daemon. | |
| @@ -283,7 +283,7 @@ func (p *Pusher) uploadManifest(ctx context.Context, baseURL, repo, tag string, | |||
| 283 | 283 | if err != nil { | |
| 284 | 284 | return fmt.Errorf("failed to create manifest upload request: %w", err) | |
| 285 | 285 | } | |
| 286 | - | setAuthHeader(req, auth) | |
| 286 | + | setScopedAuth(req, auth, baseURL) | |
| 287 | 287 | req.Header.Set("Content-Type", string(mediaType)) | |
| 288 | 288 | req.ContentLength = int64(len(manifest)) | |
| 289 | 289 | ||
| @@ -346,7 +346,7 @@ func (p *Pusher) checkBlobExists(ctx context.Context, baseURL, repo, digest, aut | |||
| 346 | 346 | if err != nil { | |
| 347 | 347 | return fmt.Errorf("failed to create blob HEAD request: %w", err) | |
| 348 | 348 | } | |
| 349 | - | setAuthHeader(req, auth) | |
| 349 | + | setScopedAuth(req, auth, baseURL) | |
| 350 | 350 | ||
| 351 | 351 | resp, err := p.httpClient().Do(req) | |
| 352 | 352 | if err != nil { | |
| @@ -370,7 +370,7 @@ func (p *Pusher) startBlobUpload(ctx context.Context, baseURL, repo, auth string | |||
| 370 | 370 | if err != nil { | |
| 371 | 371 | return "", fmt.Errorf("failed to create blob upload start request: %w", err) | |
| 372 | 372 | } | |
| 373 | - | setAuthHeader(req, auth) | |
| 373 | + | setScopedAuth(req, auth, baseURL) | |
| 374 | 374 | ||
| 375 | 375 | resp, err := p.httpClient().Do(req) | |
| 376 | 376 | if err != nil { | |
| @@ -391,7 +391,7 @@ func (p *Pusher) startBlobUpload(ctx context.Context, baseURL, repo, auth string | |||
| 391 | 391 | return location, nil | |
| 392 | 392 | } | |
| 393 | 393 | ||
| 394 | - | func (p *Pusher) uploadBlobChunks(ctx context.Context, uploadURL string, content io.Reader, auth string) (string, error) { | |
| 394 | + | func (p *Pusher) uploadBlobChunks(ctx context.Context, baseURL, uploadURL string, content io.Reader, auth string) (string, error) { | |
| 395 | 395 | buffer := make([]byte, p.chunkSize) | |
| 396 | 396 | offset := int64(0) | |
| 397 | 397 | ||
| @@ -404,7 +404,7 @@ func (p *Pusher) uploadBlobChunks(ctx context.Context, uploadURL string, content | |||
| 404 | 404 | return "", fmt.Errorf("failed to read blob chunk: %w", err) | |
| 405 | 405 | } | |
| 406 | 406 | ||
| 407 | - | nextURL, patchErr := p.uploadBlobChunk(ctx, uploadURL, buffer[:readBytes], offset, auth) | |
| 407 | + | nextURL, patchErr := p.uploadBlobChunk(ctx, baseURL, uploadURL, buffer[:readBytes], offset, auth) | |
| 408 | 408 | if patchErr != nil { | |
| 409 | 409 | return "", patchErr | |
| 410 | 410 | } | |
| @@ -418,12 +418,12 @@ func (p *Pusher) uploadBlobChunks(ctx context.Context, uploadURL string, content | |||
| 418 | 418 | } | |
| 419 | 419 | } | |
| 420 | 420 | ||
| 421 | - | func (p *Pusher) uploadBlobChunk(ctx context.Context, uploadURL string, chunk []byte, offset int64, auth string) (string, error) { | |
| 421 | + | func (p *Pusher) uploadBlobChunk(ctx context.Context, baseURL, uploadURL string, chunk []byte, offset int64, auth string) (string, error) { | |
| 422 | 422 | req, err := http.NewRequestWithContext(ctx, http.MethodPatch, uploadURL, bytesReader(chunk)) | |
| 423 | 423 | if err != nil { | |
| 424 | 424 | return "", fmt.Errorf("failed to create blob chunk request: %w", err) | |
| 425 | 425 | } | |
| 426 | - | setAuthHeader(req, auth) | |
| 426 | + | setScopedAuth(req, auth, baseURL) | |
| 427 | 427 | req.Header.Set("Content-Type", "application/octet-stream") | |
| 428 | 428 | req.Header.Set("Content-Range", fmt.Sprintf("%d-%d", offset, offset+int64(len(chunk))-1)) | |
| 429 | 429 | req.ContentLength = int64(len(chunk)) | |
| @@ -447,7 +447,7 @@ func (p *Pusher) uploadBlobChunk(ctx context.Context, uploadURL string, chunk [] | |||
| 447 | 447 | return nextURL, nil | |
| 448 | 448 | } | |
| 449 | 449 | ||
| 450 | - | func (p *Pusher) finalizeBlobUpload(ctx context.Context, uploadURL, digest, auth string) error { | |
| 450 | + | func (p *Pusher) finalizeBlobUpload(ctx context.Context, baseURL, uploadURL, digest, auth string) error { | |
| 451 | 451 | parsedURL, err := url.Parse(uploadURL) | |
| 452 | 452 | if err != nil { | |
| 453 | 453 | return fmt.Errorf("failed to parse blob upload location: %w", err) | |
| @@ -461,7 +461,7 @@ func (p *Pusher) finalizeBlobUpload(ctx context.Context, uploadURL, digest, auth | |||
| 461 | 461 | if err != nil { | |
| 462 | 462 | return fmt.Errorf("failed to create blob finalize request: %w", err) | |
| 463 | 463 | } | |
| 464 | - | setAuthHeader(req, auth) | |
| 464 | + | setScopedAuth(req, auth, baseURL) | |
| 465 | 465 | ||
| 466 | 466 | resp, err := p.httpClient().Do(req) | |
| 467 | 467 | if err != nil { | |
| @@ -481,10 +481,21 @@ func (p *Pusher) httpClient() *http.Client { | |||
| 481 | 481 | return &http.Client{Transport: p.transport} | |
| 482 | 482 | } | |
| 483 | 483 | ||
| 484 | - | func setAuthHeader(req *http.Request, auth string) { | |
| 485 | - | if auth != "" { | |
| 486 | - | req.Header.Set("Authorization", auth) | |
| 484 | + | // setScopedAuth attaches the auth header only if the request URL matches the | |
| 485 | + | // registry origin (scheme + host). This prevents credential leakage if a | |
| 486 | + | // redirect somehow escapes origin pinning. | |
| 487 | + | func setScopedAuth(req *http.Request, auth, registryOrigin string) { | |
| 488 | + | if auth == "" { | |
| 489 | + | return | |
| 487 | 490 | } | |
| 491 | + | origin, err := url.Parse(registryOrigin) | |
| 492 | + | if err != nil { | |
| 493 | + | return | |
| 494 | + | } | |
| 495 | + | if req.URL.Scheme != origin.Scheme || req.URL.Host != origin.Host { | |
| 496 | + | return | |
| 497 | + | } | |
| 498 | + | req.Header.Set("Authorization", auth) | |
| 488 | 499 | } | |
| 489 | 500 | ||
| 490 | 501 | func resolveLocation(baseURL, location string) (string, error) { | |
| @@ -147,12 +147,12 @@ func (p *Pusher) UploadBlob(ctx context.Context, baseURL, repo, digest string, s | |||||
| 147 | } | 147 | } | ||
| 148 | } | 148 | } | ||
| 149 | 149 | ||||
| 150 | - | uploadURL, err = p.uploadBlobChunks(ctx, uploadURL, content, auth) | 150 | + | uploadURL, err = p.uploadBlobChunks(ctx, baseURL, uploadURL, content, auth) |
| 151 | if err != nil { | 151 | if err != nil { | ||
| 152 | return err | 152 | return err | ||
| 153 | } | 153 | } | ||
| 154 | 154 | ||||
| 155 | - | return p.finalizeBlobUpload(ctx, uploadURL, digest, auth) | 155 | + | return p.finalizeBlobUpload(ctx, baseURL, uploadURL, digest, auth) |
| 156 | } | 156 | } | ||
| 157 | 157 | ||||
| 158 | // defaultImageSource reads an image from the local Docker/Podman daemon. | 158 | // defaultImageSource reads an image from the local Docker/Podman daemon. | ||
| @@ -283,7 +283,7 @@ func (p *Pusher) uploadManifest(ctx context.Context, baseURL, repo, tag string, | |||||
| 283 | if err != nil { | 283 | if err != nil { | ||
| 284 | return fmt.Errorf("failed to create manifest upload request: %w", err) | 284 | return fmt.Errorf("failed to create manifest upload request: %w", err) | ||
| 285 | } | 285 | } | ||
| 286 | - | setAuthHeader(req, auth) | 286 | + | setScopedAuth(req, auth, baseURL) |
| 287 | req.Header.Set("Content-Type", string(mediaType)) | 287 | req.Header.Set("Content-Type", string(mediaType)) | ||
| 288 | req.ContentLength = int64(len(manifest)) | 288 | req.ContentLength = int64(len(manifest)) | ||
| 289 | 289 | ||||
| @@ -346,7 +346,7 @@ func (p *Pusher) checkBlobExists(ctx context.Context, baseURL, repo, digest, aut | |||||
| 346 | if err != nil { | 346 | if err != nil { | ||
| 347 | return fmt.Errorf("failed to create blob HEAD request: %w", err) | 347 | return fmt.Errorf("failed to create blob HEAD request: %w", err) | ||
| 348 | } | 348 | } | ||
| 349 | - | setAuthHeader(req, auth) | 349 | + | setScopedAuth(req, auth, baseURL) |
| 350 | 350 | ||||
| 351 | resp, err := p.httpClient().Do(req) | 351 | resp, err := p.httpClient().Do(req) | ||
| 352 | if err != nil { | 352 | if err != nil { | ||
| @@ -370,7 +370,7 @@ func (p *Pusher) startBlobUpload(ctx context.Context, baseURL, repo, auth string | |||||
| 370 | if err != nil { | 370 | if err != nil { | ||
| 371 | return "", fmt.Errorf("failed to create blob upload start request: %w", err) | 371 | return "", fmt.Errorf("failed to create blob upload start request: %w", err) | ||
| 372 | } | 372 | } | ||
| 373 | - | setAuthHeader(req, auth) | 373 | + | setScopedAuth(req, auth, baseURL) |
| 374 | 374 | ||||
| 375 | resp, err := p.httpClient().Do(req) | 375 | resp, err := p.httpClient().Do(req) | ||
| 376 | if err != nil { | 376 | if err != nil { | ||
| @@ -391,7 +391,7 @@ func (p *Pusher) startBlobUpload(ctx context.Context, baseURL, repo, auth string | |||||
| 391 | return location, nil | 391 | return location, nil | ||
| 392 | } | 392 | } | ||
| 393 | 393 | ||||
| 394 | - | func (p *Pusher) uploadBlobChunks(ctx context.Context, uploadURL string, content io.Reader, auth string) (string, error) { | 394 | + | func (p *Pusher) uploadBlobChunks(ctx context.Context, baseURL, uploadURL string, content io.Reader, auth string) (string, error) { |
| 395 | buffer := make([]byte, p.chunkSize) | 395 | buffer := make([]byte, p.chunkSize) | ||
| 396 | offset := int64(0) | 396 | offset := int64(0) | ||
| 397 | 397 | ||||
| @@ -404,7 +404,7 @@ func (p *Pusher) uploadBlobChunks(ctx context.Context, uploadURL string, content | |||||
| 404 | return "", fmt.Errorf("failed to read blob chunk: %w", err) | 404 | return "", fmt.Errorf("failed to read blob chunk: %w", err) | ||
| 405 | } | 405 | } | ||
| 406 | 406 | ||||
| 407 | - | nextURL, patchErr := p.uploadBlobChunk(ctx, uploadURL, buffer[:readBytes], offset, auth) | 407 | + | nextURL, patchErr := p.uploadBlobChunk(ctx, baseURL, uploadURL, buffer[:readBytes], offset, auth) |
| 408 | if patchErr != nil { | 408 | if patchErr != nil { | ||
| 409 | return "", patchErr | 409 | return "", patchErr | ||
| 410 | } | 410 | } | ||
| @@ -418,12 +418,12 @@ func (p *Pusher) uploadBlobChunks(ctx context.Context, uploadURL string, content | |||||
| 418 | } | 418 | } | ||
| 419 | } | 419 | } | ||
| 420 | 420 | ||||
| 421 | - | func (p *Pusher) uploadBlobChunk(ctx context.Context, uploadURL string, chunk []byte, offset int64, auth string) (string, error) { | 421 | + | func (p *Pusher) uploadBlobChunk(ctx context.Context, baseURL, uploadURL string, chunk []byte, offset int64, auth string) (string, error) { |
| 422 | req, err := http.NewRequestWithContext(ctx, http.MethodPatch, uploadURL, bytesReader(chunk)) | 422 | req, err := http.NewRequestWithContext(ctx, http.MethodPatch, uploadURL, bytesReader(chunk)) | ||
| 423 | if err != nil { | 423 | if err != nil { | ||
| 424 | return "", fmt.Errorf("failed to create blob chunk request: %w", err) | 424 | return "", fmt.Errorf("failed to create blob chunk request: %w", err) | ||
| 425 | } | 425 | } | ||
| 426 | - | setAuthHeader(req, auth) | 426 | + | setScopedAuth(req, auth, baseURL) |
| 427 | req.Header.Set("Content-Type", "application/octet-stream") | 427 | req.Header.Set("Content-Type", "application/octet-stream") | ||
| 428 | req.Header.Set("Content-Range", fmt.Sprintf("%d-%d", offset, offset+int64(len(chunk))-1)) | 428 | req.Header.Set("Content-Range", fmt.Sprintf("%d-%d", offset, offset+int64(len(chunk))-1)) | ||
| 429 | req.ContentLength = int64(len(chunk)) | 429 | req.ContentLength = int64(len(chunk)) | ||
| @@ -447,7 +447,7 @@ func (p *Pusher) uploadBlobChunk(ctx context.Context, uploadURL string, chunk [] | |||||
| 447 | return nextURL, nil | 447 | return nextURL, nil | ||
| 448 | } | 448 | } | ||
| 449 | 449 | ||||
| 450 | - | func (p *Pusher) finalizeBlobUpload(ctx context.Context, uploadURL, digest, auth string) error { | 450 | + | func (p *Pusher) finalizeBlobUpload(ctx context.Context, baseURL, uploadURL, digest, auth string) error { |
| 451 | parsedURL, err := url.Parse(uploadURL) | 451 | parsedURL, err := url.Parse(uploadURL) | ||
| 452 | if err != nil { | 452 | if err != nil { | ||
| 453 | return fmt.Errorf("failed to parse blob upload location: %w", err) | 453 | return fmt.Errorf("failed to parse blob upload location: %w", err) | ||
| @@ -461,7 +461,7 @@ func (p *Pusher) finalizeBlobUpload(ctx context.Context, uploadURL, digest, auth | |||||
| 461 | if err != nil { | 461 | if err != nil { | ||
| 462 | return fmt.Errorf("failed to create blob finalize request: %w", err) | 462 | return fmt.Errorf("failed to create blob finalize request: %w", err) | ||
| 463 | } | 463 | } | ||
| 464 | - | setAuthHeader(req, auth) | 464 | + | setScopedAuth(req, auth, baseURL) |
| 465 | 465 | ||||
| 466 | resp, err := p.httpClient().Do(req) | 466 | resp, err := p.httpClient().Do(req) | ||
| 467 | if err != nil { | 467 | if err != nil { | ||
| @@ -481,10 +481,21 @@ func (p *Pusher) httpClient() *http.Client { | |||||
| 481 | return &http.Client{Transport: p.transport} | 481 | return &http.Client{Transport: p.transport} | ||
| 482 | } | 482 | } | ||
| 483 | 483 | ||||
| 484 | - | func setAuthHeader(req *http.Request, auth string) { | 484 | + | // setScopedAuth attaches the auth header only if the request URL matches the |
| 485 | - | if auth != "" { | 485 | + | // registry origin (scheme + host). This prevents credential leakage if a |
| 486 | - | req.Header.Set("Authorization", auth) | 486 | + | // redirect somehow escapes origin pinning. |
| 487 | + | func setScopedAuth(req *http.Request, auth, registryOrigin string) { | |||
| 488 | + | if auth == "" { | |||
| 489 | + | return | |||
| 487 | } | 490 | } | ||
| 491 | + | origin, err := url.Parse(registryOrigin) | |||
| 492 | + | if err != nil { | |||
| 493 | + | return | |||
| 494 | + | } | |||
| 495 | + | if req.URL.Scheme != origin.Scheme || req.URL.Host != origin.Host { | |||
| 496 | + | return | |||
| 497 | + | } | |||
| 498 | + | req.Header.Set("Authorization", auth) | |||
| 488 | } | 499 | } | ||
| 489 | 500 | ||||
| 490 | func resolveLocation(baseURL, location string) (string, error) { | 501 | func resolveLocation(baseURL, location string) (string, error) { | ||
M pkg/registrypush/push_test.go +61 -0
| @@ -284,6 +284,67 @@ func TestUploadBlob_RejectsCrossOriginRedirect(t *testing.T) { | |||
| 284 | 284 | assert.Contains(t, err.Error(), "cross-origin") | |
| 285 | 285 | } | |
| 286 | 286 | ||
| 287 | + | // authRecordingTransport records Authorization headers per request URL. | |
| 288 | + | type authRecordingTransport struct { | |
| 289 | + | inner http.RoundTripper | |
| 290 | + | records []authRecord | |
| 291 | + | } | |
| 292 | + | ||
| 293 | + | type authRecord struct { | |
| 294 | + | URL string | |
| 295 | + | Auth string | |
| 296 | + | } | |
| 297 | + | ||
| 298 | + | func (t *authRecordingTransport) RoundTrip(req *http.Request) (*http.Response, error) { | |
| 299 | + | t.records = append(t.records, authRecord{ | |
| 300 | + | URL: req.URL.String(), | |
| 301 | + | Auth: req.Header.Get("Authorization"), | |
| 302 | + | }) | |
| 303 | + | return t.inner.RoundTrip(req) | |
| 304 | + | } | |
| 305 | + | ||
| 306 | + | func TestUploadBlob_AuthHeaderOnlySentToSameOrigin(t *testing.T) { | |
| 307 | + | registry := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 308 | + | switch { | |
| 309 | + | case r.Method == http.MethodHead: | |
| 310 | + | w.WriteHeader(http.StatusNotFound) | |
| 311 | + | case r.Method == http.MethodPost: | |
| 312 | + | w.Header().Set("Location", "/v2/repo/blobs/uploads/abc?chunk=0") | |
| 313 | + | w.WriteHeader(http.StatusAccepted) | |
| 314 | + | case r.Method == http.MethodPatch: | |
| 315 | + | w.Header().Set("Location", "/v2/repo/blobs/uploads/abc?chunk=1") | |
| 316 | + | w.WriteHeader(http.StatusAccepted) | |
| 317 | + | case r.Method == http.MethodPut: | |
| 318 | + | w.WriteHeader(http.StatusCreated) | |
| 319 | + | default: | |
| 320 | + | w.WriteHeader(http.StatusInternalServerError) | |
| 321 | + | } | |
| 322 | + | })) | |
| 323 | + | defer registry.Close() | |
| 324 | + | ||
| 325 | + | recorder := &authRecordingTransport{inner: http.DefaultTransport} | |
| 326 | + | p := registrypush.New( | |
| 327 | + | registrypush.WithChunkSize(1024), | |
| 328 | + | registrypush.WithTransport(recorder), | |
| 329 | + | ) | |
| 330 | + | ctx := context.Background() | |
| 331 | + | secret := "Basic dXNlcjpwYXNz" | |
| 332 | + | err := p.UploadBlob(ctx, registry.URL, "test/repo", "sha256:abc", 5, strings.NewReader("hello"), secret) | |
| 333 | + | ||
| 334 | + | require.NoError(t, err) | |
| 335 | + | require.NotEmpty(t, recorder.records) | |
| 336 | + | ||
| 337 | + | registryURL, _ := url.Parse(registry.URL) | |
| 338 | + | for _, rec := range recorder.records { | |
| 339 | + | reqURL, _ := url.Parse(rec.URL) | |
| 340 | + | if reqURL.Host == registryURL.Host { | |
| 341 | + | assert.Equal(t, secret, rec.Auth, "same-origin request to %s should have auth", rec.URL) | |
| 342 | + | } else { | |
| 343 | + | assert.Empty(t, rec.Auth, "cross-origin request to %s should NOT have auth", rec.URL) | |
| 344 | + | } | |
| 345 | + | } | |
| 346 | + | } | |
| 347 | + | ||
| 287 | 348 | type fakeBlobUploadRegistry struct { | |
| 288 | 349 | t *testing.T | |
| 289 | 350 | expectedAuth string | |
| @@ -284,6 +284,67 @@ func TestUploadBlob_RejectsCrossOriginRedirect(t *testing.T) { | |||||
| 284 | assert.Contains(t, err.Error(), "cross-origin") | 284 | assert.Contains(t, err.Error(), "cross-origin") | ||
| 285 | } | 285 | } | ||
| 286 | 286 | ||||
| 287 | + | // authRecordingTransport records Authorization headers per request URL. | |||
| 288 | + | type authRecordingTransport struct { | |||
| 289 | + | inner http.RoundTripper | |||
| 290 | + | records []authRecord | |||
| 291 | + | } | |||
| 292 | + | ||||
| 293 | + | type authRecord struct { | |||
| 294 | + | URL string | |||
| 295 | + | Auth string | |||
| 296 | + | } | |||
| 297 | + | ||||
| 298 | + | func (t *authRecordingTransport) RoundTrip(req *http.Request) (*http.Response, error) { | |||
| 299 | + | t.records = append(t.records, authRecord{ | |||
| 300 | + | URL: req.URL.String(), | |||
| 301 | + | Auth: req.Header.Get("Authorization"), | |||
| 302 | + | }) | |||
| 303 | + | return t.inner.RoundTrip(req) | |||
| 304 | + | } | |||
| 305 | + | ||||
| 306 | + | func TestUploadBlob_AuthHeaderOnlySentToSameOrigin(t *testing.T) { | |||
| 307 | + | registry := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
| 308 | + | switch { | |||
| 309 | + | case r.Method == http.MethodHead: | |||
| 310 | + | w.WriteHeader(http.StatusNotFound) | |||
| 311 | + | case r.Method == http.MethodPost: | |||
| 312 | + | w.Header().Set("Location", "/v2/repo/blobs/uploads/abc?chunk=0") | |||
| 313 | + | w.WriteHeader(http.StatusAccepted) | |||
| 314 | + | case r.Method == http.MethodPatch: | |||
| 315 | + | w.Header().Set("Location", "/v2/repo/blobs/uploads/abc?chunk=1") | |||
| 316 | + | w.WriteHeader(http.StatusAccepted) | |||
| 317 | + | case r.Method == http.MethodPut: | |||
| 318 | + | w.WriteHeader(http.StatusCreated) | |||
| 319 | + | default: | |||
| 320 | + | w.WriteHeader(http.StatusInternalServerError) | |||
| 321 | + | } | |||
| 322 | + | })) | |||
| 323 | + | defer registry.Close() | |||
| 324 | + | ||||
| 325 | + | recorder := &authRecordingTransport{inner: http.DefaultTransport} | |||
| 326 | + | p := registrypush.New( | |||
| 327 | + | registrypush.WithChunkSize(1024), | |||
| 328 | + | registrypush.WithTransport(recorder), | |||
| 329 | + | ) | |||
| 330 | + | ctx := context.Background() | |||
| 331 | + | secret := "Basic dXNlcjpwYXNz" | |||
| 332 | + | err := p.UploadBlob(ctx, registry.URL, "test/repo", "sha256:abc", 5, strings.NewReader("hello"), secret) | |||
| 333 | + | ||||
| 334 | + | require.NoError(t, err) | |||
| 335 | + | require.NotEmpty(t, recorder.records) | |||
| 336 | + | ||||
| 337 | + | registryURL, _ := url.Parse(registry.URL) | |||
| 338 | + | for _, rec := range recorder.records { | |||
| 339 | + | reqURL, _ := url.Parse(rec.URL) | |||
| 340 | + | if reqURL.Host == registryURL.Host { | |||
| 341 | + | assert.Equal(t, secret, rec.Auth, "same-origin request to %s should have auth", rec.URL) | |||
| 342 | + | } else { | |||
| 343 | + | assert.Empty(t, rec.Auth, "cross-origin request to %s should NOT have auth", rec.URL) | |||
| 344 | + | } | |||
| 345 | + | } | |||
| 346 | + | } | |||
| 347 | + | ||||
| 287 | type fakeBlobUploadRegistry struct { | 348 | type fakeBlobUploadRegistry struct { | ||
| 288 | t *testing.T | 349 | t *testing.T | ||
| 289 | expectedAuth string | 350 | expectedAuth string | ||