From 054aaff0cd19972bc56e2680723be739d33b3fa1 Mon Sep 17 00:00:00 2001 From: Yannic Bonenberger Date: Thu, 8 Feb 2024 01:11:04 +0100 Subject: [PATCH 1/4] [credentialhelper] Respect `expires` field from helper This was recently specified in https://github.com/EngFlow/credential-helper-spec/pull/2. RELNOTES[NEW]: Bazel now repects `expires` from Credential Helpers. --- .../build/lib/authandtls/GoogleAuthUtils.java | 5 +- .../lib/authandtls/credentialhelper/BUILD | 1 + .../CredentialCacheExpiry.java | 69 +++++++++++++++++++ .../CredentialHelperCredentials.java | 17 +++-- .../credentialhelper/CredentialModule.java | 15 ++-- .../GetCredentialsResponse.java | 42 +++++++++++ .../build/lib/remote/RemoteModule.java | 4 +- .../GetCredentialsResponseTest.java | 31 ++++++++- .../build/lib/remote/RemoteModuleTest.java | 3 +- 9 files changed, 161 insertions(+), 26 deletions(-) create mode 100644 src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/CredentialCacheExpiry.java 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 a78d4074e96f9c..f7ac56fd708ac8 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 @@ -20,11 +20,10 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Strings; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; 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.GetCredentialsResponse; import com.google.devtools.build.lib.events.Event; import com.google.devtools.build.lib.runtime.CommandLinePathFactory; import com.google.devtools.build.lib.vfs.FileSystem; @@ -252,7 +251,7 @@ public static CallCredentialsProvider newCallCredentialsProvider(@Nullable Crede */ public static Credentials newCredentials( CredentialHelperEnvironment credentialHelperEnvironment, - Cache>> credentialCache, + Cache credentialCache, CommandLinePathFactory commandLinePathFactory, FileSystem fileSystem, AuthAndTLSOptions authAndTlsOptions) 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 67937d54048315..767764ca8ebc9c 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 @@ -18,6 +18,7 @@ java_library( "SystemMillisTicker.java", ], deps = [ + ":credentialhelper", "//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", diff --git a/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/CredentialCacheExpiry.java b/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/CredentialCacheExpiry.java new file mode 100644 index 00000000000000..af293f54308f05 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/CredentialCacheExpiry.java @@ -0,0 +1,69 @@ +// Copyright 2024 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.devtools.build.lib.authandtls.credentialhelper; + +import com.github.benmanes.caffeine.cache.Expiry; +import com.google.common.base.Preconditions; +import java.net.URI; +import java.time.Duration; +import java.time.Instant; + +final class CredentialCacheExpiry implements Expiry { + private Duration defaultCacheDuration = Duration.ZERO; + + /** + * Sets the default cache duration for {@link GetCredentialsResponse}s that don't set {@code expiry}. + */ + public void setDefaultCacheDuration(Duration duration) { + this.defaultCacheDuration = Preconditions.checkNotNull(duration); + } + + private Duration getExpirationTime(GetCredentialsResponse response) { + Preconditions.checkNotNull(response); + + var expires = response.getExpires(); + if (expires.isEmpty()) { + return defaultCacheDuration; + } + + var now = Instant.now(); + return Duration.between(expires.get(), now); + } + + @Override + public long expireAfterCreate(URI uri, GetCredentialsResponse response, long currentTime) { + Preconditions.checkNotNull(uri); + Preconditions.checkNotNull(response); + + return getExpirationTime(response).toNanos(); + } + + @Override + public long expireAfterUpdate(URI uri, GetCredentialsResponse response, long currentTime, long currentDuration) { + Preconditions.checkNotNull(uri); + Preconditions.checkNotNull(response); + + return getExpirationTime(response).toNanos(); + } + + @Override + public long expireAfterRead(URI uri, GetCredentialsResponse response, long currentTime, long currentDuration) { + Preconditions.checkNotNull(uri); + Preconditions.checkNotNull(response); + + // We don't extend the duration on access. + return currentDuration; + } +} diff --git a/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/CredentialHelperCredentials.java b/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/CredentialHelperCredentials.java index 5de760857dd1bf..f38df24472eeb7 100644 --- a/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/CredentialHelperCredentials.java +++ b/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/CredentialHelperCredentials.java @@ -17,7 +17,6 @@ import com.github.benmanes.caffeine.cache.Cache; import com.google.auth.Credentials; import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.io.IOException; import java.net.URI; @@ -33,7 +32,7 @@ public class CredentialHelperCredentials extends Credentials { private final CredentialHelperProvider credentialHelperProvider; private final CredentialHelperEnvironment credentialHelperEnvironment; - private final Cache>> credentialCache; + private final Cache credentialCache; private final Optional fallbackCredentials; /** Wraps around an {@link IOException} so we can smuggle it through {@link Cache#get}. */ @@ -53,7 +52,7 @@ IOException getWrapped() { public CredentialHelperCredentials( CredentialHelperProvider credentialHelperProvider, CredentialHelperEnvironment credentialHelperEnvironment, - Cache>> credentialCache, + Cache credentialCache, Optional fallbackCredentials) { this.credentialHelperProvider = Preconditions.checkNotNull(credentialHelperProvider); this.credentialHelperEnvironment = Preconditions.checkNotNull(credentialHelperEnvironment); @@ -75,14 +74,14 @@ public String getAuthenticationType() { public Map> getRequestMetadata(URI uri) throws IOException { Preconditions.checkNotNull(uri); - ImmutableMap> credentials; + GetCredentialsResponse response; try { - credentials = credentialCache.get(uri, this::getCredentialsFromHelper); + response = credentialCache.get(uri, this::getCredentialsFromHelper); } catch (WrappedIOException e) { throw e.getWrapped(); } - if (credentials != null) { - return (Map) credentials; + if (response != null) { + return (Map) response; } if (fallbackCredentials.isPresent()) { @@ -93,7 +92,7 @@ public Map> getRequestMetadata(URI uri) throws IOException } @Nullable - private ImmutableMap> getCredentialsFromHelper(URI uri) { + private GetCredentialsResponse getCredentialsFromHelper(URI uri) { Preconditions.checkNotNull(uri); Optional maybeCredentialHelper = @@ -113,7 +112,7 @@ private ImmutableMap> getCredentialsFromHelper(URI return null; } - return response.getHeaders(); + return response; } @Override 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 ba880430b79bb4..56c9428e6b93ef 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 @@ -17,24 +17,23 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; 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; /** A module whose sole purpose is to hold the credential cache which is shared by other modules. */ public class CredentialModule extends BlazeModule { - private final Cache>> credentialCache = + private final CredentialCacheExpiry cacheExpiry = new CredentialCacheExpiry(); + private final Cache credentialCache = Caffeine.newBuilder() - .expireAfterWrite(Duration.ZERO) .ticker(SystemMillisTicker.INSTANCE) + .expireAfter(cacheExpiry) .build(); /** Returns the credential cache. */ - public Cache>> getCredentialCache() { + public Cache getCredentialCache() { return credentialCache; } @@ -47,11 +46,7 @@ public Iterable> getCommonCommandOptions() { public void beforeCommand(CommandEnvironment env) { // Update the cache expiration policy according to the command options. AuthAndTLSOptions authAndTlsOptions = env.getOptions().getOptions(AuthAndTLSOptions.class); - credentialCache - .policy() - .expireAfterWrite() - .get() - .setExpiresAfter(authAndTlsOptions.credentialHelperCacheTimeout); + cacheExpiry.setDefaultCacheDuration(authAndTlsOptions.credentialHelperCacheTimeout); // Clear the cache on clean. if (env.getCommand().name().equals("clean")) { diff --git a/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/GetCredentialsResponse.java b/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/GetCredentialsResponse.java index 6d450e001fb682..b5663970969ecc 100644 --- a/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/GetCredentialsResponse.java +++ b/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/GetCredentialsResponse.java @@ -26,22 +26,36 @@ import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; import java.io.IOException; +import java.time.DateTimeException; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.ResolverStyle; import java.util.Locale; import java.util.Map; +import java.util.Optional; /** * Response from the {@code get} command of the Credential * Helper Protocol. + * + *

See the specification. */ @AutoValue @AutoValue.CopyAnnotations @Immutable @JsonAdapter(GetCredentialsResponse.GsonTypeAdapter.class) public abstract class GetCredentialsResponse { + public static final DateTimeFormatter RFC_3339_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX").withZone(ZoneId.from(ZoneOffset.UTC)).withResolverStyle(ResolverStyle.LENIENT); + /** Returns the headers to attach to the request. */ public abstract ImmutableMap> getHeaders(); + /** Returns the time the credentials expire and must be revalidated. */ + public abstract Optional getExpires(); + /** Returns a new builder for {@link GetCredentialsRequest}. */ public static Builder newBuilder() { return new AutoValue_GetCredentialsResponse.Builder(); @@ -52,6 +66,8 @@ public static Builder newBuilder() { public abstract static class Builder { public abstract ImmutableMap.Builder> headersBuilder(); + public abstract Builder setExpires(Instant instant); + /** Returns the newly constructed {@link GetCredentialsResponse}. */ public abstract GetCredentialsResponse build(); } @@ -80,6 +96,13 @@ public void write(JsonWriter writer, GetCredentialsResponse response) throws IOE } writer.endObject(); } + + var expires = response.getExpires(); + if (expires.isPresent()) { + writer.name("expires"); + writer.value(RFC_3339_FORMATTER.format(expires.get())); + } + writer.endObject(); } @@ -141,6 +164,25 @@ public GetCredentialsResponse read(JsonReader reader) throws IOException { reader.endObject(); break; + case "expires": + if (reader.peek() != JsonToken.STRING) { + throw new JsonSyntaxException( + String.format( + Locale.US, + "Expected value of 'expires' to be a string, got %s", + reader.peek())); + } + try { + response.setExpires(Instant.from(RFC_3339_FORMATTER.parse(reader.nextString()))); + } catch (DateTimeException e) { + throw new JsonSyntaxException( + String.format( + Locale.US, + "Expected value of 'expires' to be a RFC 3339 formatted timestamp: %s", + e.getMessage())); + } + break; + default: // We intentionally ignore unknown keys to achieve forward compatibility with responses // coming from newer tools. diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java index d06ae714ca8732..280895c2b74575 100644 --- a/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java +++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java @@ -25,7 +25,6 @@ import com.google.common.base.Strings; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.ListeningScheduledExecutorService; import com.google.common.util.concurrent.MoreExecutors; @@ -44,6 +43,7 @@ import com.google.devtools.build.lib.authandtls.GoogleAuthUtils; import com.google.devtools.build.lib.authandtls.credentialhelper.CredentialHelperEnvironment; import com.google.devtools.build.lib.authandtls.credentialhelper.CredentialModule; +import com.google.devtools.build.lib.authandtls.credentialhelper.GetCredentialsResponse; import com.google.devtools.build.lib.bazel.repository.downloader.Downloader; import com.google.devtools.build.lib.bazel.repository.downloader.HttpDownloader; import com.google.devtools.build.lib.buildeventstream.BuildEventArtifactUploader; @@ -1100,7 +1100,7 @@ RemoteActionContextProvider getActionContextProvider() { @VisibleForTesting static Credentials createCredentials( CredentialHelperEnvironment credentialHelperEnvironment, - Cache>> credentialCache, + Cache credentialCache, CommandLinePathFactory commandLinePathFactory, FileSystem fileSystem, AuthAndTLSOptions authAndTlsOptions, diff --git a/src/test/java/com/google/devtools/build/lib/authandtls/credentialhelper/GetCredentialsResponseTest.java b/src/test/java/com/google/devtools/build/lib/authandtls/credentialhelper/GetCredentialsResponseTest.java index 8d1d8f56c34ef2..8e4b84cf00c929 100644 --- a/src/test/java/com/google/devtools/build/lib/authandtls/credentialhelper/GetCredentialsResponseTest.java +++ b/src/test/java/com/google/devtools/build/lib/authandtls/credentialhelper/GetCredentialsResponseTest.java @@ -24,6 +24,8 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import java.time.Instant; + /** Tests for {@link GetCredentialsResponse}. */ @RunWith(JUnit4.class) public class GetCredentialsResponseTest { @@ -138,6 +140,33 @@ public void parseInvalidHeadersValue() { () -> GSON.fromJson("{\"headers\": {\"a\": [{}, \"a\"]}}", GetCredentialsResponse.class)); } + @Test + public void parseExpires() { + assertThat(GSON.fromJson("{\"expires\": \"1970-09-29T11:46:29Z\"}", GetCredentialsResponse.class).getExpires()).hasValue(Instant.ofEpochSecond(23456789)); + assertThat(GSON.fromJson("{\"expires\": \"1970-09-29T11:46:29+00:00\"}", GetCredentialsResponse.class).getExpires()).hasValue(Instant.ofEpochSecond(23456789)); + assertThat(GSON.fromJson("{\"expires\": \"1970-09-29T13:46:29+02:00\"}", GetCredentialsResponse.class).getExpires()).hasValue(Instant.ofEpochSecond(23456789)); + assertThat(GSON.fromJson("{\"expires\": \"1970-09-28T23:46:29-12:00\"}", GetCredentialsResponse.class).getExpires()).hasValue(Instant.ofEpochSecond(23456789)); + } + + @Test + public void parseInvalidExpires() { + assertThrows( + JsonSyntaxException.class, + () -> GSON.fromJson("{\"expires\": null}", GetCredentialsResponse.class)); + assertThrows( + JsonSyntaxException.class, + () -> GSON.fromJson("{\"expires\": \"foo\"}", GetCredentialsResponse.class)); + assertThrows( + JsonSyntaxException.class, + () -> GSON.fromJson("{\"expires\": []}", GetCredentialsResponse.class)); + assertThrows( + JsonSyntaxException.class, + () -> GSON.fromJson("{\"expires\": 1}", GetCredentialsResponse.class)); + assertThrows( + JsonSyntaxException.class, + () -> GSON.fromJson("{\"expires\": {}}", GetCredentialsResponse.class)); + } + @Test public void serializeEmptyHeaders() { GetCredentialsResponse expectedResponse = GetCredentialsResponse.newBuilder().build(); @@ -146,7 +175,7 @@ public void serializeEmptyHeaders() { @Test public void roundTrip() { - GetCredentialsResponse.Builder expectedResponseBuilder = GetCredentialsResponse.newBuilder(); + GetCredentialsResponse.Builder expectedResponseBuilder = GetCredentialsResponse.newBuilder().setExpires(Instant.ofEpochSecond(123456789)); expectedResponseBuilder.headersBuilder().put("a", ImmutableList.of()); expectedResponseBuilder.headersBuilder().put("b", ImmutableList.of("b")); expectedResponseBuilder.headersBuilder().put("c", ImmutableList.of("c", "c")); diff --git a/src/test/java/com/google/devtools/build/lib/remote/RemoteModuleTest.java b/src/test/java/com/google/devtools/build/lib/remote/RemoteModuleTest.java index b8a7c919973e07..7ce571d85ed0fb 100644 --- a/src/test/java/com/google/devtools/build/lib/remote/RemoteModuleTest.java +++ b/src/test/java/com/google/devtools/build/lib/remote/RemoteModuleTest.java @@ -38,6 +38,7 @@ import com.google.devtools.build.lib.authandtls.AuthAndTLSOptions; import com.google.devtools.build.lib.authandtls.credentialhelper.CredentialHelperEnvironment; import com.google.devtools.build.lib.authandtls.credentialhelper.CredentialModule; +import com.google.devtools.build.lib.authandtls.credentialhelper.GetCredentialsResponse; import com.google.devtools.build.lib.events.Reporter; import com.google.devtools.build.lib.exec.BinTools; import com.google.devtools.build.lib.exec.ExecutionOptions; @@ -393,7 +394,7 @@ public void testNetrc_netrcWithoutRemoteCache() throws Exception { Scratch scratch = new Scratch(fileSystem); scratch.file(netrc, "machine foo.example.org login baruser password barpass"); AuthAndTLSOptions authAndTLSOptions = Options.getDefaults(AuthAndTLSOptions.class); - Cache>> credentialCache = + Cache credentialCache = Caffeine.newBuilder().build(); Credentials credentials = From 5d5429db4bb80643a34c5a0e151022b274953b59 Mon Sep 17 00:00:00 2001 From: Yannic Bonenberger Date: Thu, 8 Feb 2024 01:38:23 +0100 Subject: [PATCH 2/4] fix --- .../credentialhelper/CredentialHelperCredentials.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/CredentialHelperCredentials.java b/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/CredentialHelperCredentials.java index f38df24472eeb7..602490ee5016bd 100644 --- a/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/CredentialHelperCredentials.java +++ b/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/CredentialHelperCredentials.java @@ -81,7 +81,7 @@ public Map> getRequestMetadata(URI uri) throws IOException throw e.getWrapped(); } if (response != null) { - return (Map) response; + return (Map) response.getHeaders(); } if (fallbackCredentials.isPresent()) { From 044586db1fb1928dafe3d8886bea6f8cc3827a35 Mon Sep 17 00:00:00 2001 From: Yannic Bonenberger Date: Wed, 14 Feb 2024 21:43:50 +0100 Subject: [PATCH 3/4] fix --- .../google/devtools/build/lib/authandtls/AuthAndTLSOptions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b4d0ef344744df..6d30ed8ed723ea 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 @@ -180,7 +180,7 @@ public class AuthAndTLSOptions extends OptionsBase { documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = - "The duration for which credentials supplied by a credential helper are cached.\n\n" + "The default duration for which credentials supplied by a credential helper are cached if the helper does not provide when the credentials expire.\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.") From 966ec68f469aaa7cfbd298269360e5bce9b27c98 Mon Sep 17 00:00:00 2001 From: Yannic Bonenberger Date: Thu, 15 Feb 2024 21:05:48 +0100 Subject: [PATCH 4/4] drop --- .../devtools/build/lib/authandtls/AuthAndTLSOptions.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 6d30ed8ed723ea..b262e88f8a0cba 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 @@ -179,11 +179,7 @@ public class AuthAndTLSOptions extends OptionsBase { converter = DurationConverter.class, documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, - help = - "The default duration for which credentials supplied by a credential helper are cached if the helper does not provide when the credentials expire.\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.") + help = "The default duration for which credentials supplied by a credential helper are cached if the helper does not provide when the credentials expire.") public Duration credentialHelperCacheTimeout; /** One of the values of the `--credential_helper` flag. */