Skip to content
Open
Show file tree
Hide file tree
Changes from 11 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
64 changes: 63 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,52 @@ 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 {
CallCredentials callCredentials = null;
if (xdsBootstrapCallCredsEnabled) {
for (Map<String, ?> callCreds : jsonList) {
String type = JsonUtil.getString(callCreds, "type");
if (type != null) {
XdsCredentialsProvider provider = XdsCredentialsRegistry.getDefaultRegistry()
.getProvider(type);
if (provider != null) {
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'");
}

if (callCredentials == null) {
callCredentials = parsedCallCredentials;
} else {
callCredentials = new CompositeCallCredentials(
callCredentials, parsedCallCredentials);
}
}
}
}
}
return callCredentials;
}
}
15 changes: 15 additions & 0 deletions xds/src/main/java/io/grpc/xds/XdsCredentialsProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package io.grpc.xds;

import io.grpc.CallCredentials;
import io.grpc.ChannelCredentials;
import io.grpc.Internal;
import java.util.Map;
Expand Down Expand Up @@ -49,6 +50,20 @@ public abstract class XdsCredentialsProvider {
*/
protected abstract ChannelCredentials newChannelCredentials(Map<String, ?> jsonConfig);

/**
* Creates a {@link CallCredentials} from the given jsonConfig, or {@code null} if the given
* config is invalid or credential data is not part of a RPC call. The provider may override
* this method. Moreover 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 CallCredentials newCallCredentials(Map<String, ?> jsonConfig) {
return null;
}

/**
* 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.
Expand Down
35 changes: 3 additions & 32 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,12 +107,12 @@ 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()) {
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) {
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);
}
}
Original file line number Diff line number Diff line change
@@ -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.XdsCredentialsProvider;
import io.grpc.xds.internal.JwtTokenFileCallCredentials;
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<String, ?> jsonConfig) {
return null;
}

@Override
protected CallCredentials newCallCredentials(Map<String, ?> 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;
}

}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
io.grpc.xds.internal.GoogleDefaultXdsCredentialsProvider
io.grpc.xds.internal.InsecureXdsCredentialsProvider
io.grpc.xds.internal.JwtTokenFileXdsCredentialsProvider
io.grpc.xds.internal.TlsXdsCredentialsProvider
Loading
Loading