@@ -5,11 +5,13 @@ import (
55 "crypto/ed25519"
66 "encoding/base64"
77 "io"
8+ "net/http"
89 "net/http/httptest"
910 "net/url"
1011 "os"
1112 "path/filepath"
1213 "strings"
14+ "sync"
1315 "testing"
1416
1517 "github.com/coder/envbuilder/git"
@@ -1033,3 +1035,111 @@ func writeTestPrivateKey(t *testing.T) string {
10331035func base64EncodeTestPrivateKey () string {
10341036 return base64 .StdEncoding .EncodeToString ([]byte (testKey ))
10351037}
1038+
1039+ // TestCloneRepoSubmoduleHostAuth verifies that parent repository credentials
1040+ // are forwarded to a submodule only when the submodule resides on the same
1041+ // host as the parent. The submodule server records every request that arrived
1042+ // with an Authorization header.
1043+ func TestCloneRepoSubmoduleHostAuth (t * testing.T ) {
1044+ t .Parallel ()
1045+
1046+ const (
1047+ parentUser = "parent-user"
1048+ parentPass = "parent-pass"
1049+ )
1050+
1051+ cases := []struct {
1052+ name string
1053+ rewriteSubURL func (string ) string
1054+ wantAuthSeen bool
1055+ }{
1056+ {
1057+ name : "SameHostForwardsAuth" ,
1058+ rewriteSubURL : func (u string ) string { return u },
1059+ wantAuthSeen : true ,
1060+ },
1061+ {
1062+ name : "CrossHostWithholdsAuth" ,
1063+ rewriteSubURL : func (u string ) string {
1064+ // 127.0.0.1 and localhost both resolve to the loopback
1065+ // interface, so the submodule server is still reachable,
1066+ // but SameHost compares the hostname strings and reports
1067+ // a mismatch.
1068+ return strings .Replace (u , "127.0.0.1" , "localhost" , 1 )
1069+ },
1070+ wantAuthSeen : false ,
1071+ },
1072+ }
1073+
1074+ for _ , tc := range cases {
1075+ tc := tc
1076+ t .Run (tc .name , func (t * testing.T ) {
1077+ t .Parallel ()
1078+
1079+ // Build the submodule repository in memory.
1080+ subFS := memfs .New ()
1081+ subRepo := gittest .NewRepo (t , subFS ,
1082+ gittest .Commit (t , "subfile.txt" , "submodule content" , "submodule init" ),
1083+ )
1084+ subHead , err := subRepo .Head ()
1085+ require .NoError (t , err )
1086+ subHash := subHead .Hash ()
1087+
1088+ // Wrap the submodule server with an observer that records any
1089+ // request that carried an Authorization header. The submodule
1090+ // server itself does not require auth; we only care about whether
1091+ // the client offered the parent credentials.
1092+ var (
1093+ authMu sync.Mutex
1094+ authSeen bool
1095+ )
1096+ observer := func (next http.Handler ) http.Handler {
1097+ return http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
1098+ if r .Header .Get ("Authorization" ) != "" {
1099+ authMu .Lock ()
1100+ authSeen = true
1101+ authMu .Unlock ()
1102+ }
1103+ next .ServeHTTP (w , r )
1104+ })
1105+ }
1106+ subSrv := httptest .NewServer (observer (gittest .NewServer (subFS )))
1107+ t .Cleanup (subSrv .Close )
1108+
1109+ // Build the parent repository pointing to the submodule. The
1110+ // .gitmodules URL may be munged to use a different hostname,
1111+ // which is what the cross-host case exercises.
1112+ subURLInGitmodules := tc .rewriteSubURL (subSrv .URL )
1113+ parentFS := memfs .New ()
1114+ _ = gittest .NewRepo (t , parentFS ,
1115+ gittest .Commit (t , "README.md" , "parent readme" , "parent init" ),
1116+ gittest .CommitSubmodule (t , "submod" , subURLInGitmodules , subHash ),
1117+ )
1118+ parentSrv := httptest .NewServer (
1119+ mwtest .BasicAuthMW (parentUser , parentPass )(gittest .NewServer (parentFS )),
1120+ )
1121+ t .Cleanup (parentSrv .Close )
1122+
1123+ // Clone the parent with credentials and submodule recursion enabled.
1124+ clientFS := memfs .New ()
1125+ _ , err = git .CloneRepo (context .Background (), t .Logf , git.CloneRepoOptions {
1126+ Path : "/workspace" ,
1127+ RepoURL : parentSrv .URL ,
1128+ Storage : clientFS ,
1129+ RepoAuth : & githttp.BasicAuth {
1130+ Username : parentUser ,
1131+ Password : parentPass ,
1132+ },
1133+ SubmoduleDepth : 5 ,
1134+ })
1135+ require .NoError (t , err )
1136+
1137+ authMu .Lock ()
1138+ got := authSeen
1139+ authMu .Unlock ()
1140+ require .Equal (t , tc .wantAuthSeen , got ,
1141+ "submodule server saw Authorization header (cross-host should withhold, same-host should forward)" )
1142+ })
1143+ }
1144+ }
1145+
0 commit comments