diff --git a/src/main/java/com/google/devtools/build/lib/authandtls/AuthAndTLSOptions.java b/src/main/java/com/google/devtools/build/lib/authandtls/AuthAndTLSOptions.java index 7423b555a13e9c..ae592aafac8981 100644 --- a/src/main/java/com/google/devtools/build/lib/authandtls/AuthAndTLSOptions.java +++ b/src/main/java/com/google/devtools/build/lib/authandtls/AuthAndTLSOptions.java @@ -148,11 +148,13 @@ public class AuthAndTLSOptions extends OptionsBase { documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = - "Configures Credential Helpers to use for retrieving credentials for the provided scope" - + " (domain).\n\n" - + "Credentials from Credential Helpers take precedence over credentials from" - + " --google_default_credentials, `--google_credentials`, or" - + " .netrc.\n\n" + "Configures a credential helper to use for retrieving authorization credentials for " + + " repository fetching, remote caching and execution, and the build event" + + " service.\n\n" + + "Credentials supplied by a helper take precedence over credentials supplied by" + + " --google_default_credentials, --google_credentials, a .netrc file, or the auth" + + " parameter to repository_ctx.download and repository_ctx.download_and_extract.\n\n" + + "May be specified multiple times to set up multiple helpers.\n\n" + "See https://github.com/bazelbuild/proposals/blob/main/designs/2022-06-07-bazel-credential-helpers.md" + " for details.") public List credentialHelpers; @@ -164,8 +166,8 @@ public class AuthAndTLSOptions extends OptionsBase { documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = - "Configures the timeout for the Credential Helper.\n\n" - + "Credential Helpers failing to respond within this timeout will fail the" + "Configures the timeout for a credential helper.\n\n" + + "Credential helpers failing to respond within this timeout will fail the" + " invocation.") public Duration credentialHelperTimeout; @@ -176,13 +178,13 @@ public class AuthAndTLSOptions extends OptionsBase { documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = - "Configures the duration for which credentials from Credential Helpers are cached.\n\n" + "The duration for which credentials supplied by a credential helper are cached.\n\n" + "Invoking with a different value will adjust the lifetime of preexisting entries;" + " pass zero to clear the cache. A clean command always clears the cache, regardless" + " of this flag.") public Duration credentialHelperCacheTimeout; - /** One of the values of the `--credential_helper` flag. */ + /** One of the values of the `--experimental_credential_helper` flag. */ @AutoValue public abstract static class UnresolvedScopedCredentialHelper { /** Returns the scope of the credential helper (if any). */ @@ -192,7 +194,7 @@ public abstract static class UnresolvedScopedCredentialHelper { public abstract String getPath(); } - /** A {@link Converter} for the `--credential_helper` flag. */ + /** A {@link Converter} for the `--experimental_credential_helper` flag. */ public static final class UnresolvedScopedCredentialHelperConverter extends Converter.Contextless { public static final UnresolvedScopedCredentialHelperConverter INSTANCE = @@ -200,7 +202,11 @@ public static final class UnresolvedScopedCredentialHelperConverter @Override public String getTypeDescription() { - return "An (unresolved) path to a credential helper for a scope."; + return "Path to a credential helper. It may be absolute, relative to the PATH environment" + + " variable, or %workspace%-relative. The path be optionally prefixed by a scope " + + " followed by an '='. The scope is a domain name, optionally with a single leading '*'" + + " wildcard component. A helper applies to URIs matching its scope, with more specific" + + " scopes preferred. If a helper has no scope, it applies to every URI."; } @Override diff --git a/src/main/java/com/google/devtools/build/lib/authandtls/GoogleAuthUtils.java b/src/main/java/com/google/devtools/build/lib/authandtls/GoogleAuthUtils.java index e226e6b069db20..409263929c7cdc 100644 --- a/src/main/java/com/google/devtools/build/lib/authandtls/GoogleAuthUtils.java +++ b/src/main/java/com/google/devtools/build/lib/authandtls/GoogleAuthUtils.java @@ -372,7 +372,6 @@ static Optional newCredentialsFromNetrc( } } - @VisibleForTesting public static CredentialHelperProvider newCredentialHelperProvider( CredentialHelperEnvironment environment, CommandLinePathFactory pathFactory, diff --git a/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/BUILD b/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/BUILD index 8a8a511068fbcc..1c95c60cdaf7da 100644 --- a/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/BUILD +++ b/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/BUILD @@ -17,6 +17,7 @@ java_library( deps = [ "//src/main/java/com/google/devtools/build/lib:runtime", "//src/main/java/com/google/devtools/build/lib/authandtls", + "//src/main/java/com/google/devtools/common/options", "//third_party:caffeine", "//third_party:guava", ], diff --git a/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/CredentialModule.java b/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/CredentialModule.java index f18aa2d338579e..ba880430b79bb4 100644 --- a/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/CredentialModule.java +++ b/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/CredentialModule.java @@ -21,6 +21,7 @@ import com.google.devtools.build.lib.authandtls.AuthAndTLSOptions; import com.google.devtools.build.lib.runtime.BlazeModule; import com.google.devtools.build.lib.runtime.CommandEnvironment; +import com.google.devtools.common.options.OptionsBase; import java.net.URI; import java.time.Duration; @@ -37,6 +38,11 @@ public Cache>> getCredentialCach return credentialCache; } + @Override + public Iterable> getCommonCommandOptions() { + return ImmutableList.of(AuthAndTLSOptions.class); + } + @Override public void beforeCommand(CommandEnvironment env) { // Update the cache expiration policy according to the command options. diff --git a/src/main/java/com/google/devtools/build/lib/bazel/BUILD b/src/main/java/com/google/devtools/build/lib/bazel/BUILD index 22c366230dbd89..3c29ea712d2909 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/BUILD +++ b/src/main/java/com/google/devtools/build/lib/bazel/BUILD @@ -26,6 +26,9 @@ java_library( "//src/main/java/com/google/devtools/build/lib/analysis:analysis_cluster", "//src/main/java/com/google/devtools/build/lib/analysis:blaze_directories", "//src/main/java/com/google/devtools/build/lib/analysis:config/build_configuration", + "//src/main/java/com/google/devtools/build/lib/authandtls", + "//src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper", + "//src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper:credential_module", "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:common", "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:inspection", "//src/main/java/com/google/devtools/build/lib/bazel/bzlmod:inspection_impl", @@ -59,6 +62,7 @@ java_library( "//src/main/java/com/google/devtools/common/options", "//src/main/protobuf:failure_details_java_proto", "//third_party:guava", + "//third_party:jsr305", ], ) diff --git a/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java b/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java index 26d05bbbef501f..5e87645a450f50 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java +++ b/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java @@ -15,6 +15,7 @@ package com.google.devtools.build.lib.bazel; +import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; @@ -25,6 +26,13 @@ import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider; import com.google.devtools.build.lib.analysis.RuleDefinition; import com.google.devtools.build.lib.analysis.config.BuildConfigurationValue; +import com.google.devtools.build.lib.authandtls.AuthAndTLSOptions; +import com.google.devtools.build.lib.authandtls.GoogleAuthUtils; +import com.google.devtools.build.lib.authandtls.StaticCredentials; +import com.google.devtools.build.lib.authandtls.credentialhelper.CredentialHelperCredentials; +import com.google.devtools.build.lib.authandtls.credentialhelper.CredentialHelperEnvironment; +import com.google.devtools.build.lib.authandtls.credentialhelper.CredentialHelperProvider; +import com.google.devtools.build.lib.authandtls.credentialhelper.CredentialModule; import com.google.devtools.build.lib.bazel.bzlmod.AttributeValues; import com.google.devtools.build.lib.bazel.bzlmod.BazelDepGraphFunction; import com.google.devtools.build.lib.bazel.bzlmod.BazelLockFileFunction; @@ -110,6 +118,7 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; +import javax.annotation.Nullable; /** Adds support for fetching external code. */ public class BazelRepositoryModule extends BlazeModule { @@ -146,6 +155,8 @@ public class BazelRepositoryModule extends BlazeModule { private List allowedYankedVersions = ImmutableList.of(); private SingleExtensionEvalFunction singleExtensionEvalFunction; + @Nullable private CredentialModule credentialModule; + public BazelRepositoryModule() { this.starlarkRepositoryFunction = new StarlarkRepositoryFunction(downloadManager); this.repositoryHandlers = repositoryRules(); @@ -263,6 +274,8 @@ SkyFunctions.BAZEL_LOCK_FILE, new BazelLockFileFunction(directories.getWorkspace .addSkyFunction(SkyFunctions.SINGLE_EXTENSION_EVAL, singleExtensionEvalFunction) .addSkyFunction(SkyFunctions.SINGLE_EXTENSION_USAGES, new SingleExtensionUsagesFunction()); filesystem = runtime.getFileSystem(); + + credentialModule = Preconditions.checkNotNull(runtime.getBlazeModule(CredentialModule.class)); } @Override @@ -373,6 +386,41 @@ public void beforeCommand(CommandEnvironment env) throws AbruptExitException { Code.BAD_DOWNLOADER_CONFIG)); } + try { + AuthAndTLSOptions authAndTlsOptions = env.getOptions().getOptions(AuthAndTLSOptions.class); + var credentialHelperEnvironment = + CredentialHelperEnvironment.newBuilder() + .setEventReporter(env.getReporter()) + .setWorkspacePath(env.getWorkspace()) + .setClientEnvironment(env.getClientEnv()) + .setHelperExecutionTimeout(authAndTlsOptions.credentialHelperTimeout) + .build(); + CredentialHelperProvider credentialHelperProvider = + GoogleAuthUtils.newCredentialHelperProvider( + credentialHelperEnvironment, + env.getCommandLinePathFactory(), + authAndTlsOptions.credentialHelpers); + + downloadManager.setCredentialFactory( + headers -> { + Preconditions.checkNotNull(headers); + + return new CredentialHelperCredentials( + credentialHelperProvider, + credentialHelperEnvironment, + credentialModule.getCredentialCache(), + Optional.of(new StaticCredentials(headers))); + }); + } catch (IOException e) { + env.getReporter().handle(Event.error(e.getMessage())); + env.getBlazeModuleEnvironment() + .exit( + new AbruptExitException( + detailedExitCode( + "Error initializing credential helper", Code.CREDENTIALS_INIT_FAILURE))); + return; + } + if (repoOptions.experimentalDistdir != null) { downloadManager.setDistdir( repoOptions.experimentalDistdir.stream() diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/DownloadManager.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/DownloadManager.java index 05250dc08608a1..f8c88defa99f73 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/DownloadManager.java +++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/DownloadManager.java @@ -61,6 +61,12 @@ public class DownloadManager { private int retries = 0; private boolean urlsAsDefaultCanonicalId; @Nullable private Credentials netrcCreds; + private CredentialFactory credentialFactory = StaticCredentials::new; + + /** Creates {@code Credentials} from a map of per-{@code URI} authentication headers. */ + public interface CredentialFactory { + Credentials create(Map>> authHeaders); + } public DownloadManager(RepositoryCache repositoryCache, Downloader downloader) { this.repositoryCache = repositoryCache; @@ -92,6 +98,10 @@ public void setNetrcCreds(Credentials netrcCreds) { this.netrcCreds = netrcCreds; } + public void setCredentialFactory(CredentialFactory credentialFactory) { + this.credentialFactory = credentialFactory; + } + /** * Downloads file to disk and returns path. * @@ -257,7 +267,7 @@ public Path download( try { downloader.download( rewrittenUrls, - new StaticCredentials(rewrittenAuthHeaders), + credentialFactory.create(rewrittenAuthHeaders), checksum, canonicalId, destination, @@ -338,7 +348,7 @@ public byte[] downloadAndReadOneUrl( for (int attempt = 0; attempt <= retries; ++attempt) { try { return httpDownloader.downloadAndReadOneUrl( - rewrittenUrls.get(0), new StaticCredentials(authHeaders), eventHandler, clientEnv); + rewrittenUrls.get(0), credentialFactory.create(authHeaders), eventHandler, clientEnv); } catch (ContentLengthMismatchException e) { if (attempt == retries) { throw e; diff --git a/src/main/protobuf/failure_details.proto b/src/main/protobuf/failure_details.proto index 99c919d2e4cdf4..2bced000af26af 100644 --- a/src/main/protobuf/failure_details.proto +++ b/src/main/protobuf/failure_details.proto @@ -249,6 +249,7 @@ message ExternalRepository { OVERRIDE_DISALLOWED_MANAGED_DIRECTORIES = 1 [(metadata) = { exit_code: 2 }]; BAD_DOWNLOADER_CONFIG = 2 [(metadata) = { exit_code: 2 }]; REPOSITORY_MAPPING_RESOLUTION_FAILED = 3 [(metadata) = { exit_code: 37 }]; + CREDENTIALS_INIT_FAILURE = 4 [(metadata) = { exit_code: 2 }]; } Code code = 1; // Additional data could include external repository names. diff --git a/src/test/java/com/google/devtools/build/lib/buildtool/util/BUILD b/src/test/java/com/google/devtools/build/lib/buildtool/util/BUILD index 09ba9771ffbd1f..bd188c69f5740e 100644 --- a/src/test/java/com/google/devtools/build/lib/buildtool/util/BUILD +++ b/src/test/java/com/google/devtools/build/lib/buildtool/util/BUILD @@ -50,6 +50,7 @@ java_library( "//src/main/java/com/google/devtools/build/lib/analysis:top_level_artifact_context", "//src/main/java/com/google/devtools/build/lib/analysis:transitive_info_collection", "//src/main/java/com/google/devtools/build/lib/analysis:workspace_status_action", + "//src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper:credential_module", "//src/main/java/com/google/devtools/build/lib/bazel:main", "//src/main/java/com/google/devtools/build/lib/bazel:repository_module", "//src/main/java/com/google/devtools/build/lib/bugreport", diff --git a/src/test/java/com/google/devtools/build/lib/buildtool/util/BuildIntegrationTestCase.java b/src/test/java/com/google/devtools/build/lib/buildtool/util/BuildIntegrationTestCase.java index 1d4cea1c13df02..2ba922485a8942 100644 --- a/src/test/java/com/google/devtools/build/lib/buildtool/util/BuildIntegrationTestCase.java +++ b/src/test/java/com/google/devtools/build/lib/buildtool/util/BuildIntegrationTestCase.java @@ -53,6 +53,7 @@ import com.google.devtools.build.lib.analysis.util.AnalysisMock; import com.google.devtools.build.lib.analysis.util.AnalysisTestUtil; import com.google.devtools.build.lib.analysis.util.AnalysisTestUtil.DummyWorkspaceStatusActionContext; +import com.google.devtools.build.lib.authandtls.credentialhelper.CredentialModule; import com.google.devtools.build.lib.bazel.BazelRepositoryModule; import com.google.devtools.build.lib.bugreport.BugReport; import com.google.devtools.build.lib.bugreport.BugReporter; @@ -530,7 +531,8 @@ protected BlazeRuntime.Builder getRuntimeBuilder() throws Exception { .addBlazeModule(new BuildIntegrationTestCommandsModule()) .addBlazeModule(new OutputFilteringModule()) .addBlazeModule(connectivityModule) - .addBlazeModule(getMockBazelRepositoryModule()); + .addBlazeModule(getMockBazelRepositoryModule()) + .addBlazeModule(new CredentialModule()); getSpawnModules().forEach(builder::addBlazeModule); builder .addBlazeModule(getBuildInfoModule()) diff --git a/src/test/shell/bazel/remote_helpers.sh b/src/test/shell/bazel/remote_helpers.sh index 52078d1065a534..bf406364fcadf4 100755 --- a/src/test/shell/bazel/remote_helpers.sh +++ b/src/test/shell/bazel/remote_helpers.sh @@ -38,8 +38,7 @@ function serve_file() { cd - } -# Serves $1 as a file on localhost:$nc_port insisting on authentication (but -# accepting any credentials. +# Serves $1 as a file on localhost:$nc_port expecting authentication. # * nc_port - the port nc is listening on. # * nc_log - the path to nc's log. # * nc_pid - the PID of nc. diff --git a/src/test/shell/bazel/starlark_repository_test.sh b/src/test/shell/bazel/starlark_repository_test.sh index 1f526efcbdeb4c..84f933fb1b6011 100755 --- a/src/test/shell/bazel/starlark_repository_test.sh +++ b/src/test/shell/bazel/starlark_repository_test.sh @@ -1713,38 +1713,64 @@ EOF expect_log "//:b.bzl" } -function test_auth_provided() { - mkdir x +# Common setup logic for test_auth_*. +# Receives as arguments the username and password for basic authentication. +# If no arguments are provided, no authentication is used. +function setup_auth() { + if [[ $# -ne 0 && $# -ne 2 ]]; then + fail "setup_auth expects exactly zero or two arguments" + fi + + mkdir -p 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 >> $(create_workspace_with_default_repos WORKSPACE) < auth_attrs.bzl < auth_attrs.bzl <<'EOF' +AUTH_ATTRS = None EOF + fi + cat > auth.bzl <<'EOF' +load("//:auth_attrs.bzl", "AUTH_ATTRS") def _impl(ctx): ctx.download_and_extract( url = ctx.attr.url, sha256 = ctx.attr.sha256, - # Use the username/password pair hard-coded - # in the testing server. - auth = {ctx.attr.url : { "type": "basic", - "login" : "foo", - "password" : "bar"}} + auth = { ctx.attr.url: AUTH_ATTRS } if AUTH_ATTRS else {}, ) -with_auth = repository_rule( +maybe_with_auth = repository_rule( implementation = _impl, - attrs = { "url" : attr.string(), "sha256" : attr.string() } + attrs = { + "url" : attr.string(), + "sha256" : attr.string(), + } ) EOF + + cat >> $(create_workspace_with_default_repos WORKSPACE) < BUILD <<'EOF' genrule( name = "it", @@ -1753,8 +1779,51 @@ genrule( cmd = "cp $< $@", ) EOF +} + +function test_auth_from_starlark() { + setup_auth foo bar + bazel build //:it \ - || fail "Expected success despite needing a file behind basic auth" + || fail "Expected success when downloading repo with basic auth" +} + +function test_auth_from_credential_helper() { + if "$is_windows"; then + # Skip on Windows: credential helper is a Python script. + return + fi + + setup_credential_helper + + setup_auth # no auth + + bazel build //:it \ + && fail "Expected failure when downloading repo without credential helper" + + bazel build --experimental_credential_helper="${TEST_TMPDIR}/credhelper" //:it \ + || fail "Expected success when downloading repo with credential helper" + + expect_credential_helper_calls 1 + + bazel build --experimental_credential_helper="${TEST_TMPDIR}/credhelper" //:it \ + || fail "Expected success when downloading repo with credential helper" + + expect_credential_helper_calls 1 # expect credentials to have been cached +} + +function test_auth_from_credential_helper_overrides_starlark() { + if "$is_windows"; then + # Skip on Windows: credential helper is a Python script. + return + fi + + setup_credential_helper + + setup_auth baduser badpass + + bazel build --experimental_credential_helper="${TEST_TMPDIR}/credhelper" //:it \ + || fail "Expected success when downloading repo with credential helper overriding basic auth" } function test_netrc_reading() { @@ -2078,7 +2147,7 @@ EOF || fail "Expected success despite needing a file behind bearer auth" } -function test_implicit_netrc() { +function test_http_archive_implicit_netrc() { mkdir x echo 'exports_files(["file.txt"])' > x/BUILD echo 'Hello World' > x/file.txt @@ -2118,6 +2187,87 @@ EOF || fail "Expected success despite needing a file behind basic auth" } +function test_http_archive_credential_helper() { + if "$is_windows"; then + # Skip on Windows: credential helper is a Python script. + return + fi + + setup_credential_helper + + 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 < BUILD <<'EOF' +genrule( + name = "it", + srcs = ["@ext//x:file.txt"], + outs = ["it.txt"], + cmd = "cp $< $@", +) +EOF + bazel build --experimental_credential_helper="${TEST_TMPDIR}/credhelper" //:it \ + || fail "Expected success despite needing a file behind credential helper" +} + +function test_http_archive_credential_helper_overrides_netrc() { + if "$is_windows"; then + # Skip on Windows: credential helper is a Python script. + return + fi + + setup_credential_helper + + 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 + + export HOME=`pwd` + if "$is_windows"; then + export USERPROFILE="$(cygpath -m ${HOME})" + fi + cat > .netrc <<'EOF' +machine 127.0.0.1 +login badusername +password badpassword +EOF + + mkdir main + cd main + cat > WORKSPACE < BUILD <<'EOF' +genrule( + name = "it", + srcs = ["@ext//x:file.txt"], + outs = ["it.txt"], + cmd = "cp $< $@", +) +EOF + bazel build --experimental_credential_helper="${TEST_TMPDIR}/credhelper" //:it \ + || fail "Expected success despite needing a file behind credential helper" +} + function test_disable_download_should_prevent_downloading() { 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 9d6cd23a57ee88..00c714792d2d95 100644 --- a/src/test/shell/bazel/testing_server.py +++ b/src/test/shell/bazel/testing_server.py @@ -94,7 +94,9 @@ def do_GET(self): # pylint: disable=invalid-name self.serve_file() else: self.do_AUTHHEAD() - self.wfile.write(b'Login required.' + str(auth_header)) + self.wfile.write( + 'Bad authorization header: {}'.format(auth_header).encode('ascii') + ) def serve_file(self): path_to_serve = self.path[1:]