-
Notifications
You must be signed in to change notification settings - Fork 17.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
cmd/go: add built in git mode for GOAUTH
This CL adds support for git as a valid GOAUTH command. Improves on implementation in cmd/auth/gitauth/gitauth.go This follows the proposed de# https://golang.org/issues/26232#issuecomment-461525141 For #26232 Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-longtest,gotip-windows-amd64-longtest Change-Id: I07810d9dc895d59e5db4bfa50cd42cb909208f93 Reviewed-on: https://go-review.googlesource.com/c/go/+/605275 Reviewed-by: Damien Neil <dneil@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Michael Matloob <matloob@golang.org> Reviewed-by: Alan Donovan <adonovan@google.com>
- Loading branch information
1 parent
3b94c35
commit 635c2dc
Showing
7 changed files
with
404 additions
and
27 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
// Copyright 2019 The Go Authors. All rights reserved. | ||
// Use of this source code is governed by a BSD-style | ||
// license that can be found in the LICENSE file. | ||
|
||
// gitauth uses 'git credential' to implement the GOAUTH protocol. | ||
// | ||
// See https://git-scm.com/docs/gitcredentials or run 'man gitcredentials' for | ||
// information on how to configure 'git credential'. | ||
package auth | ||
|
||
import ( | ||
"bytes" | ||
"cmd/go/internal/base" | ||
"cmd/go/internal/cfg" | ||
"cmd/go/internal/web/intercept" | ||
"fmt" | ||
"log" | ||
"net/http" | ||
"net/url" | ||
"os/exec" | ||
"strings" | ||
) | ||
|
||
const maxTries = 3 | ||
|
||
// runGitAuth retrieves credentials for the given prefix using | ||
// 'git credential fill', validates them with a HEAD request | ||
// (using the provided client) and updates the credential helper's cache. | ||
// It returns the matching credential prefix, the http.Header with the | ||
// Basic Authentication header set, or an error. | ||
// The caller must not mutate the header. | ||
func runGitAuth(client *http.Client, dir, prefix string) (string, http.Header, error) { | ||
if prefix == "" { | ||
// No explicit prefix was passed, but 'git credential' | ||
// provides no way to enumerate existing credentials. | ||
// Wait for a request for a specific prefix. | ||
return "", nil, fmt.Errorf("no explicit prefix was passed") | ||
} | ||
if dir == "" { | ||
// Prevent config-injection attacks by requiring an explicit working directory. | ||
// See https://golang.org/issue/29230 for details. | ||
panic("'git' invoked in an arbitrary directory") // this should be caught earlier. | ||
} | ||
cmd := exec.Command("git", "credential", "fill") | ||
cmd.Dir = dir | ||
cmd.Stdin = strings.NewReader(fmt.Sprintf("url=%s\n", prefix)) | ||
out, err := cmd.CombinedOutput() | ||
if err != nil { | ||
return "", nil, fmt.Errorf("'git credential fill' failed (url=%s): %w\n%s", prefix, err, out) | ||
} | ||
parsedPrefix, username, password := parseGitAuth(out) | ||
if parsedPrefix == "" { | ||
return "", nil, fmt.Errorf("'git credential fill' failed for url=%s, could not parse url\n", prefix) | ||
} | ||
// Check that the URL Git gave us is a prefix of the one we requested. | ||
if !strings.HasPrefix(prefix, parsedPrefix) { | ||
return "", nil, fmt.Errorf("requested a credential for %s, but 'git credential fill' provided one for %s\n", prefix, parsedPrefix) | ||
} | ||
req, err := http.NewRequest("HEAD", parsedPrefix, nil) | ||
if err != nil { | ||
return "", nil, fmt.Errorf("internal error constructing HTTP HEAD request: %v\n", err) | ||
} | ||
req.SetBasicAuth(username, password) | ||
// Asynchronously validate the provided credentials using a HEAD request, | ||
// allowing the git credential helper to update its cache without blocking. | ||
// This avoids repeatedly prompting the user for valid credentials. | ||
// This is a best-effort update; the primary validation will still occur | ||
// with the caller's client. | ||
// The request is intercepted for testing purposes to simulate interactions | ||
// with the credential helper. | ||
intercept.Request(req) | ||
go updateCredentialHelper(client, req, out) | ||
|
||
// Return the parsed prefix and headers, even if credential validation fails. | ||
// The caller is responsible for the primary validation. | ||
return parsedPrefix, req.Header, nil | ||
} | ||
|
||
// parseGitAuth parses the output of 'git credential fill', extracting | ||
// the URL prefix, user, and password. | ||
// Any of these values may be empty if parsing fails. | ||
func parseGitAuth(data []byte) (parsedPrefix, username, password string) { | ||
prefix := new(url.URL) | ||
for _, line := range strings.Split(string(data), "\n") { | ||
key, value, ok := strings.Cut(strings.TrimSpace(line), "=") | ||
if !ok { | ||
continue | ||
} | ||
switch key { | ||
case "protocol": | ||
prefix.Scheme = value | ||
case "host": | ||
prefix.Host = value | ||
case "path": | ||
prefix.Path = value | ||
case "username": | ||
username = value | ||
case "password": | ||
password = value | ||
case "url": | ||
// Write to a local variable instead of updating prefix directly: | ||
// if the url field is malformed, we don't want to invalidate | ||
// information parsed from the protocol, host, and path fields. | ||
u, err := url.ParseRequestURI(value) | ||
if err != nil { | ||
if cfg.BuildX { | ||
log.Printf("malformed URL from 'git credential fill' (%v): %q\n", err, value) | ||
// Proceed anyway: we might be able to parse the prefix from other fields of the response. | ||
} | ||
continue | ||
} | ||
prefix = u | ||
} | ||
} | ||
return prefix.String(), username, password | ||
} | ||
|
||
// updateCredentialHelper validates the given credentials by sending a HEAD request | ||
// and updates the git credential helper's cache accordingly. It retries the | ||
// request up to maxTries times. | ||
func updateCredentialHelper(client *http.Client, req *http.Request, credentialOutput []byte) { | ||
for range maxTries { | ||
release, err := base.AcquireNet() | ||
if err != nil { | ||
return | ||
} | ||
res, err := client.Do(req) | ||
if err != nil { | ||
release() | ||
continue | ||
} | ||
res.Body.Close() | ||
release() | ||
if res.StatusCode == http.StatusOK || res.StatusCode == http.StatusUnauthorized { | ||
approveOrRejectCredential(credentialOutput, res.StatusCode == http.StatusOK) | ||
break | ||
} | ||
} | ||
} | ||
|
||
// approveOrRejectCredential approves or rejects the provided credential using | ||
// 'git credential approve/reject'. | ||
func approveOrRejectCredential(credentialOutput []byte, approve bool) { | ||
action := "reject" | ||
if approve { | ||
action = "approve" | ||
} | ||
cmd := exec.Command("git", "credential", action) | ||
cmd.Stdin = bytes.NewReader(credentialOutput) | ||
cmd.Run() // ignore error | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
package auth | ||
|
||
import ( | ||
"testing" | ||
) | ||
|
||
func TestParseGitAuth(t *testing.T) { | ||
testCases := []struct { | ||
gitauth string // contents of 'git credential fill' | ||
wantPrefix string | ||
wantUsername string | ||
wantPassword string | ||
}{ | ||
{ // Standard case. | ||
gitauth: ` | ||
protocol=https | ||
host=example.com | ||
username=bob | ||
password=secr3t | ||
`, | ||
wantPrefix: "https://example.com", | ||
wantUsername: "bob", | ||
wantPassword: "secr3t", | ||
}, | ||
{ // Should not use an invalid url. | ||
gitauth: ` | ||
protocol=https | ||
host=example.com | ||
username=bob | ||
password=secr3t | ||
url=invalid | ||
`, | ||
wantPrefix: "https://example.com", | ||
wantUsername: "bob", | ||
wantPassword: "secr3t", | ||
}, | ||
{ // Should use the new url. | ||
gitauth: ` | ||
protocol=https | ||
host=example.com | ||
username=bob | ||
password=secr3t | ||
url=https://go.dev | ||
`, | ||
wantPrefix: "https://go.dev", | ||
wantUsername: "bob", | ||
wantPassword: "secr3t", | ||
}, | ||
{ // Empty data. | ||
gitauth: ` | ||
`, | ||
wantPrefix: "", | ||
wantUsername: "", | ||
wantPassword: "", | ||
}, | ||
{ // Does not follow the '=' format. | ||
gitauth: ` | ||
protocol:https | ||
host:example.com | ||
username:bob | ||
password:secr3t | ||
`, | ||
wantPrefix: "", | ||
wantUsername: "", | ||
wantPassword: "", | ||
}, | ||
} | ||
for _, tc := range testCases { | ||
parsedPrefix, username, password := parseGitAuth([]byte(tc.gitauth)) | ||
if parsedPrefix != tc.wantPrefix { | ||
t.Errorf("parseGitAuth(%s):\nhave %q\nwant %q", tc.gitauth, parsedPrefix, tc.wantPrefix) | ||
} | ||
if username != tc.wantUsername { | ||
t.Errorf("parseGitAuth(%s):\nhave %q\nwant %q", tc.gitauth, username, tc.wantUsername) | ||
} | ||
if password != tc.wantPassword { | ||
t.Errorf("parseGitAuth(%s):\nhave %q\nwant %q", tc.gitauth, password, tc.wantPassword) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.