1
0
mirror of https://github.com/golang/go synced 2024-11-22 11:04:40 -07:00

cmd/go: add GOAUTH mechanism for HTTP authentication

This change adds a new environment variable GOAUTH which takes a semicolon-separated list of commands to run for authentication during go-import resolution and HTTPS module mirror protocol interactions.
This CL only supports netrc and off. Future CLs to follow will extend support to git and a custom authenticator command.

For #26232

Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-longtest,gotip-windows-amd64-longtest
Change-Id: I6cfa4c89fd27a7a4e7d25c8713d191dc82b7e28a
Reviewed-on: https://go-review.googlesource.com/c/go/+/605256
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>
This commit is contained in:
Sam Thanawalla 2024-08-13 16:48:11 +00:00
parent b2a856e82c
commit 8194d735cf
8 changed files with 223 additions and 17 deletions

View File

@ -2273,6 +2273,12 @@
// GOARCH // GOARCH
// The architecture, or processor, for which to compile code. // The architecture, or processor, for which to compile code.
// Examples are amd64, 386, arm, ppc64. // Examples are amd64, 386, arm, ppc64.
// GOAUTH
// A semicolon-separated list of authentication commands for go-import and
// HTTPS module mirror interactions. Currently supports
// "off" (disables authentication) and
// "netrc" (uses credentials from NETRC or the .netrc file in your home directory).
// The default is netrc.
// GOBIN // GOBIN
// The directory where 'go install' will install a command. // The directory where 'go install' will install a command.
// GOCACHE // GOCACHE

View File

@ -5,28 +5,103 @@
// Package auth provides access to user-provided authentication credentials. // Package auth provides access to user-provided authentication credentials.
package auth package auth
import "net/http" import (
"cmd/go/internal/base"
"cmd/go/internal/cfg"
"net/http"
"path"
"slices"
"strings"
"sync"
)
// AddCredentials fills in the user's credentials for req, if any. var (
// The return value reports whether any matching credentials were found. credentialCache sync.Map // prefix → http.Header
func AddCredentials(req *http.Request) (added bool) { authOnce sync.Once
netrc, _ := readNetrc() )
if len(netrc) == 0 {
// AddCredentials populates the request header with the user's credentials
// as specified by the GOAUTH environment variable.
// It returns whether any matching credentials were found.
// req must use HTTPS or this function will panic.
func AddCredentials(req *http.Request) bool {
if req.URL.Scheme != "https" {
panic("GOAUTH called without https")
}
if cfg.GOAUTH == "off" {
return false return false
} }
authOnce.Do(runGoAuth)
host := req.Host currentPrefix := strings.TrimPrefix(req.URL.String(), "https://")
if host == "" { // Iteratively try prefixes, moving up the path hierarchy.
host = req.URL.Hostname() for currentPrefix != "/" && currentPrefix != "." && currentPrefix != "" {
} if loadCredential(req, currentPrefix) {
// TODO(golang.org/issue/26232): Support arbitrary user-provided credentials.
for _, l := range netrc {
if l.machine == host {
req.SetBasicAuth(l.login, l.password)
return true return true
} }
}
// Move to the parent directory.
currentPrefix = path.Dir(currentPrefix)
}
return false return false
} }
// runGoAuth executes authentication commands specified by the GOAUTH
// environment variable handling 'off', 'netrc', and 'git' methods specially,
// and storing retrieved credentials for future access.
func runGoAuth() {
// The GOAUTH commands are processed in reverse order to prioritize
// credentials in the order they were specified.
goAuthCmds := strings.Split(cfg.GOAUTH, ";")
slices.Reverse(goAuthCmds)
for _, cmdStr := range goAuthCmds {
cmdStr = strings.TrimSpace(cmdStr)
switch {
case cmdStr == "off":
if len(goAuthCmds) != 1 {
base.Fatalf("GOAUTH=off cannot be combined with other authentication commands (GOAUTH=%s)", cfg.GOAUTH)
}
return
case cmdStr == "netrc":
lines, err := readNetrc()
if err != nil {
base.Fatalf("could not parse netrc (GOAUTH=%s): %v", cfg.GOAUTH, err)
}
for _, l := range lines {
r := http.Request{Header: make(http.Header)}
r.SetBasicAuth(l.login, l.password)
storeCredential([]string{l.machine}, r.Header)
}
case strings.HasPrefix(cmdStr, "git"):
base.Fatalf("unimplemented: %s", cmdStr)
default:
base.Fatalf("unimplemented: %s", cmdStr)
}
}
}
// loadCredential retrieves cached credentials for the given url prefix and adds
// them to the request headers.
func loadCredential(req *http.Request, prefix string) bool {
headers, ok := credentialCache.Load(prefix)
if !ok {
return false
}
for key, values := range headers.(http.Header) {
for _, value := range values {
req.Header.Add(key, value)
}
}
return true
}
// storeCredential caches or removes credentials (represented by HTTP headers)
// associated with given URL prefixes.
func storeCredential(prefixes []string, header http.Header) {
for _, prefix := range prefixes {
if len(header) == 0 {
credentialCache.Delete(prefix)
} else {
credentialCache.Store(prefix, header)
}
}
}

View File

@ -0,0 +1,51 @@
// Copyright 2018 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.
package auth
import (
"net/http"
"reflect"
"testing"
)
func TestCredentialCache(t *testing.T) {
testCases := []netrcLine{
{"api.github.com", "user", "pwd"},
{"test.host", "user2", "pwd2"},
{"oneline", "user3", "pwd3"},
{"hasmacro.too", "user4", "pwd4"},
{"hasmacro.too", "user5", "pwd5"},
}
for _, tc := range testCases {
want := http.Request{Header: make(http.Header)}
want.SetBasicAuth(tc.login, tc.password)
storeCredential([]string{tc.machine}, want.Header)
got := &http.Request{Header: make(http.Header)}
ok := loadCredential(got, tc.machine)
if !ok || !reflect.DeepEqual(got.Header, want.Header) {
t.Errorf("loadCredential:\nhave %q\nwant %q", got.Header, want.Header)
}
}
}
func TestCredentialCacheDelete(t *testing.T) {
// Store a credential for api.github.com
want := http.Request{Header: make(http.Header)}
want.SetBasicAuth("user", "pwd")
storeCredential([]string{"api.github.com"}, want.Header)
got := &http.Request{Header: make(http.Header)}
ok := loadCredential(got, "api.github.com")
if !ok || !reflect.DeepEqual(got.Header, want.Header) {
t.Errorf("parseNetrc:\nhave %q\nwant %q", got.Header, want.Header)
}
// Providing an empty header for api.github.com should clear credentials.
want = http.Request{Header: make(http.Header)}
storeCredential([]string{"api.github.com"}, want.Header)
got = &http.Request{Header: make(http.Header)}
ok = loadCredential(got, "api.github.com")
if ok {
t.Errorf("loadCredential:\nhave %q\nwant %q", got.Header, want.Header)
}
}

View File

@ -433,6 +433,7 @@ var (
GONOSUMDB, GONOSUMDBChanged = EnvOrAndChanged("GONOSUMDB", GOPRIVATE) GONOSUMDB, GONOSUMDBChanged = EnvOrAndChanged("GONOSUMDB", GOPRIVATE)
GOINSECURE = Getenv("GOINSECURE") GOINSECURE = Getenv("GOINSECURE")
GOVCS = Getenv("GOVCS") GOVCS = Getenv("GOVCS")
GOAUTH, GOAUTHChanged = EnvOrAndChanged("GOAUTH", "netrc")
) )
// EnvOrAndChanged returns the environment variable value // EnvOrAndChanged returns the environment variable value

View File

@ -80,6 +80,7 @@ func MkEnv() []cfg.EnvVar {
env := []cfg.EnvVar{ env := []cfg.EnvVar{
{Name: "GO111MODULE", Value: cfg.Getenv("GO111MODULE")}, {Name: "GO111MODULE", Value: cfg.Getenv("GO111MODULE")},
{Name: "GOARCH", Value: cfg.Goarch, Changed: cfg.Goarch != runtime.GOARCH}, {Name: "GOARCH", Value: cfg.Goarch, Changed: cfg.Goarch != runtime.GOARCH},
{Name: "GOAUTH", Value: cfg.GOAUTH, Changed: cfg.GOAUTHChanged},
{Name: "GOBIN", Value: cfg.GOBIN}, {Name: "GOBIN", Value: cfg.GOBIN},
{Name: "GOCACHE"}, {Name: "GOCACHE"},
{Name: "GOENV", Value: envFile, Changed: envFileChanged}, {Name: "GOENV", Value: envFile, Changed: envFileChanged},

View File

@ -491,6 +491,12 @@ General-purpose environment variables:
GOARCH GOARCH
The architecture, or processor, for which to compile code. The architecture, or processor, for which to compile code.
Examples are amd64, 386, arm, ppc64. Examples are amd64, 386, arm, ppc64.
GOAUTH
A semicolon-separated list of authentication commands for go-import and
HTTPS module mirror interactions. Currently supports
"off" (disables authentication) and
"netrc" (uses credentials from NETRC or the .netrc file in your home directory).
The default is netrc.
GOBIN GOBIN
The directory where 'go install' will install a command. The directory where 'go install' will install a command.
GOCACHE GOCACHE

View File

@ -0,0 +1,65 @@
# This test exercises the GOAUTH mechanism for specifying
# credentials passed in HTTPS requests to VCS servers.
# See golang.org/issue/26232
[short] skip
env GOPROXY=direct
env GOSUMDB=off
# GOAUTH should default to netrc behavior.
# Without credentials, downloading a module from a path that requires HTTPS
# basic auth should fail.
# Override default location of $HOME/.netrc
env NETRC=$WORK/empty
! go get vcs-test.golang.org/auth/or401
stderr '^\tserver response: ACCESS DENIED, buddy$'
# With credentials from a netrc file, it should succeed.
env NETRC=$WORK/netrc
go get vcs-test.golang.org/auth/or401
# GOAUTH=off should result in failures.
env GOAUTH='off'
# Without credentials, downloading a module from a path that requires HTTPS
# basic auth should fail.
env NETRC=$WORK/empty
! go get vcs-test.golang.org/auth/or401
stderr '^\tserver response: ACCESS DENIED, buddy$'
# GOAUTH='off' should ignore credentials from a valid netrc file.
env GOAUTH='off'
env NETRC=$WORK/netrc
! go get vcs-test.golang.org/auth/or401
stderr '^\tserver response: ACCESS DENIED, buddy$'
# GOAUTH=off cannot be combined with other authentication commands
env GOAUTH='off; netrc'
env NETRC=$WORK/netrc
! go get vcs-test.golang.org/auth/or401
stderr 'GOAUTH=off cannot be combined with other authentication commands \(GOAUTH=off; netrc\)'
# An unset GOAUTH should default to netrc.
env GOAUTH=
# Without credentials, downloading a module from a path that requires HTTPS
# basic auth should fail.
env NETRC=$WORK/empty
! go get vcs-test.golang.org/auth/or401
stderr '^\tserver response: ACCESS DENIED, buddy$'
# With credentials from a netrc file, it should succeed.
env NETRC=$WORK/netrc
go get vcs-test.golang.org/auth/or401
# A missing file should be fail as well.
env NETRC=$WORK/missing
! go get vcs-test.golang.org/auth/or401
stderr '^\tserver response: ACCESS DENIED, buddy$'
-- go.mod --
module private.example.com
-- $WORK/empty --
-- $WORK/netrc --
machine vcs-test.golang.org
login aladdin
password opensesame

View File

@ -37,6 +37,7 @@ const KnownEnv = `
GOARCH GOARCH
GOARM GOARM
GOARM64 GOARM64
GOAUTH
GOBIN GOBIN
GOCACHE GOCACHE
GOCACHEPROG GOCACHEPROG