Pass authentication types to Git credential helpers
Git recently added a new field to the credential helper, `wwwauth[]`, which may be repeated and includes all of the `WWW-Authenticate` headers so that the credential helper may choose an appropriate set of credentials and extract any sort of necessary data from the field (such as challenge). In Git LFS, we also want to do this with the `LFS-Authenticate` headers as well, since those are often used for the same purpose, so include both these headers in that field when passing them to `git credential fill`. Note that `git credential fill` only honours this value and passes it to the credential helper in Git 2.41 and newer (including the latest `HEAD`). However, just to be safe, let's add an undocumented and experimental option (`credential.*.skipwwwauth`) that users can use to control this, which we can remove in a few releases if it turns out it's not needed. Similarly, skip our new tests if we have an older version of Git where this doesn't work, since they'll otherwise fail.
This commit is contained in:
parent
5d5f90e286
commit
badde878a3
@ -74,7 +74,8 @@ type CredentialHelperContext struct {
|
||||
askpassCredHelper *AskPassCredentialHelper
|
||||
cachingCredHelper *credentialCacher
|
||||
|
||||
urlConfig *config.URLConfig
|
||||
urlConfig *config.URLConfig
|
||||
wwwAuthHeaders []string
|
||||
}
|
||||
|
||||
func NewCredentialHelperContext(gitEnv config.Environment, osEnv config.Environment) *CredentialHelperContext {
|
||||
@ -113,6 +114,10 @@ func NewCredentialHelperContext(gitEnv config.Environment, osEnv config.Environm
|
||||
return c
|
||||
}
|
||||
|
||||
func (ctxt *CredentialHelperContext) SetWWWAuthHeaders(headers []string) {
|
||||
ctxt.wwwAuthHeaders = headers
|
||||
}
|
||||
|
||||
// getCredentialHelper parses a 'credsConfig' from the git and OS environments,
|
||||
// returning the appropriate CredentialHelper to authenticate requests with.
|
||||
//
|
||||
@ -127,6 +132,9 @@ func (ctxt *CredentialHelperContext) GetCredentialHelper(helper CredentialHelper
|
||||
if u.Scheme == "cert" || ctxt.urlConfig.Bool("credential", rawurl, "usehttppath", false) {
|
||||
input["path"] = []string{strings.TrimPrefix(u.Path, "/")}
|
||||
}
|
||||
if len(ctxt.wwwAuthHeaders) != 0 && !ctxt.urlConfig.Bool("credential", rawurl, "skipwwwauth", false) {
|
||||
input["wwwauth[]"] = ctxt.wwwAuthHeaders
|
||||
}
|
||||
|
||||
if helper != nil {
|
||||
return CredentialHelperWrapper{CredentialHelper: helper, Input: input, Url: u}
|
||||
|
@ -68,7 +68,7 @@ func (c *Client) doWithAuth(remote string, access creds.Access, req *http.Reques
|
||||
res, err := c.doWithCreds(req, credWrapper, access, via)
|
||||
if err != nil {
|
||||
if errors.IsAuthError(err) {
|
||||
newMode, newModes := getAuthAccess(res, access.Mode(), c.access)
|
||||
newMode, newModes, headers := getAuthAccess(res, access.Mode(), c.access)
|
||||
newAccess := access.Upgrade(newMode)
|
||||
if newAccess.Mode() != access.Mode() {
|
||||
c.Endpoints.SetAccess(newAccess)
|
||||
@ -79,6 +79,7 @@ func (c *Client) doWithAuth(remote string, access creds.Access, req *http.Reques
|
||||
req.Header.Del("Authorization")
|
||||
credWrapper.CredentialHelper.Reject(credWrapper.Creds)
|
||||
}
|
||||
c.credContext.SetWWWAuthHeaders(headers)
|
||||
}
|
||||
}
|
||||
|
||||
@ -314,14 +315,18 @@ var (
|
||||
authenticateHeaders = []string{"Lfs-Authenticate", "Www-Authenticate"}
|
||||
)
|
||||
|
||||
func getAuthAccess(res *http.Response, access creds.AccessMode, modes []creds.AccessMode) (creds.AccessMode, []creds.AccessMode) {
|
||||
func getAuthAccess(res *http.Response, access creds.AccessMode, modes []creds.AccessMode) (creds.AccessMode, []creds.AccessMode, []string) {
|
||||
newModes := make([]creds.AccessMode, 0, len(modes))
|
||||
for _, mode := range modes {
|
||||
if access != mode {
|
||||
newModes = append(newModes, mode)
|
||||
}
|
||||
}
|
||||
headers := make([]string, 0)
|
||||
if res != nil {
|
||||
for _, headerName := range authenticateHeaders {
|
||||
headers = append(headers, res.Header[headerName]...)
|
||||
}
|
||||
supportedModes := make(map[creds.AccessMode]struct{})
|
||||
for _, headerName := range authenticateHeaders {
|
||||
for _, auth := range res.Header[headerName] {
|
||||
@ -338,10 +343,10 @@ func getAuthAccess(res *http.Response, access creds.AccessMode, modes []creds.Ac
|
||||
}
|
||||
for _, mode := range newModes {
|
||||
if _, ok := supportedModes[mode]; ok {
|
||||
return mode, newModes
|
||||
return mode, newModes, headers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return creds.BasicAccess, newModes
|
||||
return creds.BasicAccess, newModes, headers
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ func main() {
|
||||
|
||||
func fill() {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
creds := map[string]string{}
|
||||
creds := map[string][]string{}
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
@ -58,7 +58,11 @@ func fill() {
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "CREDS RECV: %s\n", line)
|
||||
creds[parts[0]] = strings.TrimSpace(parts[1])
|
||||
if _, ok := creds[parts[0]]; ok {
|
||||
creds[parts[0]] = append(creds[parts[0]], strings.TrimSpace(parts[1]))
|
||||
} else {
|
||||
creds[parts[0]] = []string{strings.TrimSpace(parts[1])}
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
@ -66,8 +70,8 @@ func fill() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
hostPieces := strings.SplitN(creds["host"], ":", 2)
|
||||
user, pass, err := credsForHostAndPath(hostPieces[0], creds["path"])
|
||||
hostPieces := strings.SplitN(firstEntryForKey(creds, "host"), ":", 2)
|
||||
user, pass, err := credsForHostAndPath(hostPieces[0], firstEntryForKey(creds, "path"))
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
@ -75,17 +79,30 @@ func fill() {
|
||||
|
||||
if user != "skip" {
|
||||
if _, ok := creds["username"]; !ok {
|
||||
creds["username"] = user
|
||||
creds["username"] = []string{user}
|
||||
}
|
||||
|
||||
if _, ok := creds["password"]; !ok {
|
||||
creds["password"] = pass
|
||||
creds["password"] = []string{pass}
|
||||
}
|
||||
}
|
||||
|
||||
mode := os.Getenv("LFS_TEST_CREDS_WWWAUTH")
|
||||
wwwauth := firstEntryForKey(creds, "wwwauth[]")
|
||||
if mode == "required" && !strings.HasPrefix(wwwauth, "Basic ") {
|
||||
fmt.Fprintf(os.Stderr, "Missing required 'wwwauth[]' key in credentials\n")
|
||||
os.Exit(1)
|
||||
} else if mode == "forbidden" && wwwauth != "" {
|
||||
fmt.Fprintf(os.Stderr, "Unexpected 'wwwauth[]' key in credentials\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
delete(creds, "wwwauth[]")
|
||||
|
||||
for key, value := range creds {
|
||||
fmt.Fprintf(os.Stderr, "CREDS SEND: %s=%s\n", key, value)
|
||||
fmt.Fprintf(os.Stdout, "%s=%s\n", key, value)
|
||||
for _, entry := range value {
|
||||
fmt.Fprintf(os.Stderr, "CREDS SEND: %s=%s\n", key, entry)
|
||||
fmt.Fprintf(os.Stdout, "%s=%s\n", key, entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,3 +145,10 @@ func credsFromFilename(file string) (string, string, error) {
|
||||
func log() {
|
||||
fmt.Fprintf(os.Stderr, "CREDS received command: %s (ignored)\n", os.Args[1])
|
||||
}
|
||||
|
||||
func firstEntryForKey(input map[string][]string, key string) string {
|
||||
if val, ok := input[key]; ok && len(val) > 0 {
|
||||
return val[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
@ -1589,6 +1589,7 @@ func skipIfNoCookie(w http.ResponseWriter, r *http.Request, id string) bool {
|
||||
func skipIfBadAuth(w http.ResponseWriter, r *http.Request, id string) bool {
|
||||
auth := r.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
w.Header().Add("Lfs-Authenticate", "Basic realm=\"testsuite\"")
|
||||
w.WriteHeader(401)
|
||||
return true
|
||||
}
|
||||
|
@ -169,6 +169,124 @@ begin_test "credentials with useHttpPath, with correct password"
|
||||
)
|
||||
end_test
|
||||
|
||||
begin_test "credentials send wwwauth[] by default"
|
||||
(
|
||||
set -e
|
||||
ensure_git_version_isnt $VERSION_LOWER "2.41.0"
|
||||
export LFS_TEST_CREDS_WWWAUTH=required
|
||||
|
||||
reponame="$(basename "$0" ".sh")-wwwauth-required"
|
||||
setup_remote_repo "$reponame"
|
||||
|
||||
printf "path:$reponame" > "$CREDSDIR/127.0.0.1--$reponame"
|
||||
|
||||
clone_repo "$reponame" "$reponame"
|
||||
git checkout -b new-branch
|
||||
|
||||
git lfs track "*.dat" 2>&1 | tee track.log
|
||||
grep "Tracking \"\*.dat\"" track.log
|
||||
|
||||
# creating new branch does not re-send any objects existing on other
|
||||
# remote branches anymore, generate new object, different from prev tests
|
||||
contents="b"
|
||||
contents_oid=$(calc_oid "$contents")
|
||||
|
||||
printf "%s" "$contents" > b.dat
|
||||
git add b.dat
|
||||
git add .gitattributes
|
||||
git commit -m "add b.dat"
|
||||
|
||||
GIT_TERMINAL_PROMPT=0 GIT_TRACE=1 git push origin new-branch 2>&1 | tee push.log
|
||||
grep "Uploading LFS objects: 100% (1/1), 1 B" push.log
|
||||
echo "approvals:"
|
||||
[ "1" -eq "$(cat push.log | grep "creds: git credential approve" | wc -l)" ]
|
||||
echo "fills:"
|
||||
[ "1" -eq "$(cat push.log | grep "creds: git credential fill" | wc -l)" ]
|
||||
echo "credential calls have path:"
|
||||
credcalls="$(grep "creds: git credential" push.log)"
|
||||
[ "0" -eq "$(echo "$credcalls" | grep '", "")' | wc -l)" ]
|
||||
expected="$(echo "$credcalls" | wc -l)"
|
||||
[ "$expected" -eq "$(printf "%s" "$credcalls" | grep "t-credentials" | wc -l)" ]
|
||||
)
|
||||
end_test
|
||||
|
||||
begin_test "credentials sends wwwauth[] and fails with finicky helper"
|
||||
(
|
||||
set -e
|
||||
ensure_git_version_isnt $VERSION_LOWER "2.41.0"
|
||||
export LFS_TEST_CREDS_WWWAUTH=forbidden
|
||||
|
||||
reponame="$(basename "$0" ".sh")-wwwauth-forbidden-finicky"
|
||||
setup_remote_repo "$reponame"
|
||||
|
||||
printf "path:$reponame" > "$CREDSDIR/127.0.0.1--$reponame"
|
||||
|
||||
clone_repo "$reponame" "$reponame"
|
||||
git checkout -b new-branch
|
||||
|
||||
git lfs track "*.dat" 2>&1 | tee track.log
|
||||
grep "Tracking \"\*.dat\"" track.log
|
||||
|
||||
# creating new branch does not re-send any objects existing on other
|
||||
# remote branches anymore, generate new object, different from prev tests
|
||||
contents="b"
|
||||
contents_oid=$(calc_oid "$contents")
|
||||
|
||||
printf "%s" "$contents" > b.dat
|
||||
git add b.dat
|
||||
git add .gitattributes
|
||||
git commit -m "add b.dat"
|
||||
|
||||
GIT_TERMINAL_PROMPT=0 GIT_TRACE=1 git push origin new-branch 2>&1 | tee push.log
|
||||
echo "approvals:"
|
||||
[ "0" -eq "$(cat push.log | grep "creds: git credential approve" | wc -l)" ]
|
||||
echo "fills:"
|
||||
[ "2" -eq "$(cat push.log | grep "creds: git credential fill" | wc -l)" ]
|
||||
)
|
||||
end_test
|
||||
|
||||
begin_test "credentials skips wwwauth[] with option"
|
||||
(
|
||||
set -e
|
||||
ensure_git_version_isnt $VERSION_LOWER "2.41.0"
|
||||
export LFS_TEST_CREDS_WWWAUTH=forbidden
|
||||
|
||||
reponame="$(basename "$0" ".sh")-wwwauth-skip"
|
||||
setup_remote_repo "$reponame"
|
||||
git config --global credential.$GITSERVER.skipwwwauth true
|
||||
|
||||
printf "path:$reponame" > "$CREDSDIR/127.0.0.1--$reponame"
|
||||
|
||||
clone_repo "$reponame" "$reponame"
|
||||
git checkout -b new-branch
|
||||
|
||||
git lfs track "*.dat" 2>&1 | tee track.log
|
||||
grep "Tracking \"\*.dat\"" track.log
|
||||
|
||||
# creating new branch does not re-send any objects existing on other
|
||||
# remote branches anymore, generate new object, different from prev tests
|
||||
contents="b"
|
||||
contents_oid=$(calc_oid "$contents")
|
||||
|
||||
printf "%s" "$contents" > b.dat
|
||||
git add b.dat
|
||||
git add .gitattributes
|
||||
git commit -m "add b.dat"
|
||||
|
||||
GIT_TERMINAL_PROMPT=0 GIT_TRACE=1 git push origin new-branch 2>&1 | tee push.log
|
||||
grep "Uploading LFS objects: 100% (1/1), 1 B" push.log
|
||||
echo "approvals:"
|
||||
[ "1" -eq "$(cat push.log | grep "creds: git credential approve" | wc -l)" ]
|
||||
echo "fills:"
|
||||
[ "1" -eq "$(cat push.log | grep "creds: git credential fill" | wc -l)" ]
|
||||
echo "credential calls have path:"
|
||||
credcalls="$(grep "creds: git credential" push.log)"
|
||||
[ "0" -eq "$(echo "$credcalls" | grep '", "")' | wc -l)" ]
|
||||
expected="$(echo "$credcalls" | wc -l)"
|
||||
[ "$expected" -eq "$(printf "%s" "$credcalls" | grep "t-credentials" | wc -l)" ]
|
||||
)
|
||||
end_test
|
||||
|
||||
begin_test "git credential"
|
||||
(
|
||||
set -e
|
||||
|
Loading…
Reference in New Issue
Block a user