@@ -390,6 +390,63 @@ func TestProxyEgressAllowlistEnforceAllowsPrivateRegistryRedirectTarget(t *testi
390390 assert .Equal (t , http .StatusForbidden , status , "other unlisted hosts stay blocked" )
391391}
392392
393+ // The git server handler retries an auth failure with an alternate credential
394+ // and replaces the response. A redirect returned by that retry must still
395+ // allowlist its target.
396+ func TestProxyEgressAllowlistLearnsRedirectFromAuthRetry (t * testing.T ) {
397+ t .Setenv ("PROXY_CACHE" , "false" )
398+ const bucketURL = "https://bucket.example.com/archive.tar.gz?X-Amz-Signature=abc"
399+ var rejected atomic.Int32
400+ upstream := httptest .NewTLSServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
401+ switch r .Host {
402+ case "git.example.com" :
403+ if _ , password , _ := r .BasicAuth (); password != "good" {
404+ rejected .Add (1 )
405+ w .WriteHeader (http .StatusUnauthorized )
406+ return
407+ }
408+ http .Redirect (w , r , bucketURL , http .StatusFound )
409+ case "bucket.example.com" :
410+ _ , _ = io .WriteString (w , "archive" )
411+ default :
412+ w .WriteHeader (http .StatusTeapot )
413+ }
414+ }))
415+ defer upstream .Close ()
416+
417+ cfg := & config.Config {
418+ CA : testProxyConfig .CA ,
419+ Experiments : config.Experiments {"proxy-egress-enforce" : true },
420+ Credentials : config.Credentials {
421+ {"type" : "git_source" , "host" : "git.example.com" , "username" : "x-access-token" , "password" : "bad" },
422+ {"type" : "git_source" , "host" : "git.example.com" , "username" : "x-access-token" , "password" : "good" },
423+ },
424+ }
425+ client , proxy := testProxyServer (t , cfg , nil , upstream .Certificate ())
426+ closeOnCleanup (t , proxy )
427+ t .Cleanup (client .CloseIdleConnections )
428+ proxyHandler := proxy .Handler .(* Proxy )
429+ t .Cleanup (proxyHandler .Tr .CloseIdleConnections )
430+ proxyHandler .Tr .Proxy = nil
431+ proxyHandler .Tr .DialContext = func (ctx context.Context , network , address string ) (net.Conn , error ) {
432+ return (& net.Dialer {}).DialContext (ctx , network , upstream .Listener .Addr ().String ())
433+ }
434+
435+ req , err := http .NewRequestWithContext (t .Context (), http .MethodGet , "https://git.example.com/org/repo/archive/v1.tar.gz" , nil )
436+ require .NoError (t , err )
437+ rsp , err := client .Do (req )
438+ require .NoError (t , err )
439+ defer func () {
440+ require .NoError (t , rsp .Body .Close ())
441+ }()
442+ body , err := io .ReadAll (rsp .Body )
443+ require .NoError (t , err )
444+
445+ require .Positive (t , rejected .Load (), "the first credential must fail so the retry path is exercised" )
446+ assert .Equal (t , http .StatusOK , rsp .StatusCode )
447+ assert .Equal (t , "archive" , string (body ))
448+ }
449+
393450func TestProxyHTTPSMITMFixedLengthResponseFraming (t * testing.T ) {
394451 upstream := httptest .NewTLSServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
395452 w .Header ().Set ("Content-Length" , "5" )
0 commit comments