diff --git a/xds/BUILD.bazel b/xds/BUILD.bazel index 203fa2c3d71..2fd01b276e7 100644 --- a/xds/BUILD.bazel +++ b/xds/BUILD.bazel @@ -33,6 +33,8 @@ java_library( "@com_google_protobuf//:protobuf_java", "@com_google_protobuf//:protobuf_java_util", "@maven//:com_google_auth_google_auth_library_oauth2_http", + "@maven//:com_google_http_client_google_http_client", + "@maven//:com_google_http_client_google_http_client_gson", artifact("com.google.code.findbugs:jsr305"), artifact("com.google.code.gson:gson"), artifact("com.google.errorprone:error_prone_annotations"), diff --git a/xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java b/xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java index f61fab42cae..382e8650833 100644 --- a/xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java +++ b/xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java @@ -18,7 +18,10 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; +import io.grpc.CallCredentials; import io.grpc.ChannelCredentials; +import io.grpc.CompositeCallCredentials; +import io.grpc.internal.GrpcUtil; import io.grpc.internal.JsonUtil; import io.grpc.xds.client.BootstrapperImpl; import io.grpc.xds.client.XdsInitializationException; @@ -33,6 +36,8 @@ class GrpcBootstrapperImpl extends BootstrapperImpl { private static final String BOOTSTRAP_PATH_SYS_PROPERTY = "io.grpc.xds.bootstrap"; private static final String BOOTSTRAP_CONFIG_SYS_ENV_VAR = "GRPC_XDS_BOOTSTRAP_CONFIG"; private static final String BOOTSTRAP_CONFIG_SYS_PROPERTY = "io.grpc.xds.bootstrapConfig"; + private static final String GRPC_EXPERIMENTAL_XDS_BOOTSTRAP_CALL_CREDS = + "GRPC_EXPERIMENTAL_XDS_BOOTSTRAP_CALL_CREDS"; @VisibleForTesting String bootstrapPathFromEnvVar = System.getenv(BOOTSTRAP_PATH_SYS_ENV_VAR); @VisibleForTesting @@ -41,6 +46,9 @@ class GrpcBootstrapperImpl extends BootstrapperImpl { String bootstrapConfigFromEnvVar = System.getenv(BOOTSTRAP_CONFIG_SYS_ENV_VAR); @VisibleForTesting String bootstrapConfigFromSysProp = System.getProperty(BOOTSTRAP_CONFIG_SYS_PROPERTY); + @VisibleForTesting + static boolean xdsBootstrapCallCredsEnabled = GrpcUtil.getFlag( + GRPC_EXPERIMENTAL_XDS_BOOTSTRAP_CALL_CREDS, false); GrpcBootstrapperImpl() { super(); @@ -90,7 +98,7 @@ protected String getJsonContent() throws XdsInitializationException, IOException } @Override - protected Object getImplSpecificConfig(Map serverConfig, String serverUri) + protected Object getImplSpecificChannelCredConfig(Map serverConfig, String serverUri) throws XdsInitializationException { return getChannelCredentials(serverConfig, serverUri); } @@ -135,4 +143,58 @@ private static ChannelCredentials parseChannelCredentials(List> j } return null; } + + @Override + protected Object getImplSpecificCallCredConfig(Map serverConfig, String serverUri) + throws XdsInitializationException { + return getCallCredentials(serverConfig, serverUri); + } + + private static CallCredentials getCallCredentials(Map serverConfig, + String serverUri) + throws XdsInitializationException { + List rawCallCredsList = JsonUtil.getList(serverConfig, "call_creds"); + if (rawCallCredsList == null || rawCallCredsList.isEmpty()) { + return null; + } + CallCredentials callCredentials = + parseCallCredentials(JsonUtil.checkObjectList(rawCallCredsList), serverUri); + return callCredentials; + } + + @Nullable + private static CallCredentials parseCallCredentials(List> jsonList, + String serverUri) + throws XdsInitializationException { + CallCredentials callCredentials = null; + if (xdsBootstrapCallCredsEnabled) { + for (Map callCreds : jsonList) { + String type = JsonUtil.getString(callCreds, "type"); + if (type != null) { + XdsCredentialsProvider provider = XdsCredentialsRegistry.getDefaultRegistry() + .getProvider(type); + if (provider != null) { + Map config = JsonUtil.getObject(callCreds, "config"); + if (config == null) { + config = ImmutableMap.of(); + } + CallCredentials parsedCallCredentials = provider.newCallCredentials(config); + if (parsedCallCredentials == null) { + throw new XdsInitializationException( + "Invalid bootstrap: server " + serverUri + " with invalid 'config' for " + type + + " 'call_creds'"); + } + + if (callCredentials == null) { + callCredentials = parsedCallCredentials; + } else { + callCredentials = new CompositeCallCredentials( + callCredentials, parsedCallCredentials); + } + } + } + } + } + return callCredentials; + } } diff --git a/xds/src/main/java/io/grpc/xds/GrpcXdsTransportFactory.java b/xds/src/main/java/io/grpc/xds/GrpcXdsTransportFactory.java index 0da51bf47f7..d795d4435d1 100644 --- a/xds/src/main/java/io/grpc/xds/GrpcXdsTransportFactory.java +++ b/xds/src/main/java/io/grpc/xds/GrpcXdsTransportFactory.java @@ -23,6 +23,8 @@ import io.grpc.CallOptions; import io.grpc.ChannelCredentials; import io.grpc.ClientCall; +import io.grpc.CompositeCallCredentials; +import io.grpc.CompositeChannelCredentials; import io.grpc.Context; import io.grpc.Grpc; import io.grpc.ManagedChannel; @@ -68,11 +70,23 @@ public GrpcXdsTransport(ManagedChannel channel) { public GrpcXdsTransport(Bootstrapper.ServerInfo serverInfo, CallCredentials callCredentials) { String target = serverInfo.target(); - ChannelCredentials channelCredentials = (ChannelCredentials) serverInfo.implSpecificConfig(); - this.channel = Grpc.newChannelBuilder(target, channelCredentials) + Object implSpecificConfig = serverInfo.implSpecificConfig(); + + this.channel = Grpc.newChannelBuilder(target, (ChannelCredentials) implSpecificConfig) .keepAliveTime(5, TimeUnit.MINUTES) .build(); - this.callCredentials = callCredentials; + + if (callCredentials != null && implSpecificConfig instanceof CompositeChannelCredentials) { + this.callCredentials = + new CompositeCallCredentials( + callCredentials, + ((CompositeChannelCredentials) implSpecificConfig).getCallCredentials()); + } else if (implSpecificConfig instanceof CompositeChannelCredentials) { + this.callCredentials = + ((CompositeChannelCredentials) implSpecificConfig).getCallCredentials(); + } else { + this.callCredentials = callCredentials; + } } @VisibleForTesting diff --git a/xds/src/main/java/io/grpc/xds/JwtTokenFileCallCredentials.java b/xds/src/main/java/io/grpc/xds/JwtTokenFileCallCredentials.java new file mode 100644 index 00000000000..9480b075c42 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/JwtTokenFileCallCredentials.java @@ -0,0 +1,66 @@ +/* + * Copyright 2025 The gRPC Authors + * + * 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 io.grpc.xds; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.json.gson.GsonFactory; +import com.google.api.client.json.webtoken.JsonWebSignature; +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.OAuth2Credentials; +import com.google.common.io.Files; +import io.grpc.CallCredentials; +import io.grpc.auth.MoreCallCredentials; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Date; + +/** + * JWT token file call credentials. + * See gRFC A97 (https://github.com/grpc/proposal/pull/492). + */ +public final class JwtTokenFileCallCredentials extends OAuth2Credentials { + private static final long serialVersionUID = 0L; + private final String path; + + private JwtTokenFileCallCredentials(String path) { + this.path = checkNotNull(path, "path"); + } + + @Override + public AccessToken refreshAccessToken() throws IOException { + String tokenString = new String(Files.toByteArray(new File(path)), StandardCharsets.UTF_8); + Long expTime = JsonWebSignature.parse(new GsonFactory(), tokenString) + .getPayload() + .getExpirationTimeSeconds(); + if (expTime == null) { + throw new IOException("No expiration time found for JWT token"); + } + + return AccessToken.newBuilder() + .setTokenValue(tokenString) + .setExpirationTime(new Date(expTime * 1000L)) + .build(); + } + + // using {@link MoreCallCredentials} adapter to be compatible with {@link CallCredentials} iface + public static CallCredentials create(String path) { + JwtTokenFileCallCredentials jwtTokenFileCallCredentials = new JwtTokenFileCallCredentials(path); + return MoreCallCredentials.from(jwtTokenFileCallCredentials); + } +} diff --git a/xds/src/main/java/io/grpc/xds/XdsCredentialsProvider.java b/xds/src/main/java/io/grpc/xds/XdsCredentialsProvider.java index e9466f37a0a..33576d826f5 100644 --- a/xds/src/main/java/io/grpc/xds/XdsCredentialsProvider.java +++ b/xds/src/main/java/io/grpc/xds/XdsCredentialsProvider.java @@ -16,6 +16,7 @@ package io.grpc.xds; +import io.grpc.CallCredentials; import io.grpc.ChannelCredentials; import io.grpc.Internal; import java.util.Map; @@ -49,6 +50,17 @@ public abstract class XdsCredentialsProvider { */ protected abstract ChannelCredentials newChannelCredentials(Map jsonConfig); + /** + * Creates a {@link CallCredentials} from the given jsonConfig, or + * {@code null} if the given config is invalid. The provider is free to ignore + * the config if it's not needed for producing the call credentials. + * + * @param jsonConfig json config that can be consumed by the provider to create + * the call credentials + * + */ + protected abstract CallCredentials newCallCredentials(Map jsonConfig); + /** * Returns the xDS credential name associated with this provider which makes it selectable * via {@link XdsCredentialsRegistry#getProvider}. This is called only when the class is loaded. diff --git a/xds/src/main/java/io/grpc/xds/XdsCredentialsRegistry.java b/xds/src/main/java/io/grpc/xds/XdsCredentialsRegistry.java index 9dfefaf1a65..5c57473cf72 100644 --- a/xds/src/main/java/io/grpc/xds/XdsCredentialsRegistry.java +++ b/xds/src/main/java/io/grpc/xds/XdsCredentialsRegistry.java @@ -114,7 +114,7 @@ public static synchronized XdsCredentialsRegistry getDefaultRegistry() { new XdsCredentialsProviderPriorityAccessor()); if (providerList.isEmpty()) { logger.warning("No XdsCredsRegistry found via ServiceLoader, including for GoogleDefault, " - + "TLS and Insecure. This is probably due to a broken build."); + + "TLS, Insecure and JWT token file. This is probably due to a broken build."); } instance = new XdsCredentialsRegistry(); for (XdsCredentialsProvider provider : providerList) { @@ -170,7 +170,13 @@ static List> getHardCodedClasses() { } catch (ClassNotFoundException e) { logger.log(Level.WARNING, "Unable to find TlsXdsCredentialsProvider", e); } - + + try { + list.add(Class.forName("io.grpc.xds.internal.JwtTokenFileXdsCredentialsProvider")); + } catch (ClassNotFoundException e) { + logger.log(Level.WARNING, "Unable to find JwtTokenFileXdsCredentialsProvider", e); + } + return Collections.unmodifiableList(list); } diff --git a/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java b/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java index 423c1a118e8..48df6401f69 100644 --- a/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java +++ b/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java @@ -19,6 +19,9 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import io.grpc.CallCredentials; +import io.grpc.ChannelCredentials; +import io.grpc.CompositeChannelCredentials; import io.grpc.Internal; import io.grpc.InternalLogId; import io.grpc.internal.GrpcUtil; @@ -76,9 +79,13 @@ protected BootstrapperImpl() { protected abstract String getJsonContent() throws IOException, XdsInitializationException; - protected abstract Object getImplSpecificConfig(Map serverConfig, String serverUri) + protected abstract Object getImplSpecificChannelCredConfig( + Map serverConfig, String serverUri) throws XdsInitializationException; + protected abstract Object getImplSpecificCallCredConfig( + Map serverConfig, String serverUri) + throws XdsInitializationException; /** * Reads and parses bootstrap config. The config is expected to be in JSON format. @@ -253,7 +260,9 @@ private List parseServerInfos(List rawServerConfigs, XdsLogger lo } logger.log(XdsLogLevel.INFO, "xDS server URI: {0}", serverUri); - Object implSpecificConfig = getImplSpecificConfig(serverConfig, serverUri); + Object implSpecificChannelCredConfig = + getImplSpecificChannelCredConfig(serverConfig, serverUri); + Object implSpecificCallCredConfig = getImplSpecificCallCredConfig(serverConfig, serverUri); boolean resourceTimerIsTransientError = false; boolean ignoreResourceDeletion = false; @@ -267,10 +276,17 @@ private List parseServerInfos(List rawServerConfigs, XdsLogger lo && serverFeatures.contains(SERVER_FEATURE_RESOURCE_TIMER_IS_TRANSIENT_ERROR); } servers.add( - ServerInfo.create(serverUri, implSpecificConfig, ignoreResourceDeletion, - serverFeatures != null - && serverFeatures.contains(SERVER_FEATURE_TRUSTED_XDS_SERVER), - resourceTimerIsTransientError)); + ServerInfo.create( + serverUri, + (implSpecificCallCredConfig != null) + ? CompositeChannelCredentials.create( + (ChannelCredentials) implSpecificChannelCredConfig, + (CallCredentials) implSpecificCallCredConfig) + : implSpecificChannelCredConfig, + ignoreResourceDeletion, + serverFeatures != null + && serverFeatures.contains(SERVER_FEATURE_TRUSTED_XDS_SERVER), + resourceTimerIsTransientError)); } return servers.build(); } diff --git a/xds/src/main/java/io/grpc/xds/internal/GoogleDefaultXdsCredentialsProvider.java b/xds/src/main/java/io/grpc/xds/internal/GoogleDefaultXdsCredentialsProvider.java index 383c19b6665..4240dcb396f 100644 --- a/xds/src/main/java/io/grpc/xds/internal/GoogleDefaultXdsCredentialsProvider.java +++ b/xds/src/main/java/io/grpc/xds/internal/GoogleDefaultXdsCredentialsProvider.java @@ -16,6 +16,7 @@ package io.grpc.xds.internal; +import io.grpc.CallCredentials; import io.grpc.ChannelCredentials; import io.grpc.alts.GoogleDefaultChannelCredentials; import io.grpc.xds.XdsCredentialsProvider; @@ -33,6 +34,11 @@ protected ChannelCredentials newChannelCredentials(Map jsonConfig) { return GoogleDefaultChannelCredentials.create(); } + @Override + protected CallCredentials newCallCredentials(Map jsonConfig) { + return null; + } + @Override protected String getName() { return CREDS_NAME; diff --git a/xds/src/main/java/io/grpc/xds/internal/InsecureXdsCredentialsProvider.java b/xds/src/main/java/io/grpc/xds/internal/InsecureXdsCredentialsProvider.java index d57cfe2f238..31dfe65fc9c 100644 --- a/xds/src/main/java/io/grpc/xds/internal/InsecureXdsCredentialsProvider.java +++ b/xds/src/main/java/io/grpc/xds/internal/InsecureXdsCredentialsProvider.java @@ -16,6 +16,7 @@ package io.grpc.xds.internal; +import io.grpc.CallCredentials; import io.grpc.ChannelCredentials; import io.grpc.InsecureChannelCredentials; import io.grpc.xds.XdsCredentialsProvider; @@ -33,6 +34,11 @@ protected ChannelCredentials newChannelCredentials(Map jsonConfig) { return InsecureChannelCredentials.create(); } + @Override + protected CallCredentials newCallCredentials(Map jsonConfig) { + return null; + } + @Override protected String getName() { return CREDS_NAME; diff --git a/xds/src/main/java/io/grpc/xds/internal/JwtTokenFileXdsCredentialsProvider.java b/xds/src/main/java/io/grpc/xds/internal/JwtTokenFileXdsCredentialsProvider.java new file mode 100644 index 00000000000..78a6f2eae54 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/JwtTokenFileXdsCredentialsProvider.java @@ -0,0 +1,68 @@ +/* + * Copyright 2025 The gRPC Authors + * + * 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 io.grpc.xds.internal; + +import io.grpc.CallCredentials; +import io.grpc.ChannelCredentials; +import io.grpc.internal.JsonUtil; +import io.grpc.xds.JwtTokenFileCallCredentials; +import io.grpc.xds.XdsCredentialsProvider; +import java.io.File; +import java.util.Map; + +/** + * A wrapper class that supports {@link JwtTokenFileXdsCredentialsProvider} for + * Xds by implementing {@link XdsCredentialsProvider}. + */ +public final class JwtTokenFileXdsCredentialsProvider extends XdsCredentialsProvider { + private static final String CREDS_NAME = "jwt_token_file"; + + @Override + protected ChannelCredentials newChannelCredentials(Map jsonConfig) { + return null; + } + + @Override + protected CallCredentials newCallCredentials(Map jsonConfig) { + if (jsonConfig == null) { + return null; + } + + String jwtTokenPath = JsonUtil.getString(jsonConfig, getName()); + if (jwtTokenPath == null || !new File(jwtTokenPath).isFile()) { + return null; + } + + return JwtTokenFileCallCredentials.create(jwtTokenPath); + } + + @Override + protected String getName() { + return CREDS_NAME; + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public int priority() { + return 5; + } + +} diff --git a/xds/src/main/java/io/grpc/xds/internal/TlsXdsCredentialsProvider.java b/xds/src/main/java/io/grpc/xds/internal/TlsXdsCredentialsProvider.java index f4d26a83795..364eb7c7016 100644 --- a/xds/src/main/java/io/grpc/xds/internal/TlsXdsCredentialsProvider.java +++ b/xds/src/main/java/io/grpc/xds/internal/TlsXdsCredentialsProvider.java @@ -16,6 +16,7 @@ package io.grpc.xds.internal; +import io.grpc.CallCredentials; import io.grpc.ChannelCredentials; import io.grpc.TlsChannelCredentials; import io.grpc.xds.XdsCredentialsProvider; @@ -33,6 +34,11 @@ protected ChannelCredentials newChannelCredentials(Map jsonConfig) { return TlsChannelCredentials.create(); } + @Override + protected CallCredentials newCallCredentials(Map jsonConfig) { + return null; + } + @Override protected String getName() { return CREDS_NAME; diff --git a/xds/src/main/resources/META-INF/services/io.grpc.xds.XdsCredentialsProvider b/xds/src/main/resources/META-INF/services/io.grpc.xds.XdsCredentialsProvider index a51cd114737..b46ef34dfaf 100644 --- a/xds/src/main/resources/META-INF/services/io.grpc.xds.XdsCredentialsProvider +++ b/xds/src/main/resources/META-INF/services/io.grpc.xds.XdsCredentialsProvider @@ -1,3 +1,4 @@ io.grpc.xds.internal.GoogleDefaultXdsCredentialsProvider io.grpc.xds.internal.InsecureXdsCredentialsProvider +io.grpc.xds.internal.JwtTokenFileXdsCredentialsProvider io.grpc.xds.internal.TlsXdsCredentialsProvider \ No newline at end of file diff --git a/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java b/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java index 3f93cc6f191..3667e3f882b 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java @@ -17,6 +17,7 @@ package io.grpc.xds; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; @@ -24,6 +25,9 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; +import io.grpc.CallCredentials; +import io.grpc.CompositeCallCredentials; +import io.grpc.CompositeChannelCredentials; import io.grpc.InsecureChannelCredentials; import io.grpc.TlsChannelCredentials; import io.grpc.internal.GrpcUtil; @@ -37,13 +41,16 @@ import io.grpc.xds.client.EnvoyProtoData.Node; import io.grpc.xds.client.Locality; import io.grpc.xds.client.XdsInitializationException; +import java.io.File; import java.io.IOException; import java.util.List; import java.util.Map; import org.junit.After; import org.junit.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -60,11 +67,16 @@ public class GrpcBootstrapperImplTest { private String originalBootstrapConfigFromEnvVar; private String originalBootstrapConfigFromSysProp; private boolean originalExperimentalXdsFallbackFlag; + private boolean originalExperimentalXdsBootstrapCallCredsFlag; + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); @Before public void setUp() { saveEnvironment(); originalExperimentalXdsFallbackFlag = CommonBootstrapperTestUtils.setEnableXdsFallback(true); + GrpcBootstrapperImpl.xdsBootstrapCallCredsEnabled = true; bootstrapper.bootstrapPathFromEnvVar = BOOTSTRAP_FILE_PATH; } @@ -73,6 +85,8 @@ private void saveEnvironment() { originalBootstrapPathFromSysProp = bootstrapper.bootstrapPathFromSysProp; originalBootstrapConfigFromEnvVar = bootstrapper.bootstrapConfigFromEnvVar; originalBootstrapConfigFromSysProp = bootstrapper.bootstrapConfigFromSysProp; + originalExperimentalXdsBootstrapCallCredsFlag = + GrpcBootstrapperImpl.xdsBootstrapCallCredsEnabled; } @After @@ -82,6 +96,8 @@ public void restoreEnvironment() { bootstrapper.bootstrapConfigFromEnvVar = originalBootstrapConfigFromEnvVar; bootstrapper.bootstrapConfigFromSysProp = originalBootstrapConfigFromSysProp; CommonBootstrapperTestUtils.setEnableXdsFallback(originalExperimentalXdsFallbackFlag); + GrpcBootstrapperImpl.xdsBootstrapCallCredsEnabled = + originalExperimentalXdsBootstrapCallCredsFlag; } @Test @@ -115,7 +131,8 @@ public void parseBootstrap_singleXdsServer() throws XdsInitializationException { assertThat(info.servers()).hasSize(1); ServerInfo serverInfo = Iterables.getOnlyElement(info.servers()); assertThat(serverInfo.target()).isEqualTo(SERVER_URI); - assertThat(serverInfo.implSpecificConfig()).isInstanceOf(InsecureChannelCredentials.class); + assertThat(serverInfo.implSpecificConfig()).isInstanceOf( + InsecureChannelCredentials.class); assertThat(info.node()).isEqualTo( getNodeBuilder() .setId("ENVOY_NODE_ID") @@ -217,7 +234,8 @@ public void parseBootstrap_IgnoreIrrelevantFields() throws XdsInitializationExce assertThat(info.servers()).hasSize(1); ServerInfo serverInfo = Iterables.getOnlyElement(info.servers()); assertThat(serverInfo.target()).isEqualTo(SERVER_URI); - assertThat(serverInfo.implSpecificConfig()).isInstanceOf(InsecureChannelCredentials.class); + assertThat(serverInfo.implSpecificConfig()).isInstanceOf( + InsecureChannelCredentials.class); assertThat(info.node()).isEqualTo( getNodeBuilder() .setId("ENVOY_NODE_ID") @@ -288,7 +306,8 @@ public void parseBootstrap_useFirstSupportedChannelCredentials() assertThat(info.servers()).hasSize(1); ServerInfo serverInfo = Iterables.getOnlyElement(info.servers()); assertThat(serverInfo.target()).isEqualTo(SERVER_URI); - assertThat(serverInfo.implSpecificConfig()).isInstanceOf(InsecureChannelCredentials.class); + assertThat(serverInfo.implSpecificConfig()).isInstanceOf( + InsecureChannelCredentials.class); assertThat(info.node()).isEqualTo(getNodeBuilder().build()); } @@ -583,7 +602,8 @@ public void useV2ProtocolByDefault() throws XdsInitializationException { BootstrapInfo info = bootstrapper.bootstrap(); ServerInfo serverInfo = Iterables.getOnlyElement(info.servers()); assertThat(serverInfo.target()).isEqualTo(SERVER_URI); - assertThat(serverInfo.implSpecificConfig()).isInstanceOf(InsecureChannelCredentials.class); + assertThat(serverInfo.implSpecificConfig()).isInstanceOf( + InsecureChannelCredentials.class); assertThat(serverInfo.ignoreResourceDeletion()).isFalse(); } @@ -605,7 +625,8 @@ public void useV3ProtocolIfV3FeaturePresent() throws XdsInitializationException BootstrapInfo info = bootstrapper.bootstrap(); ServerInfo serverInfo = Iterables.getOnlyElement(info.servers()); assertThat(serverInfo.target()).isEqualTo(SERVER_URI); - assertThat(serverInfo.implSpecificConfig()).isInstanceOf(InsecureChannelCredentials.class); + assertThat(serverInfo.implSpecificConfig()).isInstanceOf( + InsecureChannelCredentials.class); assertThat(serverInfo.ignoreResourceDeletion()).isFalse(); } @@ -627,7 +648,8 @@ public void serverFeatureIgnoreResourceDeletion() throws XdsInitializationExcept BootstrapInfo info = bootstrapper.bootstrap(); ServerInfo serverInfo = Iterables.getOnlyElement(info.servers()); assertThat(serverInfo.target()).isEqualTo(SERVER_URI); - assertThat(serverInfo.implSpecificConfig()).isInstanceOf(InsecureChannelCredentials.class); + assertThat(serverInfo.implSpecificConfig()).isInstanceOf( + InsecureChannelCredentials.class); // Only ignore_resource_deletion feature enabled: confirm it's on, and xds_v3 is off. assertThat(serverInfo.ignoreResourceDeletion()).isTrue(); } @@ -650,7 +672,8 @@ public void serverFeatureTrustedXdsServer() throws XdsInitializationException { BootstrapInfo info = bootstrapper.bootstrap(); ServerInfo serverInfo = Iterables.getOnlyElement(info.servers()); assertThat(serverInfo.target()).isEqualTo(SERVER_URI); - assertThat(serverInfo.implSpecificConfig()).isInstanceOf(InsecureChannelCredentials.class); + assertThat(serverInfo.implSpecificConfig()).isInstanceOf( + InsecureChannelCredentials.class); assertThat(serverInfo.isTrustedXdsServer()).isTrue(); } @@ -672,7 +695,8 @@ public void serverFeatureIgnoreResourceDeletion_xdsV3() throws XdsInitialization BootstrapInfo info = bootstrapper.bootstrap(); ServerInfo serverInfo = Iterables.getOnlyElement(info.servers()); assertThat(serverInfo.target()).isEqualTo(SERVER_URI); - assertThat(serverInfo.implSpecificConfig()).isInstanceOf(InsecureChannelCredentials.class); + assertThat(serverInfo.implSpecificConfig()).isInstanceOf( + InsecureChannelCredentials.class); // ignore_resource_deletion features enabled: confirm both are on. assertThat(serverInfo.ignoreResourceDeletion()).isTrue(); } @@ -898,6 +922,134 @@ public void badFederationConfig() { } } + @Test + public void parseNotSupportedCallCredentials() throws Exception { + String rawData = "{\n" + + " \"xds_servers\": [\n" + + " {\n" + + " \"server_uri\": \"" + SERVER_URI + "\",\n" + + " \"channel_creds\": [\n" + + " {\"type\": \"insecure\"}\n" + + " ],\n" + + " \"call_creds\": [\n" + + " {\"type\": \"unknown\"}\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}"; + bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData)); + BootstrapInfo info = bootstrapper.bootstrap(); + assertThat(info.servers()).hasSize(1); + ServerInfo serverInfo = Iterables.getOnlyElement(info.servers()); + assertThat(serverInfo.implSpecificConfig()).isInstanceOf( + InsecureChannelCredentials.class); + } + + @Test + public void parseSupportedCallCredentialsWithInvalidConfig() throws Exception { + String rawData = "{\n" + + " \"xds_servers\": [\n" + + " {\n" + + " \"server_uri\": \"" + SERVER_URI + "\",\n" + + " \"channel_creds\": [\n" + + " {\"type\": \"insecure\"}\n" + + " ],\n" + + " \"call_creds\": [\n" + + " {\n" + + " \"type\": \"jwt_token_file\",\n" + + " \"config\": {}\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}"; + bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData)); + Exception ex = assertThrows(XdsInitializationException.class, () -> { + bootstrapper.bootstrap(); + }); + + String expectedMsg = "Invalid bootstrap: server " + + SERVER_URI + " with invalid 'config' for jwt_token_file 'call_creds'"; + String actualMsg = ex.getMessage(); + + assertEquals(expectedMsg, actualMsg); + } + + @Test + public void parseSupportedCallCredentialsWithJwtFileMissingConfig() throws Exception { + String rawData = "{\n" + + " \"xds_servers\": [\n" + + " {\n" + + " \"server_uri\": \"" + SERVER_URI + "\",\n" + + " \"channel_creds\": [\n" + + " {\"type\": \"insecure\"}\n" + + " ],\n" + + " \"call_creds\": [\n" + + " {\n" + + " \"type\": \"jwt_token_file\",\n" + + " \"config\": {\n" + + " \"jwt_token_file\": \"/path/to/file\"\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}"; + bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData)); + Exception ex = assertThrows(XdsInitializationException.class, () -> { + bootstrapper.bootstrap(); + }); + + String expectedMsg = "Invalid bootstrap: server " + + SERVER_URI + " with invalid 'config' for jwt_token_file 'call_creds'"; + String actualMsg = ex.getMessage(); + + assertEquals(expectedMsg, actualMsg); + } + + @Test + public void parseTwoSupportedCallCredentialsWithValidConfig() throws Exception { + File jwtToken_1 = tempFolder.newFile(new String("jwt-token-1.txt")); + File jwtToken_2 = tempFolder.newFile(new String("jwt-token-2.txt")); + + String rawData = "{\n" + + " \"xds_servers\": [\n" + + " {\n" + + " \"server_uri\": \"" + SERVER_URI + "\",\n" + + " \"channel_creds\": [\n" + + " {\"type\": \"insecure\"}\n" + + " ],\n" + + " \"call_creds\": [\n" + + " {\n" + + " \"type\": \"jwt_token_file\",\n" + + " \"config\": {\n" + + " \"jwt_token_file\": \"" + jwtToken_1.getAbsolutePath() + "\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"type\": \"jwt_token_file\",\n" + + " \"config\": {\n" + + " \"jwt_token_file\": \"" + jwtToken_2.getAbsolutePath() + "\"\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}"; + + bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData)); + BootstrapInfo info = bootstrapper.bootstrap(); + assertThat(info.servers()).hasSize(1); + ServerInfo serverInfo = Iterables.getOnlyElement(info.servers()); + assertThat(serverInfo.implSpecificConfig()).isInstanceOf(CompositeChannelCredentials.class); + CallCredentials callCredentials = + ((CompositeChannelCredentials) serverInfo.implSpecificConfig()).getCallCredentials(); + assertThat(callCredentials).isInstanceOf(CompositeCallCredentials.class); + + jwtToken_1.delete(); + jwtToken_2.delete(); + } + private static BootstrapperImpl.FileReader createFileReader( final String expectedPath, final String rawData) { return new BootstrapperImpl.FileReader() { diff --git a/xds/src/test/java/io/grpc/xds/JwtTokenFileCallCredentialsTest.java b/xds/src/test/java/io/grpc/xds/JwtTokenFileCallCredentialsTest.java new file mode 100644 index 00000000000..6e69faf01d1 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/JwtTokenFileCallCredentialsTest.java @@ -0,0 +1,127 @@ +/* + * Copyright 2025 The gRPC Authors + * + * 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 io.grpc.xds; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import com.google.auth.oauth2.AccessToken; +import com.google.common.truth.Truth; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.time.Instant; +import java.util.Date; +import java.util.concurrent.TimeUnit; +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link JwtTokenFileCallCredentials}. */ +@RunWith(Enclosed.class) +public class JwtTokenFileCallCredentialsTest { + @RunWith(JUnit4.class) + public static class WithEmptyJwtTokenTest { + private File jwtTokenFile; + private JwtTokenFileCallCredentials unit; + + @Before + public void setUp() throws Exception { + this.jwtTokenFile = JwtTokenFileTestUtils.createEmptyJwtToken(); + + Constructor ctor = + JwtTokenFileCallCredentials.class.getDeclaredConstructor(String.class); + ctor.setAccessible(true); + this.unit = ctor.newInstance(jwtTokenFile.toString()); + } + + @Test + public void givenJwtTokenFileEmpty_WhenTokenRefreshed_ExpectException() { + assertThrows(IllegalArgumentException.class, () -> { + unit.refreshAccessToken(); + }); + } + } + + @RunWith(JUnit4.class) + public static class WithInvalidJwtTokenTest { + private File jwtTokenFile; + private JwtTokenFileCallCredentials unit; + + @Before + public void setUp() throws Exception { + this.jwtTokenFile = JwtTokenFileTestUtils.createJwtTokenWithoutExpiration(); + + Constructor ctor = + JwtTokenFileCallCredentials.class.getDeclaredConstructor(String.class); + ctor.setAccessible(true); + this.unit = ctor.newInstance(jwtTokenFile.toString()); + } + + @Test + public void givenJwtTokenFileWithoutExpiration_WhenTokenRefreshed_ExpectException() + throws Exception { + Exception ex = assertThrows(IOException.class, () -> { + unit.refreshAccessToken(); + }); + + String expectedMsg = "No expiration time found for JWT token"; + String actualMsg = ex.getMessage(); + + assertEquals(expectedMsg, actualMsg); + } + } + + @RunWith(JUnit4.class) + public static class WithValidJwtTokenTest { + private File jwtTokenFile; + private JwtTokenFileCallCredentials unit; + private Long givenExpTimeInSeconds; + + @Before + public void setUp() throws Exception { + this.givenExpTimeInSeconds = Instant.now().getEpochSecond() + TimeUnit.HOURS.toSeconds(1); + + this.jwtTokenFile = JwtTokenFileTestUtils.createValidJwtToken(givenExpTimeInSeconds); + + Constructor ctor = + JwtTokenFileCallCredentials.class.getDeclaredConstructor(String.class); + ctor.setAccessible(true); + this.unit = ctor.newInstance(jwtTokenFile.toString()); + } + + @Test + public void givenValidJwtTokenFile_WhenTokenRefreshed_ExpectAccessTokenInstance() + throws Exception { + final Date givenExpTimeDate = new Date(TimeUnit.SECONDS.toMillis(givenExpTimeInSeconds)); + + String givenTokenValue = new String( + Files.readAllBytes(jwtTokenFile.toPath()), + StandardCharsets.UTF_8); + + AccessToken token = unit.refreshAccessToken(); + + Truth.assertThat(token.getExpirationTime()) + .isEquivalentAccordingToCompareTo(givenExpTimeDate); + assertEquals(token.getTokenValue(), givenTokenValue); + } + } +} diff --git a/xds/src/test/java/io/grpc/xds/JwtTokenFileTestUtils.java b/xds/src/test/java/io/grpc/xds/JwtTokenFileTestUtils.java new file mode 100644 index 00000000000..3cda6025f66 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/JwtTokenFileTestUtils.java @@ -0,0 +1,66 @@ +/* + * Copyright 2025 The gRPC Authors + * + * 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 io.grpc.xds; + +import com.google.common.io.BaseEncoding; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class JwtTokenFileTestUtils { + public static File createEmptyJwtToken() throws IOException { + File jwtToken = File.createTempFile(new String("jwt.token"), ""); + jwtToken.deleteOnExit(); + return jwtToken; + } + + public static File createJwtTokenWithoutExpiration() throws IOException { + File jwtToken = File.createTempFile(new String("jwt.token"), ""); + jwtToken.deleteOnExit(); + FileOutputStream outputStream = new FileOutputStream(jwtToken); + String content = + BaseEncoding.base64().encode( + new String("{\"typ\": \"JWT\", \"alg\": \"HS256\"}").getBytes(StandardCharsets.UTF_8)) + + "." + + BaseEncoding.base64().encode( + new String("{\"name\": \"Google\"}").getBytes(StandardCharsets.UTF_8)) + + "." + + BaseEncoding.base64().encode(new String("signature").getBytes(StandardCharsets.UTF_8)); + outputStream.write(content.getBytes(StandardCharsets.UTF_8)); + outputStream.close(); + return jwtToken; + } + + public static File createValidJwtToken(long expTime) + throws Exception { + File jwtToken = File.createTempFile(new String("jwt.token"), ""); + jwtToken.deleteOnExit(); + FileOutputStream outputStream = new FileOutputStream(jwtToken); + String content = + BaseEncoding.base64().encode( + new String("{\"typ\": \"JWT\", \"alg\": \"HS256\"}").getBytes(StandardCharsets.UTF_8)) + + "." + + BaseEncoding.base64().encode( + String.format("{\"exp\": %d}", expTime).getBytes(StandardCharsets.UTF_8)) + + "." + + BaseEncoding.base64().encode(new String("signature").getBytes(StandardCharsets.UTF_8)); + outputStream.write(content.getBytes(StandardCharsets.UTF_8)); + outputStream.close(); + return jwtToken; + } +} diff --git a/xds/src/test/java/io/grpc/xds/SharedXdsClientPoolProviderTest.java b/xds/src/test/java/io/grpc/xds/SharedXdsClientPoolProviderTest.java index 24f1750d5a8..9c1e352e3b6 100644 --- a/xds/src/test/java/io/grpc/xds/SharedXdsClientPoolProviderTest.java +++ b/xds/src/test/java/io/grpc/xds/SharedXdsClientPoolProviderTest.java @@ -28,6 +28,7 @@ import com.google.auth.oauth2.OAuth2Credentials; import com.google.common.util.concurrent.SettableFuture; import io.grpc.CallCredentials; +import io.grpc.CompositeChannelCredentials; import io.grpc.Grpc; import io.grpc.InsecureChannelCredentials; import io.grpc.InsecureServerCredentials; @@ -39,6 +40,8 @@ import io.grpc.ServerInterceptor; import io.grpc.auth.MoreCallCredentials; import io.grpc.internal.ObjectPool; +import io.grpc.xds.JwtTokenFileCallCredentials; +import io.grpc.xds.JwtTokenFileTestUtils; import io.grpc.xds.SharedXdsClientPoolProvider.RefCountedXdsClientObjectPool; import io.grpc.xds.XdsListenerResource.LdsUpdate; import io.grpc.xds.client.Bootstrapper.BootstrapInfo; @@ -47,6 +50,10 @@ import io.grpc.xds.client.XdsClient; import io.grpc.xds.client.XdsClient.ResourceWatcher; import io.grpc.xds.client.XdsInitializationException; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.time.Instant; import java.util.Collections; import java.util.concurrent.TimeUnit; import org.junit.Rule; @@ -212,4 +219,55 @@ public void xdsClient_usesCallCredentials() throws Exception { xdsClientPool.returnObject(xdsClient); xdsServer.shutdownNow(); } + + @Test + public void xdsClient_usesJwtTokenFileCallCredentials() throws Exception { + GrpcBootstrapperImpl.xdsBootstrapCallCredsEnabled = true; + + Long givenExpTimeInSeconds = Instant.now().getEpochSecond() + TimeUnit.HOURS.toSeconds(1); + File jwtToken = JwtTokenFileTestUtils.createValidJwtToken(givenExpTimeInSeconds); + String jwtTokenContent = new String( + Files.readAllBytes(jwtToken.toPath()), + StandardCharsets.UTF_8); + + // Set up fake xDS server + XdsTestControlPlaneService fakeXdsService = new XdsTestControlPlaneService(); + CallCredsServerInterceptor callCredentialsInterceptor = new CallCredsServerInterceptor(); + Server xdsServer = + Grpc.newServerBuilderForPort(0, InsecureServerCredentials.create()) + .addService(fakeXdsService) + .intercept(callCredentialsInterceptor) + .build() + .start(); + String xdsServerUri = "localhost:" + xdsServer.getPort(); + + // Set up bootstrap & xDS client pool provider + ServerInfo server = ServerInfo.create( + xdsServerUri, + CompositeChannelCredentials.create( + InsecureChannelCredentials.create(), + JwtTokenFileCallCredentials.create(jwtToken.toString()))); + BootstrapInfo bootstrapInfo = + BootstrapInfo.builder().servers(Collections.singletonList(server)).node(node).build(); + when(bootstrapper.bootstrap()).thenReturn(bootstrapInfo); + SharedXdsClientPoolProvider provider = new SharedXdsClientPoolProvider(bootstrapper); + + // Create xDS client that uses the JwtTokenFileCallCredentials on the transport + ObjectPool xdsClientPool = + provider.getOrCreate( + "target", + metricRecorder, + JwtTokenFileCallCredentials.create(jwtToken.toString())); + XdsClient xdsClient = xdsClientPool.getObject(); + xdsClient.watchXdsResource( + XdsListenerResource.getInstance(), "someLDSresource", ldsResourceWatcher); + + // Wait for xDS server to get the request and verify that it received the CallCredentials + assertThat(callCredentialsInterceptor.getTokenWithTimeout(5, TimeUnit.SECONDS)) + .isEqualTo("Bearer " + jwtTokenContent); + + // Clean up + xdsClientPool.returnObject(xdsClient); + xdsServer.shutdownNow(); + } } diff --git a/xds/src/test/java/io/grpc/xds/XdsCredentialsRegistryTest.java b/xds/src/test/java/io/grpc/xds/XdsCredentialsRegistryTest.java index facaffc67a2..b8a057fb2f3 100644 --- a/xds/src/test/java/io/grpc/xds/XdsCredentialsRegistryTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsCredentialsRegistryTest.java @@ -22,11 +22,13 @@ import static org.junit.Assert.fail; import com.google.common.collect.ImmutableMap; +import io.grpc.CallCredentials; import io.grpc.ChannelCredentials; import io.grpc.xds.XdsCredentialsProvider; import io.grpc.xds.XdsCredentialsRegistry; import io.grpc.xds.internal.GoogleDefaultXdsCredentialsProvider; import io.grpc.xds.internal.InsecureXdsCredentialsProvider; +import io.grpc.xds.internal.JwtTokenFileXdsCredentialsProvider; import io.grpc.xds.internal.TlsXdsCredentialsProvider; import java.util.List; import java.util.Map; @@ -136,11 +138,13 @@ public ChannelCredentials newChannelCredentials(Map config) { public void defaultRegistry_providers() { Map providers = XdsCredentialsRegistry.getDefaultRegistry().providers(); - assertThat(providers).hasSize(3); + assertThat(providers).hasSize(4); assertThat(providers.get("google_default").getClass()) .isEqualTo(GoogleDefaultXdsCredentialsProvider.class); assertThat(providers.get("insecure").getClass()) .isEqualTo(InsecureXdsCredentialsProvider.class); + assertThat(providers.get("jwt_token_file").getClass()) + .isEqualTo(JwtTokenFileXdsCredentialsProvider.class); assertThat(providers.get("tls").getClass()) .isEqualTo(TlsXdsCredentialsProvider.class); } @@ -151,6 +155,7 @@ public void getClassesViaHardcoded_classesPresent() throws Exception { assertThat(classes).containsExactly( GoogleDefaultXdsCredentialsProvider.class, InsecureXdsCredentialsProvider.class, + JwtTokenFileXdsCredentialsProvider.class, TlsXdsCredentialsProvider.class); } @@ -195,6 +200,11 @@ public int priority() { public ChannelCredentials newChannelCredentials(Map config) { throw new UnsupportedOperationException(); } + + @Override + public CallCredentials newCallCredentials(Map config) { + throw new UnsupportedOperationException(); + } } private static class SampleChannelCredentials extends ChannelCredentials { diff --git a/xds/src/test/java/io/grpc/xds/internal/GoogleDefaultXdsCredentialsProviderTest.java b/xds/src/test/java/io/grpc/xds/internal/GoogleDefaultXdsCredentialsProviderTest.java index dd615809bc2..60cc6e6266e 100644 --- a/xds/src/test/java/io/grpc/xds/internal/GoogleDefaultXdsCredentialsProviderTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/GoogleDefaultXdsCredentialsProviderTest.java @@ -16,6 +16,7 @@ package io.grpc.xds.internal; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -54,4 +55,9 @@ public void channelCredentials() { assertSame(CompositeChannelCredentials.class, provider.newChannelCredentials(null).getClass()); } + + @Test + public void callCredentials() { + assertNull(provider.newCallCredentials(null)); + } } diff --git a/xds/src/test/java/io/grpc/xds/internal/InsecureXdsCredentialsProviderTest.java b/xds/src/test/java/io/grpc/xds/internal/InsecureXdsCredentialsProviderTest.java index 583255473eb..e3d4b1515f1 100644 --- a/xds/src/test/java/io/grpc/xds/internal/InsecureXdsCredentialsProviderTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/InsecureXdsCredentialsProviderTest.java @@ -16,6 +16,7 @@ package io.grpc.xds.internal; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -54,4 +55,9 @@ public void channelCredentials() { assertSame(InsecureChannelCredentials.class, provider.newChannelCredentials(null).getClass()); } + + @Test + public void callCredentials() { + assertNull(provider.newCallCredentials(null)); + } } diff --git a/xds/src/test/java/io/grpc/xds/internal/JwtTokenFileXdsCredentialsProviderTest.java b/xds/src/test/java/io/grpc/xds/internal/JwtTokenFileXdsCredentialsProviderTest.java new file mode 100644 index 00000000000..ad707f58fd0 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/internal/JwtTokenFileXdsCredentialsProviderTest.java @@ -0,0 +1,85 @@ +/* + * Copyright 2025 The gRPC Authors + * + * 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 io.grpc.xds.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.common.collect.ImmutableMap; +import io.grpc.InternalServiceProviders; +import io.grpc.xds.XdsCredentialsProvider; +import java.io.File; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + + +/** Unit tests for {@link JwtTokenFileXdsCredentialsProvider}. */ +@RunWith(JUnit4.class) +public class JwtTokenFileXdsCredentialsProviderTest { + private JwtTokenFileXdsCredentialsProvider provider = new JwtTokenFileXdsCredentialsProvider(); + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + @Test + public void provided() { + for (XdsCredentialsProvider current + : InternalServiceProviders.getCandidatesViaServiceLoader( + XdsCredentialsProvider.class, getClass().getClassLoader())) { + if (current instanceof JwtTokenFileXdsCredentialsProvider) { + return; + } + } + fail("ServiceLoader unable to load JwtTokenFileXdsCredentialsProvider"); + } + + @Test + public void isAvailable() { + assertTrue(provider.isAvailable()); + } + + @Test + public void channelCredentials() { + assertNull(provider.newChannelCredentials(null)); + } + + @Test + public void callCredentialsWhenNullConfig() { + assertNull(provider.newCallCredentials(null)); + } + + @Test + public void callCredentialsWhenWrongConfig() { + Map jsonConfig = ImmutableMap.of("jwt_token_file", "/tmp/not-exisiting-file.txt"); + assertNull(provider.newCallCredentials(jsonConfig)); + } + + @Test + public void callCredentialsWhenExpectedConfig() throws Exception { + File createdFile = tempFolder.newFile(new String("existing-file.txt")); + Map jsonConfig = ImmutableMap.of("jwt_token_file", createdFile.toString()); + assertEquals("io.grpc.auth.GoogleAuthLibraryCallCredentials", + provider.newCallCredentials(jsonConfig).getClass().getName()); + createdFile.delete(); + } +} diff --git a/xds/src/test/java/io/grpc/xds/internal/TlsXdsCredentialsProviderTest.java b/xds/src/test/java/io/grpc/xds/internal/TlsXdsCredentialsProviderTest.java index 3ba26bdb281..4eefdb35699 100644 --- a/xds/src/test/java/io/grpc/xds/internal/TlsXdsCredentialsProviderTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/TlsXdsCredentialsProviderTest.java @@ -16,6 +16,7 @@ package io.grpc.xds.internal; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -54,4 +55,9 @@ public void channelCredentials() { assertSame(TlsChannelCredentials.class, provider.newChannelCredentials(null).getClass()); } + + @Test + public void callCredentials() { + assertNull(provider.newCallCredentials(null)); + } }