Skip to content

Commit 6990c02

Browse files
brentleyjonesDenys Kurylenko
and
Denys Kurylenko
authored
UrlRewriter should be able to load credentials from .netrc (#14834)
Addresses #13111 Closes #14066. PiperOrigin-RevId: 424854105 (cherry picked from commit 1e53b1f) Co-authored-by: Denys Kurylenko <[email protected]>
1 parent af34c45 commit 6990c02

File tree

6 files changed

+344
-69
lines changed

6 files changed

+344
-69
lines changed

src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,10 @@ public void beforeCommand(CommandEnvironment env) throws AbruptExitException {
325325
try {
326326
UrlRewriter rewriter =
327327
UrlRewriter.getDownloaderUrlRewriter(
328-
repoOptions == null ? null : repoOptions.downloaderConfig, env.getReporter());
328+
repoOptions == null ? null : repoOptions.downloaderConfig,
329+
env.getReporter(),
330+
env.getClientEnv(),
331+
env.getRuntime().getFileSystem());
329332
downloadManager.setUrlRewriter(rewriter);
330333
} catch (UrlRewriterParseException e) {
331334
// It's important that the build stops ASAP, because this config file may be required for

src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/BUILD

+4
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,20 @@ java_library(
1313
srcs = glob(["*.java"]),
1414
deps = [
1515
"//src/main/java/com/google/devtools/build/lib/analysis:blaze_version_info",
16+
"//src/main/java/com/google/devtools/build/lib/authandtls",
1617
"//src/main/java/com/google/devtools/build/lib/bazel/repository/cache",
1718
"//src/main/java/com/google/devtools/build/lib/bazel/repository/cache:events",
1819
"//src/main/java/com/google/devtools/build/lib/buildeventstream",
1920
"//src/main/java/com/google/devtools/build/lib/clock",
2021
"//src/main/java/com/google/devtools/build/lib/concurrent",
2122
"//src/main/java/com/google/devtools/build/lib/events",
2223
"//src/main/java/com/google/devtools/build/lib/util",
24+
"//src/main/java/com/google/devtools/build/lib/util:os",
2325
"//src/main/java/com/google/devtools/build/lib/vfs",
2426
"//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
2527
"//src/main/java/net/starlark/java/syntax",
28+
"//third_party:auth",
29+
"//third_party:auto_value",
2630
"//third_party:guava",
2731
"//third_party:jsr305",
2832
],

src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/DownloadManager.java

+6-3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package com.google.devtools.build.lib.bazel.repository.downloader;
1616

1717
import static com.google.common.base.Preconditions.checkArgument;
18+
import static com.google.common.collect.ImmutableList.toImmutableList;
1819

1920
import com.google.common.base.MoreObjects;
2021
import com.google.common.base.Optional;
@@ -108,12 +109,14 @@ public Path download(
108109
throw new InterruptedException();
109110
}
110111

111-
List<URL> rewrittenUrls = originalUrls;
112+
ImmutableList<URL> rewrittenUrls = ImmutableList.copyOf(originalUrls);
112113
Map<URI, Map<String, String>> rewrittenAuthHeaders = authHeaders;
113114

114115
if (rewriter != null) {
115-
rewrittenUrls = rewriter.amend(originalUrls);
116-
rewrittenAuthHeaders = rewriter.updateAuthHeaders(rewrittenUrls, authHeaders);
116+
ImmutableList<UrlRewriter.RewrittenURL> rewrittenUrlMappings = rewriter.amend(originalUrls);
117+
rewrittenUrls =
118+
rewrittenUrlMappings.stream().map(url -> url.url()).collect(toImmutableList());
119+
rewrittenAuthHeaders = rewriter.updateAuthHeaders(rewrittenUrlMappings, authHeaders);
117120
}
118121

119122
URL mainUrl; // The "main" URL for this request

src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/UrlRewriter.java

+138-29
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,23 @@
1616
import static com.google.common.collect.ImmutableList.toImmutableList;
1717
import static java.nio.charset.StandardCharsets.ISO_8859_1;
1818

19+
import com.google.auth.Credentials;
20+
import com.google.auto.value.AutoValue;
1921
import com.google.common.annotations.VisibleForTesting;
2022
import com.google.common.base.Ascii;
2123
import com.google.common.base.Preconditions;
2224
import com.google.common.base.Strings;
2325
import com.google.common.collect.ImmutableList;
2426
import com.google.common.collect.ImmutableMap;
2527
import com.google.common.collect.ImmutableSet;
28+
import com.google.devtools.build.lib.authandtls.Netrc;
29+
import com.google.devtools.build.lib.authandtls.NetrcCredentials;
30+
import com.google.devtools.build.lib.authandtls.NetrcParser;
2631
import com.google.devtools.build.lib.events.Event;
2732
import com.google.devtools.build.lib.events.Reporter;
33+
import com.google.devtools.build.lib.util.OS;
34+
import com.google.devtools.build.lib.vfs.FileSystem;
35+
import com.google.devtools.build.lib.vfs.Path;
2836
import java.io.BufferedReader;
2937
import java.io.IOException;
3038
import java.io.Reader;
@@ -38,14 +46,17 @@
3846
import java.nio.file.Paths;
3947
import java.util.Base64;
4048
import java.util.Collection;
49+
import java.util.HashMap;
4150
import java.util.List;
4251
import java.util.Map;
4352
import java.util.Objects;
53+
import java.util.Optional;
4454
import java.util.function.Consumer;
4555
import java.util.function.Function;
4656
import java.util.regex.Matcher;
4757
import java.util.regex.Pattern;
4858
import javax.annotation.Nullable;
59+
import net.starlark.java.syntax.Location;
4960

5061
/**
5162
* Helper class for taking URLs and converting them according to an optional config specified by
@@ -59,13 +70,19 @@ public class UrlRewriter {
5970
private static final ImmutableSet<String> REWRITABLE_SCHEMES = ImmutableSet.of("http", "https");
6071

6172
private final UrlRewriterConfig config;
62-
private final Function<URL, List<URL>> rewriter;
73+
private final Function<URL, List<RewrittenURL>> rewriter;
74+
@Nullable private final Credentials netrcCreds;
6375

6476
@VisibleForTesting
65-
UrlRewriter(Consumer<String> log, String filePathForErrorReporting, Reader reader)
77+
UrlRewriter(
78+
Consumer<String> log,
79+
String filePathForErrorReporting,
80+
Reader reader,
81+
@Nullable Credentials netrcCreds)
6682
throws UrlRewriterParseException {
6783
Preconditions.checkNotNull(reader, "UrlRewriterConfig source must be set");
6884
this.config = new UrlRewriterConfig(filePathForErrorReporting, reader);
85+
this.netrcCreds = netrcCreds;
6986

7087
this.rewriter = this::rewrite;
7188
}
@@ -75,89 +92,124 @@ public class UrlRewriter {
7592
*
7693
* @param configPath Path to the config file to use. May be null.
7794
* @param reporter Used for logging when URLs are rewritten.
95+
* @param clientEnv a map of the current Bazel command's environment
96+
* @param fileSystem the Blaze file system
7897
*/
79-
public static UrlRewriter getDownloaderUrlRewriter(String configPath, Reporter reporter)
98+
public static UrlRewriter getDownloaderUrlRewriter(
99+
String configPath,
100+
Reporter reporter,
101+
ImmutableMap<String, String> clientEnv,
102+
FileSystem fileSystem)
80103
throws UrlRewriterParseException {
81104
Consumer<String> log = str -> reporter.handle(Event.info(str));
82105

106+
// "empty" UrlRewriter shouldn't alter auth headers
83107
if (Strings.isNullOrEmpty(configPath)) {
84-
return new UrlRewriter(log, "", new StringReader(""));
108+
return new UrlRewriter(log, "", new StringReader(""), null);
85109
}
86110

111+
Credentials creds = null;
112+
try {
113+
creds = newCredentialsFromNetrc(clientEnv, fileSystem);
114+
} catch (UrlRewriterParseException e) {
115+
// If the credentials extraction failed, we're letting bazel try without credentials.
116+
}
87117
try (BufferedReader reader = Files.newBufferedReader(Paths.get(configPath))) {
88-
return new UrlRewriter(log, configPath, reader);
118+
return new UrlRewriter(log, configPath, reader, creds);
89119
} catch (IOException e) {
90120
throw new UncheckedIOException(e);
91121
}
92122
}
93123

94124
/**
95125
* Rewrites {@code urls} using the configuration provided to {@link
96-
* #getDownloaderUrlRewriter(String, Reporter)}. The returned list of URLs may be empty if the
97-
* configuration used blocks all the input URLs.
126+
* #getDownloaderUrlRewriter(String, Reporter, ImmutableMap, FileSystem)}. The returned list of
127+
* URLs may be empty if the configuration used blocks all the input URLs.
98128
*
99129
* @param urls The input list of {@link URL}s. May be empty.
100130
* @return The amended lists of URLs.
101131
*/
102-
public List<URL> amend(List<URL> urls) {
132+
public ImmutableList<RewrittenURL> amend(List<URL> urls) {
103133
Objects.requireNonNull(urls, "URLS to check must be set but may be empty");
104134

105-
ImmutableList<URL> rewritten =
106-
urls.stream().map(rewriter).flatMap(Collection::stream).collect(toImmutableList());
107-
108-
return rewritten;
135+
return urls.stream().map(rewriter).flatMap(Collection::stream).collect(toImmutableList());
109136
}
110137

111138
/**
112-
* Updates {@code authHeaders} using the userInfo available in the provided {@code urls}.
139+
* Updates {@code authHeaders} using the userInfo available in the provided {@code urls}. Note
140+
* that if the same url is present in both {@code authHeaders} and <b>download config</b> then it
141+
* will be overridden with the value from <b>download config</b>.
113142
*
114143
* @param urls The input list of {@link URL}s. May be empty.
115144
* @param authHeaders A map of the URLs and their corresponding auth tokens.
116145
* @return A map of the updated authentication headers.
117146
*/
118147
public Map<URI, Map<String, String>> updateAuthHeaders(
119-
List<URL> urls, Map<URI, Map<String, String>> authHeaders) {
120-
ImmutableMap.Builder<URI, Map<String, String>> authHeadersBuilder =
121-
ImmutableMap.<URI, Map<String, String>>builder().putAll(authHeaders);
148+
List<RewrittenURL> urls, Map<URI, Map<String, String>> authHeaders) {
149+
Map<URI, Map<String, String>> updatedAuthHeaders = new HashMap<>(authHeaders);
122150

123-
for (URL url : urls) {
124-
String userInfo = url.getUserInfo();
151+
for (RewrittenURL url : urls) {
152+
// if URL was not re-written by UrlRewriter in first place, we should not attach auth headers
153+
// to it
154+
if (!url.rewritten()) {
155+
continue;
156+
}
157+
158+
String userInfo = url.url().getUserInfo();
125159
if (userInfo != null) {
126160
try {
127161
String token =
128162
"Basic " + Base64.getEncoder().encodeToString(userInfo.getBytes(ISO_8859_1));
129-
authHeadersBuilder.put(url.toURI(), ImmutableMap.of("Authorization", token));
163+
updatedAuthHeaders.put(url.url().toURI(), ImmutableMap.of("Authorization", token));
130164
} catch (URISyntaxException e) {
131165
// If the credentials extraction failed, we're letting bazel try without credentials.
132166
}
167+
} else if (this.netrcCreds != null) {
168+
try {
169+
Map<String, List<String>> urlAuthHeaders =
170+
this.netrcCreds.getRequestMetadata(url.url().toURI());
171+
if (urlAuthHeaders == null || urlAuthHeaders.isEmpty()) {
172+
continue;
173+
}
174+
// there could be multiple Auth headers, take the first one
175+
Map.Entry<String, List<String>> firstAuthHeader =
176+
urlAuthHeaders.entrySet().stream().findFirst().get();
177+
if (firstAuthHeader.getValue() != null && !firstAuthHeader.getValue().isEmpty()) {
178+
updatedAuthHeaders.put(
179+
url.url().toURI(),
180+
ImmutableMap.of(firstAuthHeader.getKey(), firstAuthHeader.getValue().get(0)));
181+
}
182+
} catch (URISyntaxException | IOException e) {
183+
// If the credentials extraction failed, we're letting bazel try without credentials.
184+
}
133185
}
134186
}
135187

136-
return authHeadersBuilder.build();
188+
return ImmutableMap.copyOf(updatedAuthHeaders);
137189
}
138190

139-
private ImmutableList<URL> rewrite(URL url) {
191+
private ImmutableList<RewrittenURL> rewrite(URL url) {
140192
Preconditions.checkNotNull(url);
141193

142194
// Cowardly refuse to rewrite non-HTTP(S) urls
143195
if (REWRITABLE_SCHEMES.stream()
144196
.noneMatch(scheme -> Ascii.equalsIgnoreCase(scheme, url.getProtocol()))) {
145-
return ImmutableList.of(url);
197+
return ImmutableList.of(RewrittenURL.create(url, false));
146198
}
147199

148-
List<URL> rewrittenUrls = applyRewriteRules(url);
200+
ImmutableList<RewrittenURL> rewrittenUrls = applyRewriteRules(url);
149201

150-
ImmutableList.Builder<URL> toReturn = ImmutableList.builder();
202+
ImmutableList.Builder<RewrittenURL> toReturn = ImmutableList.builder();
151203
// Now iterate over the URLs
152-
for (URL consider : rewrittenUrls) {
204+
for (RewrittenURL consider : rewrittenUrls) {
153205
// If there's an allow entry, add it to the set to return and continue
154-
if (isAllowMatched(consider)) {
206+
if (isAllowMatched(consider.url())) {
155207
toReturn.add(consider);
156208
continue;
157209
}
158210

159211
// If there's no block that matches the domain, add it to the set to return and continue
160-
if (!isBlockMatched(consider)) {
212+
if (!isBlockMatched(consider.url())) {
161213
toReturn.add(consider);
162214
}
163215
}
@@ -192,7 +244,7 @@ private static boolean isMatchingHostName(URL url, String host) {
192244
return host.equals(url.getHost()) || url.getHost().endsWith("." + host);
193245
}
194246

195-
private ImmutableList<URL> applyRewriteRules(URL url) {
247+
private ImmutableList<RewrittenURL> applyRewriteRules(URL url) {
196248
String withoutScheme = url.toString().substring(url.getProtocol().length() + 3);
197249

198250
ImmutableSet.Builder<String> rewrittenUrls = ImmutableSet.builder();
@@ -210,11 +262,12 @@ private ImmutableList<URL> applyRewriteRules(URL url) {
210262
}
211263

212264
if (!matchMade) {
213-
return ImmutableList.of(url);
265+
return ImmutableList.of(RewrittenURL.create(url, false));
214266
}
215267

216268
return rewrittenUrls.build().stream()
217269
.map(urlString -> prefixWithProtocol(urlString, url.getProtocol()))
270+
.map(plainUrl -> RewrittenURL.create(plainUrl, true))
218271
.collect(toImmutableList());
219272
}
220273

@@ -232,8 +285,64 @@ private static URL prefixWithProtocol(String url, String protocol) {
232285
}
233286
}
234287

288+
/**
289+
* Create a new {@link Credentials} object by parsing the .netrc file with following order to
290+
* search it:
291+
*
292+
* <ol>
293+
* <li>If environment variable $NETRC exists, use it as the path to the .netrc file
294+
* <li>Fallback to $HOME/.netrc or $USERPROFILE/.netrc
295+
* </ol>
296+
*
297+
* @return the {@link Credentials} object or {@code null} if there is no .netrc file.
298+
* @throws UrlRewriterParseException in case the credentials can't be constructed.
299+
*/
300+
// TODO : consider re-using RemoteModule.newCredentialsFromNetrc
301+
@VisibleForTesting
302+
static Credentials newCredentialsFromNetrc(Map<String, String> clientEnv, FileSystem fileSystem)
303+
throws UrlRewriterParseException {
304+
final Optional<String> homeDir;
305+
if (OS.getCurrent() == OS.WINDOWS) {
306+
homeDir = Optional.ofNullable(clientEnv.get("USERPROFILE"));
307+
} else {
308+
homeDir = Optional.ofNullable(clientEnv.get("HOME"));
309+
}
310+
String netrcFileString =
311+
Optional.ofNullable(clientEnv.get("NETRC"))
312+
.orElseGet(() -> homeDir.map(home -> home + "/.netrc").orElse(null));
313+
if (netrcFileString == null) {
314+
return null;
315+
}
316+
Location location = Location.fromFileLineColumn(netrcFileString, 0, 0);
317+
318+
Path netrcFile = fileSystem.getPath(netrcFileString);
319+
if (netrcFile.exists()) {
320+
try {
321+
Netrc netrc = NetrcParser.parseAndClose(netrcFile.getInputStream());
322+
return new NetrcCredentials(netrc);
323+
} catch (IOException e) {
324+
throw new UrlRewriterParseException(
325+
"Failed to parse " + netrcFile.getPathString() + ": " + e.getMessage(), location);
326+
}
327+
} else {
328+
return null;
329+
}
330+
}
331+
235332
@Nullable
236333
public String getAllBlockedMessage() {
237334
return config.getAllBlockedMessage();
238335
}
336+
337+
/** Holds the URL along with meta-info, such as whether URL was re-written or not. */
338+
@AutoValue
339+
public abstract static class RewrittenURL {
340+
static RewrittenURL create(URL url, boolean rewritten) {
341+
return new AutoValue_UrlRewriter_RewrittenURL(url, rewritten);
342+
}
343+
344+
abstract URL url();
345+
346+
abstract boolean rewritten();
347+
}
239348
}

src/test/java/com/google/devtools/build/lib/bazel/repository/downloader/BUILD

+3
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@ java_library(
1616
name = "DownloaderTestSuite_lib",
1717
srcs = glob(["*.java"]),
1818
deps = [
19+
"//src/main/java/com/google/devtools/build/lib/authandtls",
1920
"//src/main/java/com/google/devtools/build/lib/bazel/repository/cache",
2021
"//src/main/java/com/google/devtools/build/lib/bazel/repository/downloader",
2122
"//src/main/java/com/google/devtools/build/lib/events",
2223
"//src/main/java/com/google/devtools/build/lib/util",
2324
"//src/main/java/com/google/devtools/build/lib/vfs",
25+
"//src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs",
2426
"//src/main/java/net/starlark/java/syntax",
2527
"//src/test/java/com/google/devtools/build/lib/testutil",
28+
"//third_party:auth",
2629
"//third_party:guava",
2730
"//third_party:jsr305",
2831
"//third_party:junit4",

0 commit comments

Comments
 (0)