Skip to content

Commit 368d1a5

Browse files
committed
git-clone: Set up git creds
Supports two formats: 1. .git-credentials and .gitconfig files (copied directly) 2. username and password files (kubernetes.io/basic-auth secret format) Assisted-by: Claude Opus 4.5 Signed-off-by: Tomáš Nevrlka <tnevrlka@redhat.com>
1 parent 81dfc2c commit 368d1a5

File tree

2 files changed

+187
-11
lines changed

2 files changed

+187
-11
lines changed

pkg/commands/git_clone/git_clone.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ func (c *GitClone) Run() error {
7777
return err
7878
}
7979

80+
// Setup authentication
81+
if err := c.setupBasicAuth(); err != nil {
82+
return err
83+
}
84+
8085
// Clean checkout directory if requested
8186
if c.Params.DeleteExisting {
8287
if err := c.cleanCheckoutDir(); err != nil {

pkg/commands/git_clone/setup.go

Lines changed: 182 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ package git_clone
22

33
import (
44
"fmt"
5+
"io"
6+
"net/url"
57
"os"
68
"path/filepath"
9+
"strings"
710

811
l "github.com/konflux-ci/konflux-build-cli/pkg/logger"
912
)
@@ -45,24 +48,192 @@ func (c *GitClone) cleanCheckoutDir() error {
4548
}
4649

4750
// setupGitConfig configures git settings for SSL verification and CA bundle.
51+
// Uses environment variables instead of git config to avoid modifying global git state.
4852
func (c *GitClone) setupGitConfig() error {
4953
if !c.Params.SSLVerify {
50-
l.Logger.Info("Disabling SSL verification (http.sslVerify=false)")
51-
if err := c.CliWrappers.GitCli.ConfigLocal("", "http.sslVerify", "false"); err != nil {
52-
return fmt.Errorf("failed to configure http.sslVerify: %w", err)
54+
l.Logger.Info("Disabling SSL verification (GIT_SSL_NO_VERIFY=true)")
55+
if err := os.Setenv("GIT_SSL_NO_VERIFY", "true"); err != nil {
56+
return err
5357
}
5458
}
5559

56-
caBundlePath := c.Params.CaBundlePath
57-
if caBundlePath == "" {
58-
caBundlePath = "/mnt/trusted-ca/ca-bundle.crt"
59-
}
60-
if _, err := os.Stat(caBundlePath); err == nil {
61-
l.Logger.Infof("Using mounted CA bundle: %s", caBundlePath)
62-
if err := c.CliWrappers.GitCli.ConfigLocal("", "http.sslCAInfo", caBundlePath); err != nil {
63-
return fmt.Errorf("failed to configure http.sslCAInfo: %w", err)
60+
if c.Params.CaBundlePath != "" {
61+
if _, err := os.Stat(c.Params.CaBundlePath); err == nil {
62+
l.Logger.Infof("Using CA bundle: %s", c.Params.CaBundlePath)
63+
if err := os.Setenv("GIT_SSL_CAINFO", c.Params.CaBundlePath); err != nil {
64+
return err
65+
}
66+
} else {
67+
l.Logger.Warnf("CA bundle path specified but not found: %s", c.Params.CaBundlePath)
6468
}
6569
}
6670

6771
return nil
6872
}
73+
74+
// setupBasicAuth sets up git credentials from a basic-auth workspace.
75+
// Supports two formats:
76+
// 1. .git-credentials and .gitconfig files (copied directly)
77+
// 2. username and password files (kubernetes.io/basic-auth secret format)
78+
func (c *GitClone) setupBasicAuth() error {
79+
if c.Params.BasicAuthDirectory == "" {
80+
return nil
81+
}
82+
83+
authDir := c.Params.BasicAuthDirectory
84+
85+
if _, err := os.Stat(authDir); os.IsNotExist(err) {
86+
l.Logger.Infof("Basic auth directory not found: %s", authDir)
87+
return nil
88+
}
89+
90+
gitCredentialsPath := filepath.Join(authDir, ".git-credentials")
91+
gitConfigPath := filepath.Join(authDir, ".gitconfig")
92+
usernamePath := filepath.Join(authDir, "username")
93+
passwordPath := filepath.Join(authDir, "password")
94+
95+
destCredentials := filepath.Join(c.internalDir, ".git-credentials")
96+
destConfig := filepath.Join(c.internalDir, ".gitconfig")
97+
98+
// Format 1: .git-credentials and .gitconfig files
99+
if fileExists(gitCredentialsPath) && fileExists(gitConfigPath) {
100+
l.Logger.Info("Setting up basic auth from .git-credentials and .gitconfig")
101+
102+
if err := copyFile(gitCredentialsPath, destCredentials, 0400); err != nil {
103+
return fmt.Errorf("failed to copy .git-credentials: %w", err)
104+
}
105+
106+
configContent, err := readFileWithLimit(gitConfigPath, maxAuthFileSize)
107+
if err != nil {
108+
return fmt.Errorf("failed to read .gitconfig: %w", err)
109+
}
110+
rewritten := rewriteGitConfigCredentialHelper(string(configContent), destCredentials)
111+
if err := os.WriteFile(destConfig, []byte(rewritten), 0400); err != nil {
112+
return fmt.Errorf("failed to write .gitconfig: %w", err)
113+
}
114+
115+
if err := os.Setenv("GIT_CONFIG_GLOBAL", destConfig); err != nil {
116+
return err
117+
}
118+
119+
l.Logger.Info("Basic auth credentials configured")
120+
return nil
121+
}
122+
123+
// Format 2: kubernetes.io/basic-auth secret (username and password files)
124+
if fileExists(usernamePath) && fileExists(passwordPath) {
125+
l.Logger.Info("Setting up basic auth from username/password files")
126+
127+
username, err := readFileWithLimit(usernamePath, maxAuthFileSize)
128+
if err != nil {
129+
return fmt.Errorf("failed to read username file: %w", err)
130+
}
131+
132+
password, err := readFileWithLimit(passwordPath, maxAuthFileSize)
133+
if err != nil {
134+
return fmt.Errorf("failed to read password file: %w", err)
135+
}
136+
137+
parsedURL, err := url.Parse(c.Params.URL)
138+
if err != nil {
139+
return fmt.Errorf("failed to parse repository URL: %w", err)
140+
}
141+
hostname := parsedURL.Host
142+
143+
credentialsContent := fmt.Sprintf("https://%s:%s@%s\n",
144+
url.PathEscape(strings.TrimSpace(string(username))),
145+
url.PathEscape(strings.TrimSpace(string(password))),
146+
hostname)
147+
148+
if err := os.WriteFile(destCredentials, []byte(credentialsContent), 0400); err != nil {
149+
return fmt.Errorf("failed to write .git-credentials: %w", err)
150+
}
151+
152+
gitConfigContent := fmt.Sprintf("[credential \"https://%s\"]\n helper = store --file=%s\n", hostname, destCredentials)
153+
if err := os.WriteFile(destConfig, []byte(gitConfigContent), 0400); err != nil {
154+
return fmt.Errorf("failed to write .gitconfig: %w", err)
155+
}
156+
157+
if err := os.Setenv("GIT_CONFIG_GLOBAL", destConfig); err != nil {
158+
return err
159+
}
160+
161+
l.Logger.Infof("Basic auth credentials configured for %s", hostname)
162+
return nil
163+
}
164+
165+
return fmt.Errorf("unknown basic-auth workspace format: expected .git-credentials/.gitconfig or username/password files")
166+
}
167+
168+
// rewriteGitConfigCredentialHelper rewrites "helper = store" lines in a git config
169+
// to include an explicit --file flag pointing to the given credentials path.
170+
func rewriteGitConfigCredentialHelper(configContent, credentialsPath string) string {
171+
lines := strings.Split(configContent, "\n")
172+
for i, line := range lines {
173+
trimmed := strings.TrimSpace(line)
174+
if trimmed == "helper = store" {
175+
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
176+
lines[i] = fmt.Sprintf("%shelper = store --file=%s", indent, credentialsPath)
177+
}
178+
}
179+
return strings.Join(lines, "\n")
180+
}
181+
182+
// fileExists checks if a file exists and is not a directory.
183+
func fileExists(path string) bool {
184+
info, err := os.Stat(path)
185+
if os.IsNotExist(err) {
186+
return false
187+
}
188+
return err == nil && !info.IsDir()
189+
}
190+
191+
// maxAuthFileSize is the maximum allowed size for auth-related files
192+
// (.gitconfig, .git-credentials, SSH keys, username, password).
193+
const maxAuthFileSize = 1 << 20 // 1MB
194+
195+
// readFileWithLimit reads a file, rejecting files larger than maxSize.
196+
// Uses file-descriptor-based stat and limited read to avoid TOCTOU races.
197+
func readFileWithLimit(path string, maxSize int64) (data []byte, err error) {
198+
f, err := os.Open(path)
199+
if err != nil {
200+
return nil, err
201+
}
202+
defer func() {
203+
if cerr := f.Close(); cerr != nil && err == nil {
204+
err = cerr
205+
}
206+
}()
207+
208+
info, err := f.Stat()
209+
if err != nil {
210+
return nil, err
211+
}
212+
if info.Size() > maxSize {
213+
return nil, fmt.Errorf("authentication file exceeds maximum allowed size (%d bytes)", maxSize)
214+
}
215+
216+
// Use LimitReader to enforce the size cap during read, even if the file
217+
// was extended between Stat and Read (belt-and-suspenders).
218+
data, err = io.ReadAll(io.LimitReader(f, maxSize+1))
219+
if err != nil {
220+
return nil, err
221+
}
222+
if int64(len(data)) > maxSize {
223+
return nil, fmt.Errorf("authentication file exceeds maximum allowed size (%d bytes)", maxSize)
224+
}
225+
return data, nil
226+
}
227+
228+
// copyFile copies a file from src to dest with the specified permissions.
229+
func copyFile(src, dest string, perm os.FileMode) error {
230+
if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
231+
return err
232+
}
233+
234+
data, err := readFileWithLimit(src, maxAuthFileSize)
235+
if err != nil {
236+
return err
237+
}
238+
return os.WriteFile(dest, data, perm)
239+
}

0 commit comments

Comments
 (0)