]> Cypherpunks repositories - gostls13.git/commitdiff
cmd/go: add GOAUTH mechanism for HTTP authentication
authorSam Thanawalla <samthanawalla@google.com>
Tue, 13 Aug 2024 16:48:11 +0000 (16:48 +0000)
committerSam Thanawalla <samthanawalla@google.com>
Tue, 1 Oct 2024 15:21:38 +0000 (15:21 +0000)
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>
src/cmd/go/alldocs.go
src/cmd/go/internal/auth/auth.go
src/cmd/go/internal/auth/auth_test.go [new file with mode: 0644]
src/cmd/go/internal/cfg/cfg.go
src/cmd/go/internal/envcmd/env.go
src/cmd/go/internal/help/helpdoc.go
src/cmd/go/testdata/script/goauth_netrc.txt [new file with mode: 0644]
src/internal/cfg/cfg.go

index f5af6831959c300539a49a82e57c2ce2067b427a..dcb2352beced7d7b5ae2130af5daa35205f30177 100644 (file)
 //     GOARCH
 //             The architecture, or processor, for which to compile code.
 //             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
 //             The directory where 'go install' will install a command.
 //     GOCACHE
index b4ada4ef8be5a04a6ce2d3180d87a0f40cbd942e..c5c24cf97f9d4ecf865ce7842454e453d37f6717 100644 (file)
 // Package auth provides access to user-provided authentication credentials.
 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.
-// The return value reports whether any matching credentials were found.
-func AddCredentials(req *http.Request) (added bool) {
-       netrc, _ := readNetrc()
-       if len(netrc) == 0 {
+var (
+       credentialCache sync.Map // prefix → http.Header
+       authOnce        sync.Once
+)
+
+// 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
        }
+       authOnce.Do(runGoAuth)
+       currentPrefix := strings.TrimPrefix(req.URL.String(), "https://")
+       // Iteratively try prefixes, moving up the path hierarchy.
+       for currentPrefix != "/" && currentPrefix != "." && currentPrefix != "" {
+               if loadCredential(req, currentPrefix) {
+                       return true
+               }
 
-       host := req.Host
-       if host == "" {
-               host = req.URL.Hostname()
+               // Move to the parent directory.
+               currentPrefix = path.Dir(currentPrefix)
        }
+       return false
+}
 
-       // 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
+// 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)
                }
        }
+}
 
-       return false
+// 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)
+               }
+       }
 }
diff --git a/src/cmd/go/internal/auth/auth_test.go b/src/cmd/go/internal/auth/auth_test.go
new file mode 100644 (file)
index 0000000..493c724
--- /dev/null
@@ -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)
+       }
+}
index b2545ca4ea4da3f80c3c94906b3b60b2e2714202..56b3a1677d66e688d65507bd7e96ba1d90d290c5 100644 (file)
@@ -433,6 +433,7 @@ var (
        GONOSUMDB, GONOSUMDBChanged = EnvOrAndChanged("GONOSUMDB", GOPRIVATE)
        GOINSECURE                  = Getenv("GOINSECURE")
        GOVCS                       = Getenv("GOVCS")
+       GOAUTH, GOAUTHChanged       = EnvOrAndChanged("GOAUTH", "netrc")
 )
 
 // EnvOrAndChanged returns the environment variable value
index a99b2ed140f45769ec4baff1268745cd287ef4a3..cb5e226e7bbaa158db98fafb8696c011c9e88add 100644 (file)
@@ -80,6 +80,7 @@ func MkEnv() []cfg.EnvVar {
        env := []cfg.EnvVar{
                {Name: "GO111MODULE", Value: cfg.Getenv("GO111MODULE")},
                {Name: "GOARCH", Value: cfg.Goarch, Changed: cfg.Goarch != runtime.GOARCH},
+               {Name: "GOAUTH", Value: cfg.GOAUTH, Changed: cfg.GOAUTHChanged},
                {Name: "GOBIN", Value: cfg.GOBIN},
                {Name: "GOCACHE"},
                {Name: "GOENV", Value: envFile, Changed: envFileChanged},
index dac52c4b639af8a51d5f886faa867bfc75af7101..9e3ef58e998717a1dfc08ec6c1f89c591b192eef 100644 (file)
@@ -491,6 +491,12 @@ General-purpose environment variables:
        GOARCH
                The architecture, or processor, for which to compile code.
                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
                The directory where 'go install' will install a command.
        GOCACHE
diff --git a/src/cmd/go/testdata/script/goauth_netrc.txt b/src/cmd/go/testdata/script/goauth_netrc.txt
new file mode 100644 (file)
index 0000000..2dda119
--- /dev/null
@@ -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
index 08d210b797385b26ef4a652ec8b17bdf99768b3e..ca5ab50efdb36de1e61913f0b0727c9ddf5047ea 100644 (file)
@@ -37,6 +37,7 @@ const KnownEnv = `
        GOARCH
        GOARM
        GOARM64
+       GOAUTH
        GOBIN
        GOCACHE
        GOCACHEPROG