From a8f76b0b7b2ea98fd5e123d4638851f95877543a Mon Sep 17 00:00:00 2001 From: Damian Zgoda Date: Wed, 23 Jul 2025 19:06:29 +0000 Subject: [PATCH] JWT token file call creds --- .../alts/JwtTokenFileCallCredentials.java | 63 ++++++ .../alts/JwtTokenFileCallCredentialsTest.java | 122 ++++++++++++ .../io/grpc/xds/GrpcBootstrapperImpl.java | 64 ++++++- .../io/grpc/xds/GrpcXdsTransportFactory.java | 21 +- .../io/grpc/xds/XdsCredentialsProvider.java | 12 ++ .../io/grpc/xds/XdsCredentialsRegistry.java | 10 +- .../java/io/grpc/xds/client/Bootstrapper.java | 38 +++- .../io/grpc/xds/client/BootstrapperImpl.java | 20 +- .../GoogleDefaultXdsCredentialsProvider.java | 6 + .../InsecureXdsCredentialsProvider.java | 6 + .../JwtTokenFileXdsCredentialsProvider.java | 68 +++++++ .../internal/TlsXdsCredentialsProvider.java | 6 + .../io.grpc.xds.XdsCredentialsProvider | 1 + .../io/grpc/xds/GrpcBootstrapperImplTest.java | 179 +++++++++++++++++- .../grpc/xds/GrpcXdsClientImplDataTest.java | 8 +- .../grpc/xds/GrpcXdsClientImplTestBase.java | 14 +- .../io/grpc/xds/XdsClientFallbackTest.java | 2 +- .../grpc/xds/XdsCredentialsRegistryTest.java | 12 +- .../java/io/grpc/xds/XdsNameResolverTest.java | 4 +- .../client/CommonBootstrapperTestUtils.java | 2 +- ...ogleDefaultXdsCredentialsProviderTest.java | 6 + .../InsecureXdsCredentialsProviderTest.java | 6 + ...wtTokenFileXdsCredentialsProviderTest.java | 85 +++++++++ .../TlsXdsCredentialsProviderTest.java | 6 + 24 files changed, 726 insertions(+), 35 deletions(-) create mode 100644 alts/src/main/java/io/grpc/alts/JwtTokenFileCallCredentials.java create mode 100644 alts/src/test/java/io/grpc/alts/JwtTokenFileCallCredentialsTest.java create mode 100644 xds/src/main/java/io/grpc/xds/internal/JwtTokenFileXdsCredentialsProvider.java create mode 100644 xds/src/test/java/io/grpc/xds/internal/JwtTokenFileXdsCredentialsProviderTest.java diff --git a/alts/src/main/java/io/grpc/alts/JwtTokenFileCallCredentials.java b/alts/src/main/java/io/grpc/alts/JwtTokenFileCallCredentials.java new file mode 100644 index 0000000000..6e35d46a7a --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/JwtTokenFileCallCredentials.java @@ -0,0 +1,63 @@ +/* + * 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.alts; + +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 String path = null; + + private JwtTokenFileCallCredentials(String path) { + this.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/alts/src/test/java/io/grpc/alts/JwtTokenFileCallCredentialsTest.java b/alts/src/test/java/io/grpc/alts/JwtTokenFileCallCredentialsTest.java new file mode 100644 index 0000000000..946efe596f --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/JwtTokenFileCallCredentialsTest.java @@ -0,0 +1,122 @@ +/* + * 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.alts; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import com.google.auth.oauth2.AccessToken; +import com.google.common.io.BaseEncoding; +import com.google.common.truth.Truth; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.nio.charset.StandardCharsets; +import java.util.Date; +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; + +/** Unit tests for {@link JwtTokenFileCallCredentials}. */ +@RunWith(JUnit4.class) +public class JwtTokenFileCallCredentialsTest { + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + private File jwtTokenFile; + private JwtTokenFileCallCredentials unit; + + @Before + public void setUp() throws Exception { + jwtTokenFile = tempFolder.newFile(new String("jwt.json")); + + Constructor ctor = + JwtTokenFileCallCredentials.class.getDeclaredConstructor(String.class); + ctor.setAccessible(true); + unit = ctor.newInstance(jwtTokenFile.toString()); + } + + private void fillJwtTokenWithoutExpiration(File jwtFile) throws Exception { + FileOutputStream outputStream = new FileOutputStream(jwtFile); + 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(); + } + + private String fillValidJwtToken(File jwtFile, Long expTime) throws Exception { + FileOutputStream outputStream = new FileOutputStream(jwtFile); + 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 content; + } + + @Test + public void givenJwtTokenFileEmpty_WhenTokenRefreshed_ExpectException() { + assertThrows(IllegalArgumentException.class, () -> { + unit.refreshAccessToken(); + }); + } + + @Test + public void givenJwtTokenFileWithoutExpiration_WhenTokenRefreshed_ExpectException() + throws Exception { + + fillJwtTokenWithoutExpiration(jwtTokenFile); + + Exception ex = assertThrows(IOException.class, () -> { + unit.refreshAccessToken(); + }); + + String expectedMsg = "No expiration time found for JWT token"; + String actualMsg = ex.getMessage(); + + assertEquals(expectedMsg, actualMsg); + } + + @Test + public void givenValidJwtTokenFile_WhenTokenRefreshed_ExpectAccessTokenInstance() + throws Exception { + final Long givenExpTimeInSeconds = 1753364000L; + final Date givenExpTimeDate = new Date(givenExpTimeInSeconds * 1000L); + + String givenTokenValue = fillValidJwtToken(jwtTokenFile, givenExpTimeInSeconds); + + AccessToken token = unit.refreshAccessToken(); + + Truth.assertThat(token.getExpirationTime()).isEquivalentAccordingToCompareTo(givenExpTimeDate); + assertEquals(token.getTokenValue(), givenTokenValue); + } +} diff --git a/xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java b/xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java index f61fab42ca..382e865083 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 @@ package io.grpc.xds; 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 @@ class GrpcBootstrapperImpl extends BootstrapperImpl { } @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 @@ class GrpcBootstrapperImpl extends BootstrapperImpl { } 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 0da51bf47f..a06f1dae5b 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.CallCredentials; 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,26 @@ final class GrpcXdsTransportFactory implements XdsTransportFactory { public GrpcXdsTransport(Bootstrapper.ServerInfo serverInfo, CallCredentials callCredentials) { String target = serverInfo.target(); - ChannelCredentials channelCredentials = (ChannelCredentials) serverInfo.implSpecificConfig(); + ChannelCredentials channelCredentials = + (ChannelCredentials) serverInfo.implSpecificChannelCredConfig(); + Object callCredConfig = serverInfo.implSpecificCallCredConfig(); + if (callCredConfig != null) { + channelCredentials = CompositeChannelCredentials.create( + channelCredentials, (CallCredentials) callCredConfig); + } + this.channel = Grpc.newChannelBuilder(target, channelCredentials) .keepAliveTime(5, TimeUnit.MINUTES) .build(); - this.callCredentials = callCredentials; + + if (callCredentials != null && callCredConfig != null) { + this.callCredentials = + new CompositeCallCredentials(callCredentials, (CallCredentials) callCredConfig); + } else if (callCredConfig != null) { + this.callCredentials = (CallCredentials) callCredConfig; + } else { + this.callCredentials = callCredentials; + } } @VisibleForTesting diff --git a/xds/src/main/java/io/grpc/xds/XdsCredentialsProvider.java b/xds/src/main/java/io/grpc/xds/XdsCredentialsProvider.java index e9466f37a0..5e827ceebe 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 channel credentials. + * + * @param jsonConfig json config that can be consumed by the provider to create + * the channel 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 9dfefaf1a6..5c57473cf7 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 @@ final class XdsCredentialsRegistry { 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 @@ final class XdsCredentialsRegistry { } 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/Bootstrapper.java b/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java index 90babd1e8d..a7272cf073 100644 --- a/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java +++ b/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java @@ -57,22 +57,50 @@ public abstract class Bootstrapper { public abstract static class ServerInfo { public abstract String target(); - public abstract Object implSpecificConfig(); + public abstract Object implSpecificChannelCredConfig(); + + @Nullable public abstract Object implSpecificCallCredConfig(); public abstract boolean ignoreResourceDeletion(); public abstract boolean isTrustedXdsServer(); @VisibleForTesting - public static ServerInfo create(String target, @Nullable Object implSpecificConfig) { - return new AutoValue_Bootstrapper_ServerInfo(target, implSpecificConfig, false, false); + public static ServerInfo create( + String target, + @Nullable Object implSpecificChannelCredConfig) { + return new AutoValue_Bootstrapper_ServerInfo( + target, + implSpecificChannelCredConfig, + null, + false, + false); + } + + @VisibleForTesting + public static ServerInfo create( + String target, + @Nullable Object implSpecificChannelCredConfig, + @Nullable Object implSpecificCallCredConfig) { + return new AutoValue_Bootstrapper_ServerInfo( + target, + implSpecificChannelCredConfig, + implSpecificCallCredConfig, + false, + false); } @VisibleForTesting public static ServerInfo create( - String target, Object implSpecificConfig, boolean ignoreResourceDeletion, + String target, + Object implSpecificChannelCredConfig, + @Nullable Object implSpecificCallCredConfig, + boolean ignoreResourceDeletion, boolean isTrustedXdsServer) { - return new AutoValue_Bootstrapper_ServerInfo(target, implSpecificConfig, + return new AutoValue_Bootstrapper_ServerInfo( + target, + implSpecificChannelCredConfig, + implSpecificCallCredConfig, ignoreResourceDeletion, isTrustedXdsServer); } } 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 c00685f178..37af88100f 100644 --- a/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java +++ b/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java @@ -68,9 +68,13 @@ public abstract class BootstrapperImpl extends Bootstrapper { 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. @@ -245,7 +249,9 @@ public abstract class BootstrapperImpl extends Bootstrapper { } 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 ignoreResourceDeletion = false; // "For forward compatibility reasons, the client will ignore any entry in the list that it @@ -256,9 +262,13 @@ public abstract class BootstrapperImpl extends Bootstrapper { ignoreResourceDeletion = serverFeatures.contains(SERVER_FEATURE_IGNORE_RESOURCE_DELETION); } servers.add( - ServerInfo.create(serverUri, implSpecificConfig, ignoreResourceDeletion, - serverFeatures != null - && serverFeatures.contains(SERVER_FEATURE_TRUSTED_XDS_SERVER))); + ServerInfo.create( + serverUri, + implSpecificChannelCredConfig, + implSpecificCallCredConfig, + ignoreResourceDeletion, + serverFeatures != null + && serverFeatures.contains(SERVER_FEATURE_TRUSTED_XDS_SERVER))); } 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 383c19b666..4240dcb396 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 @@ public final class GoogleDefaultXdsCredentialsProvider extends XdsCredentialsPro 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 d57cfe2f23..31dfe65fc9 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 @@ public final class InsecureXdsCredentialsProvider extends XdsCredentialsProvider 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 0000000000..ded8f0f6e7 --- /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.alts.JwtTokenFileCallCredentials; +import io.grpc.internal.JsonUtil; +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 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 f4d26a8379..364eb7c701 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 @@ public final class TlsXdsCredentialsProvider extends XdsCredentialsProvider { 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 a51cd11473..b46ef34dfa 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 3f93cc6f19..3e45de7ec5 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java @@ -17,6 +17,9 @@ package io.grpc.xds; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; @@ -24,6 +27,7 @@ import static org.mockito.Mockito.verifyNoInteractions; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; +import io.grpc.CompositeCallCredentials; import io.grpc.InsecureChannelCredentials; import io.grpc.TlsChannelCredentials; import io.grpc.internal.GrpcUtil; @@ -37,13 +41,16 @@ import io.grpc.xds.client.CommonBootstrapperTestUtils; 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 @@ public class GrpcBootstrapperImplTest { originalBootstrapPathFromSysProp = bootstrapper.bootstrapPathFromSysProp; originalBootstrapConfigFromEnvVar = bootstrapper.bootstrapConfigFromEnvVar; originalBootstrapConfigFromSysProp = bootstrapper.bootstrapConfigFromSysProp; + originalExperimentalXdsBootstrapCallCredsFlag = + GrpcBootstrapperImpl.xdsBootstrapCallCredsEnabled; } @After @@ -82,6 +96,8 @@ public class GrpcBootstrapperImplTest { bootstrapper.bootstrapConfigFromEnvVar = originalBootstrapConfigFromEnvVar; bootstrapper.bootstrapConfigFromSysProp = originalBootstrapConfigFromSysProp; CommonBootstrapperTestUtils.setEnableXdsFallback(originalExperimentalXdsFallbackFlag); + GrpcBootstrapperImpl.xdsBootstrapCallCredsEnabled = + originalExperimentalXdsBootstrapCallCredsFlag; } @Test @@ -115,7 +131,9 @@ public class GrpcBootstrapperImplTest { 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.implSpecificChannelCredConfig()).isInstanceOf( + InsecureChannelCredentials.class); + assertNull(serverInfo.implSpecificCallCredConfig()); assertThat(info.node()).isEqualTo( getNodeBuilder() .setId("ENVOY_NODE_ID") @@ -168,12 +186,14 @@ public class GrpcBootstrapperImplTest { List serverInfoList = info.servers(); assertThat(serverInfoList.get(0).target()) .isEqualTo("trafficdirector-foo.googleapis.com:443"); - assertThat(serverInfoList.get(0).implSpecificConfig()) + assertThat(serverInfoList.get(0).implSpecificChannelCredConfig()) .isInstanceOf(TlsChannelCredentials.class); + assertNull(serverInfoList.get(0).implSpecificCallCredConfig()); assertThat(serverInfoList.get(1).target()) .isEqualTo("trafficdirector-bar.googleapis.com:443"); - assertThat(serverInfoList.get(1).implSpecificConfig()) + assertThat(serverInfoList.get(1).implSpecificChannelCredConfig()) .isInstanceOf(InsecureChannelCredentials.class); + assertNull(serverInfoList.get(0).implSpecificCallCredConfig()); assertThat(info.node()).isEqualTo( getNodeBuilder() .setId("ENVOY_NODE_ID") @@ -217,7 +237,9 @@ public class GrpcBootstrapperImplTest { 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.implSpecificChannelCredConfig()).isInstanceOf( + InsecureChannelCredentials.class); + assertNull(serverInfo.implSpecificCallCredConfig()); assertThat(info.node()).isEqualTo( getNodeBuilder() .setId("ENVOY_NODE_ID") @@ -288,7 +310,9 @@ public class GrpcBootstrapperImplTest { 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.implSpecificChannelCredConfig()).isInstanceOf( + InsecureChannelCredentials.class); + assertNull(serverInfo.implSpecificCallCredConfig()); assertThat(info.node()).isEqualTo(getNodeBuilder().build()); } @@ -583,7 +607,9 @@ public class GrpcBootstrapperImplTest { BootstrapInfo info = bootstrapper.bootstrap(); ServerInfo serverInfo = Iterables.getOnlyElement(info.servers()); assertThat(serverInfo.target()).isEqualTo(SERVER_URI); - assertThat(serverInfo.implSpecificConfig()).isInstanceOf(InsecureChannelCredentials.class); + assertThat(serverInfo.implSpecificChannelCredConfig()).isInstanceOf( + InsecureChannelCredentials.class); + assertNull(serverInfo.implSpecificCallCredConfig()); assertThat(serverInfo.ignoreResourceDeletion()).isFalse(); } @@ -605,7 +631,9 @@ public class GrpcBootstrapperImplTest { BootstrapInfo info = bootstrapper.bootstrap(); ServerInfo serverInfo = Iterables.getOnlyElement(info.servers()); assertThat(serverInfo.target()).isEqualTo(SERVER_URI); - assertThat(serverInfo.implSpecificConfig()).isInstanceOf(InsecureChannelCredentials.class); + assertThat(serverInfo.implSpecificChannelCredConfig()).isInstanceOf( + InsecureChannelCredentials.class); + assertNull(serverInfo.implSpecificCallCredConfig()); assertThat(serverInfo.ignoreResourceDeletion()).isFalse(); } @@ -627,7 +655,9 @@ public class GrpcBootstrapperImplTest { BootstrapInfo info = bootstrapper.bootstrap(); ServerInfo serverInfo = Iterables.getOnlyElement(info.servers()); assertThat(serverInfo.target()).isEqualTo(SERVER_URI); - assertThat(serverInfo.implSpecificConfig()).isInstanceOf(InsecureChannelCredentials.class); + assertThat(serverInfo.implSpecificChannelCredConfig()).isInstanceOf( + InsecureChannelCredentials.class); + assertNull(serverInfo.implSpecificCallCredConfig()); // Only ignore_resource_deletion feature enabled: confirm it's on, and xds_v3 is off. assertThat(serverInfo.ignoreResourceDeletion()).isTrue(); } @@ -650,7 +680,9 @@ public class GrpcBootstrapperImplTest { BootstrapInfo info = bootstrapper.bootstrap(); ServerInfo serverInfo = Iterables.getOnlyElement(info.servers()); assertThat(serverInfo.target()).isEqualTo(SERVER_URI); - assertThat(serverInfo.implSpecificConfig()).isInstanceOf(InsecureChannelCredentials.class); + assertThat(serverInfo.implSpecificChannelCredConfig()).isInstanceOf( + InsecureChannelCredentials.class); + assertNull(serverInfo.implSpecificCallCredConfig()); assertThat(serverInfo.isTrustedXdsServer()).isTrue(); } @@ -672,7 +704,9 @@ public class GrpcBootstrapperImplTest { BootstrapInfo info = bootstrapper.bootstrap(); ServerInfo serverInfo = Iterables.getOnlyElement(info.servers()); assertThat(serverInfo.target()).isEqualTo(SERVER_URI); - assertThat(serverInfo.implSpecificConfig()).isInstanceOf(InsecureChannelCredentials.class); + assertThat(serverInfo.implSpecificChannelCredConfig()).isInstanceOf( + InsecureChannelCredentials.class); + assertNull(serverInfo.implSpecificCallCredConfig()); // ignore_resource_deletion features enabled: confirm both are on. assertThat(serverInfo.ignoreResourceDeletion()).isTrue(); } @@ -898,6 +932,131 @@ public class GrpcBootstrapperImplTest { } } + @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()); + assertNull(serverInfo.implSpecificCallCredConfig()); + } + + @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()); + assertSame(CompositeCallCredentials.class, + serverInfo.implSpecificCallCredConfig().getClass()); + + 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/GrpcXdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java index 6167f49193..9e5e0fd1fe 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java @@ -3549,7 +3549,11 @@ public class GrpcXdsClientImplDataTest { private XdsResourceType.Args getXdsResourceTypeArgs(boolean isTrustedServer) { return new XdsResourceType.Args( - ServerInfo.create("http://td", "", false, isTrustedServer), "1.0", null, null, null, null - ); + ServerInfo.create("http://td", "", "", false, isTrustedServer), + "1.0", + null, + null, + null, + null); } } diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java index 32361684a6..3d1816b641 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java @@ -346,7 +346,11 @@ public abstract class GrpcXdsClientImplTestBase { } }; - xdsServerInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, ignoreResourceDeletion(), + xdsServerInfo = ServerInfo.create( + SERVER_URI, + CHANNEL_CREDENTIALS, + null, + ignoreResourceDeletion(), true); BootstrapInfo bootstrapInfo = Bootstrapper.BootstrapInfo.builder() @@ -4219,8 +4223,12 @@ public abstract class GrpcXdsClientImplTestBase { private BootstrapInfo buildBootStrap(String serverUri) { - ServerInfo xdsServerInfo = ServerInfo.create(serverUri, CHANNEL_CREDENTIALS, - ignoreResourceDeletion(), true); + ServerInfo xdsServerInfo = ServerInfo.create( + serverUri, + CHANNEL_CREDENTIALS, + null, + ignoreResourceDeletion(), + true); return Bootstrapper.BootstrapInfo.builder() .servers(Collections.singletonList(xdsServerInfo)) diff --git a/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java b/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java index 1e7ce6dc2a..86b344833e 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java @@ -347,7 +347,7 @@ public class XdsClientFallbackTest { @Override public XdsTransport create(Bootstrapper.ServerInfo serverInfo) { ChannelCredentials channelCredentials = - (ChannelCredentials) serverInfo.implSpecificConfig(); + (ChannelCredentials) serverInfo.implSpecificChannelCredConfig(); return new GrpcXdsTransportFactory.GrpcXdsTransport( Grpc.newChannelBuilder(serverInfo.target(), channelCredentials) .executor(executor) diff --git a/xds/src/test/java/io/grpc/xds/XdsCredentialsRegistryTest.java b/xds/src/test/java/io/grpc/xds/XdsCredentialsRegistryTest.java index facaffc67a..b8a057fb2f 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.assertSame; 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 class XdsCredentialsRegistryTest { 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 class XdsCredentialsRegistryTest { assertThat(classes).containsExactly( GoogleDefaultXdsCredentialsProvider.class, InsecureXdsCredentialsProvider.class, + JwtTokenFileXdsCredentialsProvider.class, TlsXdsCredentialsProvider.class); } @@ -195,6 +200,11 @@ public class XdsCredentialsRegistryTest { 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/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index 3fa31aedf6..38d2094715 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -368,13 +368,13 @@ public class XdsNameResolverTest { String serviceAuthority = "[::FFFF:129.144.52.38]:80"; bootstrapInfo = BootstrapInfo.builder() .servers(ImmutableList.of(ServerInfo.create( - "td.googleapis.com", InsecureChannelCredentials.create(), true, true))) + "td.googleapis.com", InsecureChannelCredentials.create(), null, true, true))) .node(Node.newBuilder().build()) .authorities( ImmutableMap.of(targetAuthority, AuthorityInfo.create( "xdstp://" + targetAuthority + "/envoy.config.listener.v3.Listener/%s?foo=1&bar=2", ImmutableList.of(ServerInfo.create( - "td.googleapis.com", InsecureChannelCredentials.create(), true, true))))) + "td.googleapis.com", InsecureChannelCredentials.create(), null, true, true))))) .build(); expectedLdsResourceName = "xdstp://xds.authority.com/envoy.config.listener.v3.Listener/" + "%5B::FFFF:129.144.52.38%5D:80?bar=2&foo=1"; // query param canonified diff --git a/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java b/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java index 485970741c..a65e5b5e77 100644 --- a/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java +++ b/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java @@ -203,7 +203,7 @@ public class CommonBootstrapperTestUtils { List serverInfos = new ArrayList<>(); for (String uri : serverUris) { - serverInfos.add(ServerInfo.create(uri, CHANNEL_CREDENTIALS, false, true)); + serverInfos.add(ServerInfo.create(uri, CHANNEL_CREDENTIALS, null, false, true)); } EnvoyProtoData.Node node = EnvoyProtoData.Node.newBuilder().setId("node-id").build(); 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 dd615809bc..60cc6e6266 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 class GoogleDefaultXdsCredentialsProviderTest { 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 583255473e..e3d4b1515f 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 class InsecureXdsCredentialsProviderTest { 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 0000000000..ad707f58fd --- /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 3ba26bdb28..4eefdb3569 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 class TlsXdsCredentialsProviderTest { assertSame(TlsChannelCredentials.class, provider.newChannelCredentials(null).getClass()); } + + @Test + public void callCredentials() { + assertNull(provider.newCallCredentials(null)); + } }