Extend http downloader to add support for non-standard authorization headers

In order to leverage downloading from certain cloud storage providers that don't support basic authentication, this PR extends the existing authentication mechanism in bazel to forward an oauth2 token to the request header.

Sample netrc

```
machine storage.googleapis.com
        oauth2-token aaabbbbccccdddd
```

See https://github.com/bazelbuild/bazel/issues/10442

Closes #10445.

PiperOrigin-RevId: 299885980
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/skylark/SkylarkRepositoryContext.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/skylark/SkylarkRepositoryContext.java
index 0311248..eeb4f77 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/skylark/SkylarkRepositoryContext.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/skylark/SkylarkRepositoryContext.java
@@ -81,6 +81,7 @@
 import java.nio.charset.StandardCharsets;
 import java.time.Duration;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Base64;
 import java.util.List;
 import java.util.Map;
@@ -1102,6 +1103,37 @@
                     "Basic "
                         + Base64.getEncoder()
                             .encodeToString(credentials.getBytes(StandardCharsets.UTF_8))));
+          } else if ("pattern".equals(authMap.get("type"))) {
+            if (!authMap.containsKey("pattern")) {
+              throw new EvalException(
+                  null,
+                  "Found request to do pattern auth for "
+                      + entry.getKey()
+                      + " without a pattern being provided");
+            }
+
+            String result = (String) authMap.get("pattern");
+
+            for (String component : Arrays.asList("password", "login")) {
+              String demarcatedComponent = "<" + component + ">";
+
+              if (result.contains(demarcatedComponent)) {
+                if (!authMap.containsKey(component)) {
+                  throw new EvalException(
+                      null,
+                      "Auth pattern contains "
+                          + demarcatedComponent
+                          + " but it was not provided in auth dict.");
+                }
+              } else {
+                // component isn't in the pattern, ignore it
+                continue;
+              }
+
+              result = result.replaceAll(demarcatedComponent, (String) authMap.get(component));
+            }
+
+            headers.put(url.toURI(), ImmutableMap.<String, String>of("Authorization", result));
           }
         }
       } catch (MalformedURLException e) {
diff --git a/src/test/shell/bazel/skylark_repository_test.sh b/src/test/shell/bazel/skylark_repository_test.sh
index 782fafa..3bf128d 100755
--- a/src/test/shell/bazel/skylark_repository_test.sh
+++ b/src/test/shell/bazel/skylark_repository_test.sh
@@ -1779,6 +1779,9 @@
 machine bar.example.org
 login barusername
 password passbar
+
+machine oauthlife.com
+password TOKEN
 EOF
   # Read a given .netrc file and combine it with a list of URL,
   # and write the obtained authentication dicionary to disk; this
@@ -1787,7 +1790,7 @@
 load("@bazel_tools//tools/build_defs/repo:utils.bzl", "read_netrc", "use_netrc")
 def _impl(ctx):
   rc = read_netrc(ctx, ctx.attr.path)
-  auth = use_netrc(rc, ctx.attr.urls)
+  auth = use_netrc(rc, ctx.attr.urls, {"oauthlife.com": "Bearer <password>",})
   ctx.file("data.bzl", "auth = %s" % (auth,))
   ctx.file("BUILD", "")
   ctx.file("WORKSPACE", "")
@@ -1811,6 +1814,7 @@
     "https://foo.example.org:8080/file2.tar",
     "https://bar.example.org/file3.tar",
     "https://evil.com/bar.example.org/file4.tar",
+    "https://oauthlife.com/fizz/buzz/file5.tar",
   ],
 )
 EOF
@@ -1833,6 +1837,11 @@
       "login": "barusername",
       "password" : "passbar",
     },
+    "https://oauthlife.com/fizz/buzz/file5.tar": {
+      "type" : "pattern",
+      "pattern" : "Bearer <password>",
+      "password" : "TOKEN",
+    },
 }
 EOF
   cat > verify.bzl <<'EOF'
@@ -1939,6 +1948,41 @@
       || fail "Expected success despite needing a file behind basic auth"
 }
 
+function test_http_archive_auth_patterns() {
+  mkdir x
+  echo 'exports_files(["file.txt"])' > x/BUILD
+  echo 'Hello World' > x/file.txt
+  tar cvf x.tar x
+  sha256=$(sha256sum x.tar | head -c 64)
+  serve_file_auth x.tar
+  cat > WORKSPACE <<EOF
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+http_archive(
+  name="ext",
+  url = "http://127.0.0.1:$nc_port/x.tar",
+  netrc = "$(pwd)/.netrc",
+  sha256="$sha256",
+  auth_patterns = {
+    "127.0.0.1": "Bearer <password>"
+  }
+)
+EOF
+  cat > .netrc <<'EOF'
+machine 127.0.0.1
+password TOKEN
+EOF
+  cat > BUILD <<'EOF'
+genrule(
+  name = "it",
+  srcs = ["@ext//x:file.txt"],
+  outs = ["it.txt"],
+  cmd = "cp $< $@",
+)
+EOF
+  bazel build //:it \
+      || fail "Expected success despite needing a file behind bearer auth"
+}
+
 function test_implicit_netrc() {
   mkdir x
   echo 'exports_files(["file.txt"])' > x/BUILD
diff --git a/src/test/shell/bazel/testing_server.py b/src/test/shell/bazel/testing_server.py
index e78c49a..b84f100 100644
--- a/src/test/shell/bazel/testing_server.py
+++ b/src/test/shell/bazel/testing_server.py
@@ -44,7 +44,9 @@
   simulate_timeout = False
   filename = None
   redirect = None
-  valid_header = b'Basic ' + base64.b64encode('foo:bar'.encode('ascii'))
+  valid_headers = [
+      b'Basic ' + base64.b64encode('foo:bar'.encode('ascii')), b'Bearer TOKEN'
+  ]
 
   def do_HEAD(self):  # pylint: disable=invalid-name
     self.send_response(200)
@@ -84,12 +86,13 @@
       return
 
     auth_header = self.headers.get('Authorization', '').encode('ascii')
-    if auth_header == self.valid_header:
+
+    if auth_header in self.valid_headers:
       self.do_HEAD()
       self.serve_file()
     else:
       self.do_AUTHHEAD()
-      self.wfile.write(b'Login required.')
+      self.wfile.write(b'Login required.' + str(auth_header))
 
   def serve_file(self):
     path_to_serve = self.path[1:]
diff --git a/tools/build_defs/repo/http.bzl b/tools/build_defs/repo/http.bzl
index a77f4ed..587dff0 100644
--- a/tools/build_defs/repo/http.bzl
+++ b/tools/build_defs/repo/http.bzl
@@ -43,16 +43,16 @@
     """Given the list of URLs obtain the correct auth dict."""
     if ctx.attr.netrc:
         netrc = read_netrc(ctx, ctx.attr.netrc)
-        return use_netrc(netrc, urls)
+        return use_netrc(netrc, urls, ctx.attr.auth_patterns)
 
     if "HOME" in ctx.os.environ:
         if not ctx.os.name.startswith("windows"):
             netrcfile = "%s/.netrc" % (ctx.os.environ["HOME"],)
             if ctx.execute(["test", "-f", netrcfile]).return_code == 0:
                 netrc = read_netrc(ctx, netrcfile)
-                return use_netrc(netrc, urls)
+                return use_netrc(netrc, urls, ctx.attr.auth_patterns)
 
-        # TODO: Search at a similarly canonical place for Windows as well
+    # TODO: Search at a similarly canonical place for Windows as well
 
     return {}
 
@@ -192,6 +192,45 @@
     "netrc": attr.string(
         doc = "Location of the .netrc file to use for authentication",
     ),
+    "auth_patterns": attr.string_dict(
+        doc = """An optional dict mapping host names to custom authorization patterns.
+
+If a URL's host name is present in this dict the value will be used as a pattern when
+generating the authorization header for the http request. This enables the use of custom
+authorization schemes used in a lot of common cloud storage providers.
+
+The pattern currently supports 2 tokens: `<login>` and `<password>`, which are replaced with
+their equivalent value in the netrc file for the same host name. After formatting, the result
+is set as the value for the `Authorization` field of the HTTP request.
+
+Example WORKSPACE and netrc for a http download to an oauth2 enabled API using a bearer token:
+
+```python
+http_jar(
+    name = "custom-artifact",
+    url = "https://storage.cloudprovider.com/custom-bucket/custom-artifact.jar",
+    sha256 = "...",
+    netrc = "/home/testuser/workspace/netrc",
+    auth_patterns = {
+        "storage.cloudprovider.com": "Bearer <password>"
+    }
+)
+
+netrc:
+
+```
+machine storage.cloudprovider.com
+        password RANDOM-TOKEN
+```
+
+The final HTTP request would have the following header:
+
+```
+Authorization: Bearer RANDOM-TOKEN
+```
+
+""",
+    ),
     "canonical_id": attr.string(
         doc = """A canonical id of the archive downloaded.
 
@@ -378,6 +417,45 @@
     "netrc": attr.string(
         doc = "Location of the .netrc file to use for authentication",
     ),
+    "auth_patterns": attr.string_dict(
+        doc = """An optional dict mapping host names to custom authorization patterns.
+
+If a URL's host name is present in this dict the value will be used as a pattern when
+generating the authorization header for the http request. This enables the use of custom
+authorization schemes used in a lot of common cloud storage providers.
+
+The pattern currently supports 2 tokens: `<login>` and `<password>`, which are replaced with
+their equivalent value in the netrc file for the same host name. After formatting, the result
+is set as the value for the `Authorization` field of the HTTP request.
+
+Example WORKSPACE and netrc for a http download to an oauth2 enabled API using a bearer token:
+
+```python
+http_jar(
+    name = "custom-artifact",
+    url = "https://storage.cloudprovider.com/custom-bucket/custom-artifact.jar",
+    sha256 = "...",
+    netrc = "/home/testuser/workspace/netrc",
+    auth_patterns = {
+        "storage.cloudprovider.com": "Bearer <password>"
+    }
+)
+
+netrc:
+
+```
+machine storage.cloudprovider.com
+        password RANDOM-TOKEN
+```
+
+The final HTTP request would have the following header:
+
+```
+Authorization: Bearer RANDOM-TOKEN
+```
+
+""",
+    ),
 }
 
 http_file = repository_rule(
@@ -429,6 +507,44 @@
     "netrc": attr.string(
         doc = "Location of the .netrc file to use for authentication",
     ),
+    "auth_patterns": attr.string_dict(
+        doc = """An optional dict mapping host names to custom authorization patterns.
+
+If a URL's host name is present in this dict the value will be used as a pattern when
+generating the authorization header for the http request. This enables the use of custom
+authorization schemes used in a lot of common cloud storage providers.
+
+The pattern currently supports 2 tokens: `<login>` and `<password>`, which are replaced with
+their equivalent value in the netrc file for the same host name. After formatting, the result
+is set as the value for the `Authorization` field of the HTTP request.
+
+Example WORKSPACE and netrc for a http download to an oauth2 enabled API using a bearer token:
+
+```python
+http_jar(
+    name = "custom-artifact",
+    url = "https://storage.cloudprovider.com/custom-bucket/custom-artifact.jar",
+    sha256 = "...",
+    netrc = "/home/testuser/workspace/netrc",
+    auth_patterns = {
+        "storage.cloudprovider.com": "Bearer <password>"
+    }
+)
+
+netrc:
+
+```
+machine storage.cloudprovider.com
+        password RANDOM-TOKEN
+```
+
+The final HTTP request would have the following header:
+
+```
+Authorization: Bearer RANDOM-TOKEN
+```
+""",
+    ),
 }
 
 http_jar = repository_rule(
diff --git a/tools/build_defs/repo/utils.bzl b/tools/build_defs/repo/utils.bzl
index 6c01a47..0ee6681 100644
--- a/tools/build_defs/repo/utils.bzl
+++ b/tools/build_defs/repo/utils.bzl
@@ -289,19 +289,20 @@
         netrc[currentmachinename] = currentmachine
     return netrc
 
-def use_netrc(netrc, urls):
+def use_netrc(netrc, urls, patterns):
     """compute an auth dict from a parsed netrc file and a list of URLs
 
     Args:
       netrc: a netrc file already parsed to a dict, e.g., as obtained from
         read_netrc
       urls: a list of URLs.
+      patterns: optional dict of url to authorization patterns
 
     Returns:
       dict suitable as auth argument for ctx.download; more precisely, the dict
       will map all URLs where the netrc file provides login and password to a
-      dict containing the corresponding login and passwored, as well as the
-      mapping of "type" to "basic"
+      dict containing the corresponding login, password and optional authorization pattern,
+      as well as the mapping of "type" to "basic" or "pattern".
     """
     auth = {}
     for url in urls:
@@ -316,10 +317,24 @@
         if not host in netrc:
             continue
         authforhost = netrc[host]
-        if "login" in authforhost and "password" in authforhost:
+        if host in patterns:
+            auth_dict = {
+                "type": "pattern",
+                "pattern": patterns[host],
+            }
+
+            if "login" in authforhost:
+                auth_dict["login"] = authforhost["login"]
+
+            if "password" in authforhost:
+                auth_dict["password"] = authforhost["password"]
+
+            auth[url] = auth_dict
+        elif "login" in authforhost and "password" in authforhost:
             auth[url] = {
                 "type": "basic",
                 "login": authforhost["login"],
                 "password": authforhost["password"],
             }
+
     return auth