Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions xds/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,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"),
Expand Down
67 changes: 66 additions & 1 deletion xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@

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.CompositeChannelCredentials;
import io.grpc.internal.GrpcUtil;
import io.grpc.internal.JsonUtil;
import io.grpc.xds.client.BootstrapperImpl;
import io.grpc.xds.client.XdsInitializationException;
Expand All @@ -33,6 +37,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
Expand All @@ -41,6 +47,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();
Expand Down Expand Up @@ -92,7 +101,12 @@ protected String getJsonContent() throws XdsInitializationException, IOException
@Override
protected Object getImplSpecificConfig(Map<String, ?> serverConfig, String serverUri)
throws XdsInitializationException {
return getChannelCredentials(serverConfig, serverUri);
ChannelCredentials channelCreds = getChannelCredentials(serverConfig, serverUri);
CallCredentials callCreds = getCallCredentials(serverConfig, serverUri);
if (callCreds != null) {
channelCreds = CompositeChannelCredentials.create(channelCreds, callCreds);
}
return channelCreds;
}

private static ChannelCredentials getChannelCredentials(Map<String, ?> serverConfig,
Expand Down Expand Up @@ -135,4 +149,55 @@ private static ChannelCredentials parseChannelCredentials(List<Map<String, ?>> j
}
return null;
}

private static CallCredentials getCallCredentials(Map<String, ?> 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<Map<String, ?>> jsonList,
String serverUri)
throws XdsInitializationException {
if (!xdsBootstrapCallCredsEnabled) {
return null;
}

CallCredentials callCredentials = null;
for (Map<String, ?> callCreds : jsonList) {
String type = JsonUtil.getString(callCreds, "type");
if (type == null) {
continue;
}

XdsCallCredentialsProvider provider = XdsCallCredentialsRegistry.getDefaultRegistry()
.getProvider(type);
if (provider == null) {
continue;
}

Map<String, ?> 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'");
}

callCredentials = (callCredentials == null)
? parsedCallCredentials
: new CompositeCallCredentials(callCredentials, parsedCallCredentials);
}
return callCredentials;
}
}
47 changes: 47 additions & 0 deletions xds/src/main/java/io/grpc/xds/XdsCallCredentialsProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* 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 io.grpc.CallCredentials;
import io.grpc.Internal;
import java.util.Map;

/**
* Provider of credentials data which will be propagated to the server for each RPC. The actual call
* credential to be used for a particular xDS communication will be chosen based on the bootstrap
* configuration.
*/
@Internal
public abstract class XdsCallCredentialsProvider {
/**
* 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<String, ?> jsonConfig);

/**
* Returns the xDS call credential name associated with this provider which makes it selectable
* via {@link XdsCallCredentialsRegistry#getProvider}. This is called only when the class is
* loaded. It shouldn't change, and there is no point doing so.
*/
protected abstract String getName();
}
76 changes: 76 additions & 0 deletions xds/src/main/java/io/grpc/xds/XdsCallCredentialsRegistry.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* 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.common.annotations.VisibleForTesting;
import io.grpc.xds.internal.JwtTokenFileXdsCallCredentialsProvider;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;

/**
* Registry of {@link XdsCallCredentialsProvider}s. The {@link #getDefaultRegistry default
* instance} loads hardcoded providers at runtime.
*/
@ThreadSafe
final class XdsCallCredentialsRegistry {
private static XdsCallCredentialsRegistry instance;

private final Map<String, XdsCallCredentialsProvider> registeredProviders =
new HashMap<>();

/**
* Returns the default registry that loads hardcoded providers at runtime.
*/
public static synchronized XdsCallCredentialsRegistry getDefaultRegistry() {
if (instance == null) {
instance = newRegistry().register(new JwtTokenFileXdsCallCredentialsProvider());
}
return instance;
}

@VisibleForTesting
static XdsCallCredentialsRegistry newRegistry() {
return new XdsCallCredentialsRegistry();
}

@VisibleForTesting
XdsCallCredentialsRegistry register(XdsCallCredentialsProvider... providers) {
for (XdsCallCredentialsProvider provider : providers) {
registeredProviders.put(provider.getName(), provider);
}
return this;
}

@VisibleForTesting
synchronized Map<String, XdsCallCredentialsProvider> providers() {
return registeredProviders;
}

/**
* Returns the registered provider for the given xDS call credential name, or {@code null} if no
* suitable provider can be found.
* Each provider declares its name via {@link XdsCallCredentialsProvider#getName}.
*/
@Nullable
public synchronized XdsCallCredentialsProvider getProvider(String name) {
return registeredProviders.get(checkNotNull(name, "name"));
}
}
33 changes: 2 additions & 31 deletions xds/src/main/java/io/grpc/xds/XdsCredentialsRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,14 @@
import static com.google.common.base.Preconditions.checkNotNull;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import io.grpc.InternalServiceProviders;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
Expand Down Expand Up @@ -109,7 +107,7 @@ public static synchronized XdsCredentialsRegistry getDefaultRegistry() {
if (instance == null) {
List<XdsCredentialsProvider> providerList = InternalServiceProviders.loadAll(
XdsCredentialsProvider.class,
getHardCodedClasses(),
ImmutableList.of(),
XdsCredentialsProvider.class.getClassLoader(),
new XdsCredentialsProviderPriorityAccessor());
if (providerList.isEmpty()) {
Expand Down Expand Up @@ -147,33 +145,6 @@ public synchronized XdsCredentialsProvider getProvider(String name) {
return effectiveProviders.get(checkNotNull(name, "name"));
}

@VisibleForTesting
static List<Class<?>> getHardCodedClasses() {
// Class.forName(String) is used to remove the need for ProGuard configuration. Note that
// ProGuard does not detect usages of Class.forName(String, boolean, ClassLoader):
// https://sourceforge.net/p/proguard/bugs/418/
ArrayList<Class<?>> list = new ArrayList<>();
try {
list.add(Class.forName("io.grpc.xds.internal.GoogleDefaultXdsCredentialsProvider"));
} catch (ClassNotFoundException e) {
logger.log(Level.WARNING, "Unable to find GoogleDefaultXdsCredentialsProvider", e);
}

try {
list.add(Class.forName("io.grpc.xds.internal.InsecureXdsCredentialsProvider"));
} catch (ClassNotFoundException e) {
logger.log(Level.WARNING, "Unable to find InsecureXdsCredentialsProvider", e);
}

try {
list.add(Class.forName("io.grpc.xds.internal.TlsXdsCredentialsProvider"));
} catch (ClassNotFoundException e) {
logger.log(Level.WARNING, "Unable to find TlsXdsCredentialsProvider", e);
}

return Collections.unmodifiableList(list);
}

private static final class XdsCredentialsProviderPriorityAccessor
implements InternalServiceProviders.PriorityAccessor<XdsCredentialsProvider> {
@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -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.internal;

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);
}
}
Loading
Loading