From 4baea6ba66309f3dad691c9f4ef4a8c6ecae7d09 Mon Sep 17 00:00:00 2001 From: Saurav Date: Mon, 10 Nov 2025 21:52:52 +0000 Subject: [PATCH 01/10] feat(xds): Add configuration objects for ExtAuthz and GrpcService This commit introduces configuration objects for the external authorization (ExtAuthz) filter and the gRPC service it uses. These classes provide a structured, immutable representation of the configuration defined in the xDS protobuf messages. The main new classes are: - `ExtAuthzConfig`: Represents the configuration for the `ExtAuthz` filter, including settings for the gRPC service, header mutation rules, and other filter behaviors. - `GrpcServiceConfig`: Represents the configuration for a gRPC service, including the target URI, credentials, and other settings. - `HeaderMutationRulesConfig`: Represents the configuration for header mutation rules. This commit also includes parsers to create these configuration objects from the corresponding protobuf messages, as well as unit tests for the new classes. --- .../xds/internal/extauthz/ExtAuthzConfig.java | 250 ++++++++++++++ .../extauthz/ExtAuthzParseException.java | 34 ++ .../grpcservice/GrpcServiceConfig.java | 308 ++++++++++++++++++ .../GrpcServiceConfigChannelFactory.java | 26 ++ .../GrpcServiceParseException.java | 33 ++ .../InsecureGrpcChannelFactory.java | 43 +++ .../HeaderMutationRulesConfig.java | 77 +++++ .../internal/extauthz/ExtAuthzConfigTest.java | 259 +++++++++++++++ .../grpcservice/GrpcServiceConfigTest.java | 243 ++++++++++++++ .../InsecureGrpcChannelFactoryTest.java | 57 ++++ .../HeaderMutationRulesConfigTest.java | 84 +++++ 11 files changed, 1414 insertions(+) create mode 100644 xds/src/main/java/io/grpc/xds/internal/extauthz/ExtAuthzConfig.java create mode 100644 xds/src/main/java/io/grpc/xds/internal/extauthz/ExtAuthzParseException.java create mode 100644 xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfig.java create mode 100644 xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigChannelFactory.java create mode 100644 xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceParseException.java create mode 100644 xds/src/main/java/io/grpc/xds/internal/grpcservice/InsecureGrpcChannelFactory.java create mode 100644 xds/src/main/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesConfig.java create mode 100644 xds/src/test/java/io/grpc/xds/internal/extauthz/ExtAuthzConfigTest.java create mode 100644 xds/src/test/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigTest.java create mode 100644 xds/src/test/java/io/grpc/xds/internal/grpcservice/InsecureGrpcChannelFactoryTest.java create mode 100644 xds/src/test/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesConfigTest.java diff --git a/xds/src/main/java/io/grpc/xds/internal/extauthz/ExtAuthzConfig.java b/xds/src/main/java/io/grpc/xds/internal/extauthz/ExtAuthzConfig.java new file mode 100644 index 00000000000..e826f501d9c --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/extauthz/ExtAuthzConfig.java @@ -0,0 +1,250 @@ +/* + * 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.extauthz; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import io.envoyproxy.envoy.config.common.mutation_rules.v3.HeaderMutationRules; +import io.envoyproxy.envoy.extensions.filters.http.ext_authz.v3.ExtAuthz; +import io.grpc.Status; +import io.grpc.internal.GrpcUtil; +import io.grpc.xds.internal.MatcherParser; +import io.grpc.xds.internal.Matchers; +import io.grpc.xds.internal.grpcservice.GrpcServiceConfig; +import io.grpc.xds.internal.grpcservice.GrpcServiceParseException; +import io.grpc.xds.internal.headermutations.HeaderMutationRulesConfig; +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * Represents the configuration for the external authorization (ext_authz) filter. This class + * encapsulates the settings defined in the + * {@link io.envoyproxy.envoy.extensions.filters.http.ext_authz.v3.ExtAuthz} proto, providing a + * structured, immutable representation for use within gRPC. It includes configurations for the gRPC + * service used for authorization, header mutation rules, and other filter behaviors. + */ +@AutoValue +public abstract class ExtAuthzConfig { + + /** Creates a new builder for creating {@link ExtAuthzConfig} instances. */ + public static Builder builder() { + return new AutoValue_ExtAuthzConfig.Builder().allowedHeaders(ImmutableList.of()) + .disallowedHeaders(ImmutableList.of()).statusOnError(Status.PERMISSION_DENIED) + .filterEnabled(Matchers.FractionMatcher.create(100, 100)); + } + + /** + * Parses the {@link io.envoyproxy.envoy.extensions.filters.http.ext_authz.v3.ExtAuthz} proto to + * create an {@link ExtAuthzConfig} instance. + * + * @param extAuthzProto The ext_authz proto to parse. + * @return An {@link ExtAuthzConfig} instance. + * @throws ExtAuthzParseException if the proto is invalid or contains unsupported features. + */ + public static ExtAuthzConfig fromProto(ExtAuthz extAuthzProto) throws ExtAuthzParseException { + if (!extAuthzProto.hasGrpcService()) { + throw new ExtAuthzParseException( + "unsupported ExtAuthz service type: only grpc_service is " + "supported"); + } + GrpcServiceConfig grpcServiceConfig; + try { + grpcServiceConfig = GrpcServiceConfig.fromProto(extAuthzProto.getGrpcService()); + } catch (GrpcServiceParseException e) { + throw new ExtAuthzParseException("Failed to parse GrpcService config: " + e.getMessage(), e); + } + Builder builder = builder().grpcService(grpcServiceConfig) + .failureModeAllow(extAuthzProto.getFailureModeAllow()) + .failureModeAllowHeaderAdd(extAuthzProto.getFailureModeAllowHeaderAdd()) + .includePeerCertificate(extAuthzProto.getIncludePeerCertificate()) + .denyAtDisable(extAuthzProto.getDenyAtDisable().getDefaultValue().getValue()); + + if (extAuthzProto.hasFilterEnabled()) { + builder.filterEnabled(parsePercent(extAuthzProto.getFilterEnabled().getDefaultValue())); + } + + if (extAuthzProto.hasStatusOnError()) { + builder.statusOnError( + GrpcUtil.httpStatusToGrpcStatus(extAuthzProto.getStatusOnError().getCodeValue())); + } + + if (extAuthzProto.hasAllowedHeaders()) { + builder.allowedHeaders(extAuthzProto.getAllowedHeaders().getPatternsList().stream() + .map(MatcherParser::parseStringMatcher).collect(ImmutableList.toImmutableList())); + } + + if (extAuthzProto.hasDisallowedHeaders()) { + builder.disallowedHeaders(extAuthzProto.getDisallowedHeaders().getPatternsList().stream() + .map(MatcherParser::parseStringMatcher).collect(ImmutableList.toImmutableList())); + } + + if (extAuthzProto.hasDecoderHeaderMutationRules()) { + builder.decoderHeaderMutationRules( + parseHeaderMutationRules(extAuthzProto.getDecoderHeaderMutationRules())); + } + + return builder.build(); + } + + /** + * The gRPC service configuration for the external authorization service. This is a required + * field. + * + * @see ExtAuthz#getGrpcService() + */ + public abstract GrpcServiceConfig grpcService(); + + /** + * Changes the filter's behavior on errors from the authorization service. If {@code true}, the + * filter will accept the request even if the authorization service fails or returns an error. + * + * @see ExtAuthz#getFailureModeAllow() + */ + public abstract boolean failureModeAllow(); + + /** + * Determines if the {@code x-envoy-auth-failure-mode-allowed} header is added to the request when + * {@link #failureModeAllow()} is true. + * + * @see ExtAuthz#getFailureModeAllowHeaderAdd() + */ + public abstract boolean failureModeAllowHeaderAdd(); + + /** + * Specifies if the peer certificate is sent to the external authorization service. + * + * @see ExtAuthz#getIncludePeerCertificate() + */ + public abstract boolean includePeerCertificate(); + + /** + * The gRPC status returned to the client when the authorization server returns an error or is + * unreachable. Defaults to {@code PERMISSION_DENIED}. + * + * @see io.envoyproxy.envoy.extensions.filters.http.ext_authz.v3.ExtAuthz#getStatusOnError() + */ + public abstract Status statusOnError(); + + /** + * Specifies whether to deny requests when the filter is disabled. Defaults to {@code false}. + * + * @see ExtAuthz#getDenyAtDisable() + */ + public abstract boolean denyAtDisable(); + + /** + * The fraction of requests that will be checked by the authorization service. Defaults to all + * requests. + * + * @see ExtAuthz#getFilterEnabled() + */ + public abstract Matchers.FractionMatcher filterEnabled(); + + /** + * Specifies which request headers are sent to the authorization service. If not set, all headers + * are sent. + * + * @see ExtAuthz#getAllowedHeaders() + */ + public abstract ImmutableList allowedHeaders(); + + /** + * Specifies which request headers are not sent to the authorization service. This overrides + * {@link #allowedHeaders()}. + * + * @see ExtAuthz#getDisallowedHeaders() + */ + public abstract ImmutableList disallowedHeaders(); + + /** + * Rules for what modifications an ext_authz server may make to request headers. + * + * @see ExtAuthz#getDecoderHeaderMutationRules() + */ + public abstract Optional decoderHeaderMutationRules(); + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder grpcService(GrpcServiceConfig grpcService); + + public abstract Builder failureModeAllow(boolean failureModeAllow); + + public abstract Builder failureModeAllowHeaderAdd(boolean failureModeAllowHeaderAdd); + + public abstract Builder includePeerCertificate(boolean includePeerCertificate); + + public abstract Builder statusOnError(Status statusOnError); + + public abstract Builder denyAtDisable(boolean denyAtDisable); + + public abstract Builder filterEnabled(Matchers.FractionMatcher filterEnabled); + + public abstract Builder allowedHeaders(Iterable allowedHeaders); + + public abstract Builder disallowedHeaders(Iterable disallowedHeaders); + + public abstract Builder decoderHeaderMutationRules(HeaderMutationRulesConfig rules); + + public abstract ExtAuthzConfig build(); + } + + + private static Matchers.FractionMatcher parsePercent( + io.envoyproxy.envoy.type.v3.FractionalPercent proto) throws ExtAuthzParseException { + int denominator; + switch (proto.getDenominator()) { + case HUNDRED: + denominator = 100; + break; + case TEN_THOUSAND: + denominator = 10_000; + break; + case MILLION: + denominator = 1_000_000; + break; + case UNRECOGNIZED: + default: + throw new ExtAuthzParseException("Unknown denominator type: " + proto.getDenominator()); + } + return Matchers.FractionMatcher.create(proto.getNumerator(), denominator); + } + + private static HeaderMutationRulesConfig parseHeaderMutationRules(HeaderMutationRules proto) + throws ExtAuthzParseException { + HeaderMutationRulesConfig.Builder builder = HeaderMutationRulesConfig.builder(); + builder.disallowAll(proto.getDisallowAll().getValue()); + builder.disallowIsError(proto.getDisallowIsError().getValue()); + if (proto.hasAllowExpression()) { + builder.allowExpression( + parseRegex(proto.getAllowExpression().getRegex(), "allow_expression")); + } + if (proto.hasDisallowExpression()) { + builder.disallowExpression( + parseRegex(proto.getDisallowExpression().getRegex(), "disallow_expression")); + } + return builder.build(); + } + + private static Pattern parseRegex(String regex, String fieldName) throws ExtAuthzParseException { + try { + return Pattern.compile(regex); + } catch (PatternSyntaxException e) { + throw new ExtAuthzParseException( + "Invalid regex pattern for " + fieldName + ": " + e.getMessage(), e); + } + } +} diff --git a/xds/src/main/java/io/grpc/xds/internal/extauthz/ExtAuthzParseException.java b/xds/src/main/java/io/grpc/xds/internal/extauthz/ExtAuthzParseException.java new file mode 100644 index 00000000000..78edea5c305 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/extauthz/ExtAuthzParseException.java @@ -0,0 +1,34 @@ +/* + * 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.extauthz; + +/** + * A custom exception for signaling errors during the parsing of external authorization + * (ext_authz) configurations. + */ +public class ExtAuthzParseException extends Exception { + + private static final long serialVersionUID = 0L; + + public ExtAuthzParseException(String message) { + super(message); + } + + public ExtAuthzParseException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfig.java b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfig.java new file mode 100644 index 00000000000..da9be978f87 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfig.java @@ -0,0 +1,308 @@ +/* + * 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.grpcservice; + +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.OAuth2Credentials; +import com.google.auto.value.AutoValue; +import com.google.common.io.BaseEncoding; +import com.google.protobuf.Any; +import com.google.protobuf.InvalidProtocolBufferException; +import io.envoyproxy.envoy.config.core.v3.GrpcService; +import io.envoyproxy.envoy.extensions.grpc_service.call_credentials.access_token.v3.AccessTokenCredentials; +import io.envoyproxy.envoy.extensions.grpc_service.channel_credentials.xds.v3.XdsCredentials; +import io.grpc.CallCredentials; +import io.grpc.ChannelCredentials; +import io.grpc.InsecureChannelCredentials; +import io.grpc.Metadata; +import io.grpc.alts.GoogleDefaultChannelCredentials; +import io.grpc.auth.MoreCallCredentials; +import io.grpc.xds.XdsChannelCredentials; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Optional; + + +/** + * A Java representation of the {@link io.envoyproxy.envoy.config.core.v3.GrpcService} proto, + * designed for parsing and internal use within gRPC. This class encapsulates the configuration for + * a gRPC service, including target URI, credentials, and other settings. The parsing logic adheres + * to the specifications outlined in + * A102: xDS GrpcService Support. This class is immutable and uses the AutoValue library for its + * implementation. + */ +@AutoValue +public abstract class GrpcServiceConfig { + + public static Builder builder() { + return new AutoValue_GrpcServiceConfig.Builder(); + } + + /** + * Parses the {@link io.envoyproxy.envoy.config.core.v3.GrpcService} proto to create a + * {@link GrpcServiceConfig} instance. This method adheres to gRFC A102, which specifies that only + * the {@code google_grpc} target specifier is supported. Other fields like {@code timeout} and + * {@code initial_metadata} are also parsed as per the gRFC. + * + * @param grpcServiceProto The proto to parse. + * @return A {@link GrpcServiceConfig} instance. + * @throws GrpcServiceParseException if the proto is invalid or uses unsupported features. + */ + public static GrpcServiceConfig fromProto(GrpcService grpcServiceProto) + throws GrpcServiceParseException { + if (!grpcServiceProto.hasGoogleGrpc()) { + throw new GrpcServiceParseException( + "Unsupported: GrpcService must have GoogleGrpc, got: " + grpcServiceProto); + } + GoogleGrpcConfig googleGrpcConfig = + GoogleGrpcConfig.fromProto(grpcServiceProto.getGoogleGrpc()); + + Builder builder = GrpcServiceConfig.builder().googleGrpc(googleGrpcConfig); + + if (!grpcServiceProto.getInitialMetadataList().isEmpty()) { + Metadata initialMetadata = new Metadata(); + for (io.envoyproxy.envoy.config.core.v3.HeaderValue header : grpcServiceProto + .getInitialMetadataList()) { + String key = header.getKey(); + if (key.endsWith(Metadata.BINARY_HEADER_SUFFIX)) { + initialMetadata.put(Metadata.Key.of(key, Metadata.BINARY_BYTE_MARSHALLER), + BaseEncoding.base64().decode(header.getValue())); + } else { + initialMetadata.put(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER), + header.getValue()); + } + } + builder.initialMetadata(initialMetadata); + } + + if (grpcServiceProto.hasTimeout()) { + com.google.protobuf.Duration timeout = grpcServiceProto.getTimeout(); + builder.timeout(Duration.ofSeconds(timeout.getSeconds(), timeout.getNanos())); + } + return builder.build(); + } + + public abstract GoogleGrpcConfig googleGrpc(); + + public abstract Optional timeout(); + + public abstract Optional initialMetadata(); + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder googleGrpc(GoogleGrpcConfig googleGrpc); + + public abstract Builder timeout(Duration timeout); + + public abstract Builder initialMetadata(Metadata initialMetadata); + + public abstract GrpcServiceConfig build(); + } + + /** + * Represents the configuration for a Google gRPC service, as defined in the + * {@link io.envoyproxy.envoy.config.core.v3.GrpcService.GoogleGrpc} proto. This class + * encapsulates settings specific to Google's gRPC implementation, such as target URI and + * credentials. The parsing of this configuration is guided by gRFC A102, which specifies how gRPC + * clients should interpret the GrpcService proto. + */ + @AutoValue + public abstract static class GoogleGrpcConfig { + + private static final String TLS_CREDENTIALS_TYPE_URL = + "type.googleapis.com/envoy.extensions.grpc_service.channel_credentials." + + "tls.v3.TlsCredentials"; + private static final String LOCAL_CREDENTIALS_TYPE_URL = + "type.googleapis.com/envoy.extensions.grpc_service.channel_credentials." + + "local.v3.LocalCredentials"; + private static final String XDS_CREDENTIALS_TYPE_URL = + "type.googleapis.com/envoy.extensions.grpc_service.channel_credentials." + + "xds.v3.XdsCredentials"; + private static final String INSECURE_CREDENTIALS_TYPE_URL = + "type.googleapis.com/envoy.extensions.grpc_service.channel_credentials." + + "insecure.v3.InsecureCredentials"; + private static final String GOOGLE_DEFAULT_CREDENTIALS_TYPE_URL = + "type.googleapis.com/envoy.extensions.grpc_service.channel_credentials." + + "google_default.v3.GoogleDefaultCredentials"; + + public static Builder builder() { + return new AutoValue_GrpcServiceConfig_GoogleGrpcConfig.Builder(); + } + + /** + * Parses the {@link io.envoyproxy.envoy.config.core.v3.GrpcService.GoogleGrpc} proto to create + * a {@link GoogleGrpcConfig} instance. + * + * @param googleGrpcProto The proto to parse. + * @return A {@link GoogleGrpcConfig} instance. + * @throws GrpcServiceParseException if the proto is invalid. + */ + public static GoogleGrpcConfig fromProto(GrpcService.GoogleGrpc googleGrpcProto) + throws GrpcServiceParseException { + + HashedChannelCredentials channelCreds = + extractChannelCredentials(googleGrpcProto.getChannelCredentialsPluginList()); + + CallCredentials callCreds = + extractCallCredentials(googleGrpcProto.getCallCredentialsPluginList()); + + return GoogleGrpcConfig.builder().target(googleGrpcProto.getTargetUri()) + .hashedChannelCredentials(channelCreds).callCredentials(callCreds).build(); + } + + public abstract String target(); + + public abstract HashedChannelCredentials hashedChannelCredentials(); + + public abstract CallCredentials callCredentials(); + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder target(String target); + + public abstract Builder hashedChannelCredentials(HashedChannelCredentials channelCredentials); + + public abstract Builder callCredentials(CallCredentials callCredentials); + + public abstract GoogleGrpcConfig build(); + } + + private static T getFirstSupported(List configs, Parser parser, + String configName) throws GrpcServiceParseException { + List errors = new ArrayList<>(); + for (U config : configs) { + try { + return parser.parse(config); + } catch (GrpcServiceParseException e) { + errors.add(e.getMessage()); + } + } + throw new GrpcServiceParseException( + "No valid supported " + configName + " found. Errors: " + errors); + } + + private static HashedChannelCredentials channelCredsFromProto(Any cred) + throws GrpcServiceParseException { + String typeUrl = cred.getTypeUrl(); + try { + switch (typeUrl) { + case GOOGLE_DEFAULT_CREDENTIALS_TYPE_URL: + return HashedChannelCredentials.of(GoogleDefaultChannelCredentials.create(), + cred.hashCode()); + case INSECURE_CREDENTIALS_TYPE_URL: + return HashedChannelCredentials.of(InsecureChannelCredentials.create(), + cred.hashCode()); + case XDS_CREDENTIALS_TYPE_URL: + XdsCredentials xdsConfig = cred.unpack(XdsCredentials.class); + HashedChannelCredentials fallbackCreds = + channelCredsFromProto(xdsConfig.getFallbackCredentials()); + return HashedChannelCredentials.of( + XdsChannelCredentials.create(fallbackCreds.channelCredentials()), cred.hashCode()); + case LOCAL_CREDENTIALS_TYPE_URL: + // TODO(sauravzg) : What's the java alternative to LocalCredentials. + throw new GrpcServiceParseException("LocalCredentials are not yet supported."); + case TLS_CREDENTIALS_TYPE_URL: + // TODO(sauravzg) : How to instantiate a TlsChannelCredentials from TlsCredentials + // proto? + throw new GrpcServiceParseException("TlsCredentials are not yet supported."); + default: + throw new GrpcServiceParseException("Unsupported channel credentials type: " + typeUrl); + } + } catch (InvalidProtocolBufferException e) { + // TODO(sauravzg): Add unit tests when we have a solution for TLS creds. + // This code is as of writing unreachable because all channel credential message + // types except TLS are empty messages. + throw new GrpcServiceParseException( + "Failed to parse channel credentials: " + e.getMessage()); + } + } + + private static CallCredentials callCredsFromProto(Any cred) throws GrpcServiceParseException { + try { + AccessTokenCredentials accessToken = cred.unpack(AccessTokenCredentials.class); + // TODO(sauravzg): Verify if the current behavior is per spec.The `AccessTokenCredentials` + // config doesn't have any timeout/refresh, so set the token to never expire. + return MoreCallCredentials.from(OAuth2Credentials + .create(new AccessToken(accessToken.getToken(), new Date(Long.MAX_VALUE)))); + } catch (InvalidProtocolBufferException e) { + throw new GrpcServiceParseException( + "Unsupported call credentials type: " + cred.getTypeUrl()); + } + } + + private static HashedChannelCredentials extractChannelCredentials( + List channelCredentialPlugins) throws GrpcServiceParseException { + return getFirstSupported(channelCredentialPlugins, GoogleGrpcConfig::channelCredsFromProto, + "channel_credentials"); + } + + private static CallCredentials extractCallCredentials(List callCredentialPlugins) + throws GrpcServiceParseException { + return getFirstSupported(callCredentialPlugins, GoogleGrpcConfig::callCredsFromProto, + "call_credentials"); + } + } + + /** + * A container for {@link ChannelCredentials} and a hash for the purpose of caching. + */ + @AutoValue + public abstract static class HashedChannelCredentials { + /** + * Creates a new {@link HashedChannelCredentials} instance. + * + * @param creds The channel credentials. + * @param hash The hash of the credentials. + * @return A new {@link HashedChannelCredentials} instance. + */ + public static HashedChannelCredentials of(ChannelCredentials creds, int hash) { + return new AutoValue_GrpcServiceConfig_HashedChannelCredentials(creds, hash); + } + + /** + * Returns the channel credentials. + */ + public abstract ChannelCredentials channelCredentials(); + + /** + * Returns the hash of the credentials. + */ + public abstract int hash(); + } + + /** + * Defines a generic interface for parsing a configuration of type {@code U} into a result of type + * {@code T}. This functional interface is used to abstract the parsing logic for different parts + * of the GrpcService configuration. + * + * @param The type of the object that will be returned after parsing. + * @param The type of the configuration object that will be parsed. + */ + private interface Parser { + + /** + * Parses the given configuration. + * + * @param config The configuration object to parse. + * @return The parsed object of type {@code T}. + * @throws GrpcServiceParseException if an error occurs during parsing. + */ + T parse(U config) throws GrpcServiceParseException; + } +} diff --git a/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigChannelFactory.java b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigChannelFactory.java new file mode 100644 index 00000000000..0d02989eaa3 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigChannelFactory.java @@ -0,0 +1,26 @@ +/* + * 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.grpcservice; + +import io.grpc.ManagedChannel; + +/** + * A factory for creating {@link ManagedChannel}s from a {@link GrpcServiceConfig}. + */ +public interface GrpcServiceConfigChannelFactory { + ManagedChannel createChannel(GrpcServiceConfig config); +} diff --git a/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceParseException.java b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceParseException.java new file mode 100644 index 00000000000..319ad3d07e3 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceParseException.java @@ -0,0 +1,33 @@ +/* + * 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.grpcservice; + +/** + * Exception thrown when there is an error parsing the gRPC service config. + */ +public class GrpcServiceParseException extends Exception { + + private static final long serialVersionUID = 1L; + + public GrpcServiceParseException(String message) { + super(message); + } + + public GrpcServiceParseException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/xds/src/main/java/io/grpc/xds/internal/grpcservice/InsecureGrpcChannelFactory.java b/xds/src/main/java/io/grpc/xds/internal/grpcservice/InsecureGrpcChannelFactory.java new file mode 100644 index 00000000000..d6325d43be4 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/grpcservice/InsecureGrpcChannelFactory.java @@ -0,0 +1,43 @@ +/* + * 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.grpcservice; + +import io.grpc.Grpc; +import io.grpc.ManagedChannel; + +/** + * An insecure implementation of {@link GrpcServiceConfigChannelFactory} that creates a plaintext + * channel. This is a stub implementation for channel creation until the GrpcService trusted server + * implementation is completely implemented. + */ +public final class InsecureGrpcChannelFactory implements GrpcServiceConfigChannelFactory { + + private static final InsecureGrpcChannelFactory INSTANCE = new InsecureGrpcChannelFactory(); + + private InsecureGrpcChannelFactory() {} + + public static InsecureGrpcChannelFactory getInstance() { + return INSTANCE; + } + + @Override + public ManagedChannel createChannel(GrpcServiceConfig config) { + GrpcServiceConfig.GoogleGrpcConfig googleGrpc = config.googleGrpc(); + return Grpc.newChannelBuilder(googleGrpc.target(), + googleGrpc.hashedChannelCredentials().channelCredentials()).build(); + } +} diff --git a/xds/src/main/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesConfig.java b/xds/src/main/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesConfig.java new file mode 100644 index 00000000000..fd8048fdbd2 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesConfig.java @@ -0,0 +1,77 @@ +/* + * 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.headermutations; + +import com.google.auto.value.AutoValue; +import io.envoyproxy.envoy.config.common.mutation_rules.v3.HeaderMutationRules; +import java.util.Optional; +import java.util.regex.Pattern; + +/** + * Represents the configuration for header mutation rules, as defined in the + * {@link io.envoyproxy.envoy.config.common.mutation_rules.v3.HeaderMutationRules} proto. + */ +@AutoValue +public abstract class HeaderMutationRulesConfig { + /** Creates a new builder for creating {@link HeaderMutationRulesConfig} instances. */ + public static Builder builder() { + return new AutoValue_HeaderMutationRulesConfig.Builder().disallowAll(false) + .disallowIsError(false); + } + + /** + * If set, allows any header that matches this regular expression. + * + * @see HeaderMutationRules#getAllowExpression() + */ + public abstract Optional allowExpression(); + + /** + * If set, disallows any header that matches this regular expression. + * + * @see HeaderMutationRules#getDisallowExpression() + */ + public abstract Optional disallowExpression(); + + /** + * If true, disallows all header mutations. + * + * @see HeaderMutationRules#getDisallowAll() + */ + public abstract boolean disallowAll(); + + /** + * If true, disallows any header mutation that would result in an invalid header value. + * + * @see HeaderMutationRules#getDisallowIsError() + */ + public abstract boolean disallowIsError(); + + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder allowExpression(Pattern matcher); + + public abstract Builder disallowExpression(Pattern matcher); + + public abstract Builder disallowAll(boolean disallowAll); + + public abstract Builder disallowIsError(boolean disallowIsError); + + public abstract HeaderMutationRulesConfig build(); + } +} diff --git a/xds/src/test/java/io/grpc/xds/internal/extauthz/ExtAuthzConfigTest.java b/xds/src/test/java/io/grpc/xds/internal/extauthz/ExtAuthzConfigTest.java new file mode 100644 index 00000000000..9b9a55b4079 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/internal/extauthz/ExtAuthzConfigTest.java @@ -0,0 +1,259 @@ +/* + * 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.extauthz; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.protobuf.Any; +import com.google.protobuf.BoolValue; +import io.envoyproxy.envoy.config.common.mutation_rules.v3.HeaderMutationRules; +import io.envoyproxy.envoy.config.core.v3.HeaderValue; +import io.envoyproxy.envoy.config.core.v3.RuntimeFeatureFlag; +import io.envoyproxy.envoy.config.core.v3.RuntimeFractionalPercent; +import io.envoyproxy.envoy.extensions.filters.http.ext_authz.v3.ExtAuthz; +import io.envoyproxy.envoy.extensions.grpc_service.call_credentials.access_token.v3.AccessTokenCredentials; +import io.envoyproxy.envoy.extensions.grpc_service.channel_credentials.google_default.v3.GoogleDefaultCredentials; +import io.envoyproxy.envoy.type.matcher.v3.ListStringMatcher; +import io.envoyproxy.envoy.type.matcher.v3.RegexMatcher; +import io.envoyproxy.envoy.type.matcher.v3.StringMatcher; +import io.envoyproxy.envoy.type.v3.FractionalPercent; +import io.envoyproxy.envoy.type.v3.FractionalPercent.DenominatorType; +import io.grpc.Status; +import io.grpc.xds.internal.Matchers; +import io.grpc.xds.internal.headermutations.HeaderMutationRulesConfig; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ExtAuthzConfigTest { + + private static final Any GOOGLE_DEFAULT_CHANNEL_CREDS = + Any.pack(GoogleDefaultCredentials.newBuilder().build()); + private static final Any FAKE_ACCESS_TOKEN_CALL_CREDS = + Any.pack(AccessTokenCredentials.newBuilder().build()); + + private ExtAuthz.Builder extAuthzBuilder; + + @Before + public void setUp() { + extAuthzBuilder = ExtAuthz.newBuilder() + .setGrpcService(io.envoyproxy.envoy.config.core.v3.GrpcService.newBuilder() + .setGoogleGrpc(io.envoyproxy.envoy.config.core.v3.GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("test-cluster") + .addChannelCredentialsPlugin(GOOGLE_DEFAULT_CHANNEL_CREDS) + .addCallCredentialsPlugin(FAKE_ACCESS_TOKEN_CALL_CREDS).build()) + .build()); + } + + @Test + public void fromProto_missingGrpcService_throws() { + ExtAuthz extAuthz = ExtAuthz.newBuilder().build(); + try { + ExtAuthzConfig.fromProto(extAuthz); + fail("Expected ExtAuthzParseException"); + } catch (ExtAuthzParseException e) { + assertThat(e).hasMessageThat() + .isEqualTo("unsupported ExtAuthz service type: only grpc_service is supported"); + } + } + + @Test + public void fromProto_invalidGrpcService_throws() { + ExtAuthz extAuthz = ExtAuthz.newBuilder() + .setGrpcService(io.envoyproxy.envoy.config.core.v3.GrpcService.newBuilder().build()) + .build(); + try { + ExtAuthzConfig.fromProto(extAuthz); + fail("Expected ExtAuthzParseException"); + } catch (ExtAuthzParseException e) { + assertThat(e).hasMessageThat().startsWith("Failed to parse GrpcService config:"); + } + } + + @Test + public void fromProto_invalidAllowExpression_throws() { + ExtAuthz extAuthz = extAuthzBuilder + .setDecoderHeaderMutationRules(HeaderMutationRules.newBuilder() + .setAllowExpression(RegexMatcher.newBuilder().setRegex("[invalid").build()).build()) + .build(); + try { + ExtAuthzConfig.fromProto(extAuthz); + fail("Expected ExtAuthzParseException"); + } catch (ExtAuthzParseException e) { + assertThat(e).hasMessageThat().startsWith("Invalid regex pattern for allow_expression:"); + } + } + + @Test + public void fromProto_invalidDisallowExpression_throws() { + ExtAuthz extAuthz = extAuthzBuilder + .setDecoderHeaderMutationRules(HeaderMutationRules.newBuilder() + .setDisallowExpression(RegexMatcher.newBuilder().setRegex("[invalid").build()).build()) + .build(); + try { + ExtAuthzConfig.fromProto(extAuthz); + fail("Expected ExtAuthzParseException"); + } catch (ExtAuthzParseException e) { + assertThat(e).hasMessageThat().startsWith("Invalid regex pattern for disallow_expression:"); + } + } + + @Test + public void fromProto_success() throws ExtAuthzParseException { + ExtAuthz extAuthz = extAuthzBuilder + .setGrpcService(extAuthzBuilder.getGrpcServiceBuilder() + .setTimeout(com.google.protobuf.Duration.newBuilder().setSeconds(5).build()) + .addInitialMetadata(HeaderValue.newBuilder().setKey("key").setValue("value").build()) + .build()) + .setFailureModeAllow(true).setFailureModeAllowHeaderAdd(true) + .setIncludePeerCertificate(true) + .setStatusOnError( + io.envoyproxy.envoy.type.v3.HttpStatus.newBuilder().setCodeValue(403).build()) + .setDenyAtDisable( + RuntimeFeatureFlag.newBuilder().setDefaultValue(BoolValue.of(true)).build()) + .setFilterEnabled(RuntimeFractionalPercent.newBuilder() + .setDefaultValue(FractionalPercent.newBuilder().setNumerator(50) + .setDenominator(DenominatorType.TEN_THOUSAND).build()) + .build()) + .setAllowedHeaders(ListStringMatcher.newBuilder() + .addPatterns(StringMatcher.newBuilder().setExact("allowed-header").build()).build()) + .setDisallowedHeaders(ListStringMatcher.newBuilder() + .addPatterns(StringMatcher.newBuilder().setPrefix("disallowed-").build()).build()) + .setDecoderHeaderMutationRules(HeaderMutationRules.newBuilder() + .setAllowExpression(RegexMatcher.newBuilder().setRegex("allow.*").build()) + .setDisallowExpression(RegexMatcher.newBuilder().setRegex("disallow.*").build()) + .setDisallowAll(BoolValue.of(true)).setDisallowIsError(BoolValue.of(true)).build()) + .build(); + + ExtAuthzConfig config = ExtAuthzConfig.fromProto(extAuthz); + + assertThat(config.grpcService().googleGrpc().target()).isEqualTo("test-cluster"); + assertThat(config.grpcService().timeout().get().getSeconds()).isEqualTo(5); + assertThat(config.grpcService().initialMetadata().isPresent()).isTrue(); + assertThat(config.failureModeAllow()).isTrue(); + assertThat(config.failureModeAllowHeaderAdd()).isTrue(); + assertThat(config.includePeerCertificate()).isTrue(); + assertThat(config.statusOnError().getCode()).isEqualTo(Status.PERMISSION_DENIED.getCode()); + assertThat(config.statusOnError().getDescription()).isEqualTo("HTTP status code 403"); + assertThat(config.denyAtDisable()).isTrue(); + assertThat(config.filterEnabled()).isEqualTo(Matchers.FractionMatcher.create(50, 10_000)); + assertThat(config.allowedHeaders()).hasSize(1); + assertThat(config.allowedHeaders().get(0).matches("allowed-header")).isTrue(); + assertThat(config.disallowedHeaders()).hasSize(1); + assertThat(config.disallowedHeaders().get(0).matches("disallowed-foo")).isTrue(); + assertThat(config.decoderHeaderMutationRules().isPresent()).isTrue(); + HeaderMutationRulesConfig rules = config.decoderHeaderMutationRules().get(); + assertThat(rules.allowExpression().get().pattern()).isEqualTo("allow.*"); + assertThat(rules.disallowExpression().get().pattern()).isEqualTo("disallow.*"); + assertThat(rules.disallowAll()).isTrue(); + assertThat(rules.disallowIsError()).isTrue(); + } + + @Test + public void fromProto_saneDefaults() throws ExtAuthzParseException { + ExtAuthz extAuthz = extAuthzBuilder.build(); + + ExtAuthzConfig config = ExtAuthzConfig.fromProto(extAuthz); + + assertThat(config.failureModeAllow()).isFalse(); + assertThat(config.failureModeAllowHeaderAdd()).isFalse(); + assertThat(config.includePeerCertificate()).isFalse(); + assertThat(config.statusOnError()).isEqualTo(Status.PERMISSION_DENIED); + assertThat(config.denyAtDisable()).isFalse(); + assertThat(config.filterEnabled()).isEqualTo(Matchers.FractionMatcher.create(100, 100)); + assertThat(config.allowedHeaders()).isEmpty(); + assertThat(config.disallowedHeaders()).isEmpty(); + assertThat(config.decoderHeaderMutationRules().isPresent()).isFalse(); + } + + @Test + public void fromProto_headerMutationRules_allowExpressionOnly() throws ExtAuthzParseException { + ExtAuthz extAuthz = extAuthzBuilder + .setDecoderHeaderMutationRules(HeaderMutationRules.newBuilder() + .setAllowExpression(RegexMatcher.newBuilder().setRegex("allow.*").build()).build()) + .build(); + + ExtAuthzConfig config = ExtAuthzConfig.fromProto(extAuthz); + + assertThat(config.decoderHeaderMutationRules().isPresent()).isTrue(); + HeaderMutationRulesConfig rules = config.decoderHeaderMutationRules().get(); + assertThat(rules.allowExpression().get().pattern()).isEqualTo("allow.*"); + assertThat(rules.disallowExpression().isPresent()).isFalse(); + } + + @Test + public void fromProto_headerMutationRules_disallowExpressionOnly() throws ExtAuthzParseException { + ExtAuthz extAuthz = extAuthzBuilder + .setDecoderHeaderMutationRules(HeaderMutationRules.newBuilder() + .setDisallowExpression(RegexMatcher.newBuilder().setRegex("disallow.*").build()) + .build()) + .build(); + + ExtAuthzConfig config = ExtAuthzConfig.fromProto(extAuthz); + + assertThat(config.decoderHeaderMutationRules().isPresent()).isTrue(); + HeaderMutationRulesConfig rules = config.decoderHeaderMutationRules().get(); + assertThat(rules.allowExpression().isPresent()).isFalse(); + assertThat(rules.disallowExpression().get().pattern()).isEqualTo("disallow.*"); + } + + @Test + public void fromProto_filterEnabled_hundred() throws ExtAuthzParseException { + ExtAuthz extAuthz = extAuthzBuilder + .setFilterEnabled(RuntimeFractionalPercent.newBuilder().setDefaultValue(FractionalPercent + .newBuilder().setNumerator(25).setDenominator(DenominatorType.HUNDRED).build()).build()) + .build(); + + ExtAuthzConfig config = ExtAuthzConfig.fromProto(extAuthz); + + assertThat(config.filterEnabled()).isEqualTo(Matchers.FractionMatcher.create(25, 100)); + } + + @Test + public void fromProto_filterEnabled_million() throws ExtAuthzParseException { + ExtAuthz extAuthz = extAuthzBuilder + .setFilterEnabled( + RuntimeFractionalPercent.newBuilder().setDefaultValue(FractionalPercent.newBuilder() + .setNumerator(123456).setDenominator(DenominatorType.MILLION).build()).build()) + .build(); + + ExtAuthzConfig config = ExtAuthzConfig.fromProto(extAuthz); + + assertThat(config.filterEnabled()) + .isEqualTo(Matchers.FractionMatcher.create(123456, 1_000_000)); + } + + @Test + public void fromProto_filterEnabled_unrecognizedDenominator() { + ExtAuthz extAuthz = extAuthzBuilder + .setFilterEnabled(RuntimeFractionalPercent.newBuilder() + .setDefaultValue( + FractionalPercent.newBuilder().setNumerator(1).setDenominatorValue(4).build()) + .build()) + .build(); + + try { + ExtAuthzConfig.fromProto(extAuthz); + fail("Expected ExtAuthzParseException"); + } catch (ExtAuthzParseException e) { + assertThat(e).hasMessageThat().isEqualTo("Unknown denominator type: UNRECOGNIZED"); + } + } +} \ No newline at end of file diff --git a/xds/src/test/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigTest.java b/xds/src/test/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigTest.java new file mode 100644 index 00000000000..7a506220973 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigTest.java @@ -0,0 +1,243 @@ +/* + * 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.grpcservice; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.io.BaseEncoding; +import com.google.protobuf.Any; +import com.google.protobuf.Duration; +import io.envoyproxy.envoy.config.core.v3.GrpcService; +import io.envoyproxy.envoy.config.core.v3.HeaderValue; +import io.envoyproxy.envoy.extensions.grpc_service.call_credentials.access_token.v3.AccessTokenCredentials; +import io.envoyproxy.envoy.extensions.grpc_service.channel_credentials.google_default.v3.GoogleDefaultCredentials; +import io.envoyproxy.envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials; +import io.envoyproxy.envoy.extensions.grpc_service.channel_credentials.local.v3.LocalCredentials; +import io.envoyproxy.envoy.extensions.grpc_service.channel_credentials.xds.v3.XdsCredentials; +import io.grpc.InsecureChannelCredentials; +import io.grpc.Metadata; +import java.nio.charset.StandardCharsets; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class GrpcServiceConfigTest { + + @Test + public void fromProto_success() throws GrpcServiceParseException { + Any insecureCreds = Any.pack(InsecureCredentials.getDefaultInstance()); + Any accessTokenCreds = + Any.pack(AccessTokenCredentials.newBuilder().setToken("test_token").build()); + GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") + .addChannelCredentialsPlugin(insecureCreds).addCallCredentialsPlugin(accessTokenCreds) + .build(); + HeaderValue asciiHeader = + HeaderValue.newBuilder().setKey("test_key").setValue("test_value").build(); + HeaderValue binaryHeader = HeaderValue.newBuilder().setKey("test_key-bin") + .setValue( + BaseEncoding.base64().encode("test_value_binary".getBytes(StandardCharsets.UTF_8))) + .build(); + Duration timeout = Duration.newBuilder().setSeconds(10).build(); + GrpcService grpcService = + GrpcService.newBuilder().setGoogleGrpc(googleGrpc).addInitialMetadata(asciiHeader) + .addInitialMetadata(binaryHeader).setTimeout(timeout).build(); + + GrpcServiceConfig config = GrpcServiceConfig.fromProto(grpcService); + + // Assert target URI + assertThat(config.googleGrpc().target()).isEqualTo("test_uri"); + + // Assert channel credentials + assertThat(config.googleGrpc().hashedChannelCredentials().channelCredentials()) + .isInstanceOf(InsecureChannelCredentials.class); + assertThat(config.googleGrpc().hashedChannelCredentials().hash()) + .isEqualTo(insecureCreds.hashCode()); + + // Assert call credentials + assertThat(config.googleGrpc().callCredentials().getClass().getName()) + .isEqualTo("io.grpc.auth.GoogleAuthLibraryCallCredentials"); + + // Assert initial metadata + assertThat(config.initialMetadata().isPresent()).isTrue(); + assertThat(config.initialMetadata().get() + .get(Metadata.Key.of("test_key", Metadata.ASCII_STRING_MARSHALLER))) + .isEqualTo("test_value"); + assertThat(config.initialMetadata().get() + .get(Metadata.Key.of("test_key-bin", Metadata.BINARY_BYTE_MARSHALLER))) + .isEqualTo("test_value_binary".getBytes(StandardCharsets.UTF_8)); + + // Assert timeout + assertThat(config.timeout().isPresent()).isTrue(); + assertThat(config.timeout().get()).isEqualTo(java.time.Duration.ofSeconds(10)); + } + + @Test + public void fromProto_minimalSuccess_defaults() throws GrpcServiceParseException { + Any insecureCreds = Any.pack(InsecureCredentials.getDefaultInstance()); + Any accessTokenCreds = + Any.pack(AccessTokenCredentials.newBuilder().setToken("test_token").build()); + GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") + .addChannelCredentialsPlugin(insecureCreds).addCallCredentialsPlugin(accessTokenCreds) + .build(); + GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); + + GrpcServiceConfig config = GrpcServiceConfig.fromProto(grpcService); + + assertThat(config.googleGrpc().target()).isEqualTo("test_uri"); + assertThat(config.initialMetadata().isPresent()).isFalse(); + assertThat(config.timeout().isPresent()).isFalse(); + } + + @Test + public void fromProto_missingGoogleGrpc() { + GrpcService grpcService = GrpcService.newBuilder().build(); + GrpcServiceParseException exception = assertThrows(GrpcServiceParseException.class, + () -> GrpcServiceConfig.fromProto(grpcService)); + assertThat(exception).hasMessageThat() + .startsWith("Unsupported: GrpcService must have GoogleGrpc, got: "); + } + + @Test + public void fromProto_emptyCallCredentials() { + Any insecureCreds = Any.pack(InsecureCredentials.getDefaultInstance()); + GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") + .addChannelCredentialsPlugin(insecureCreds).build(); + GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); + GrpcServiceParseException exception = assertThrows(GrpcServiceParseException.class, + () -> GrpcServiceConfig.fromProto(grpcService)); + assertThat(exception).hasMessageThat() + .isEqualTo("No valid supported call_credentials found. Errors: []"); + } + + @Test + public void fromProto_emptyChannelCredentials() { + Any accessTokenCreds = + Any.pack(AccessTokenCredentials.newBuilder().setToken("test_token").build()); + GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") + .addCallCredentialsPlugin(accessTokenCreds).build(); + GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); + GrpcServiceParseException exception = assertThrows(GrpcServiceParseException.class, + () -> GrpcServiceConfig.fromProto(grpcService)); + assertThat(exception).hasMessageThat() + .isEqualTo("No valid supported channel_credentials found. Errors: []"); + } + + @Test + public void fromProto_googleDefaultCredentials() throws GrpcServiceParseException { + Any googleDefaultCreds = Any.pack(GoogleDefaultCredentials.getDefaultInstance()); + Any accessTokenCreds = + Any.pack(AccessTokenCredentials.newBuilder().setToken("test_token").build()); + GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") + .addChannelCredentialsPlugin(googleDefaultCreds).addCallCredentialsPlugin(accessTokenCreds) + .build(); + GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); + + GrpcServiceConfig config = GrpcServiceConfig.fromProto(grpcService); + + assertThat(config.googleGrpc().hashedChannelCredentials().channelCredentials()) + .isInstanceOf(io.grpc.CompositeChannelCredentials.class); + assertThat(config.googleGrpc().hashedChannelCredentials().hash()) + .isEqualTo(googleDefaultCreds.hashCode()); + } + + @Test + public void fromProto_localCredentials() throws GrpcServiceParseException { + Any localCreds = Any.pack(LocalCredentials.getDefaultInstance()); + Any accessTokenCreds = + Any.pack(AccessTokenCredentials.newBuilder().setToken("test_token").build()); + GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") + .addChannelCredentialsPlugin(localCreds).addCallCredentialsPlugin(accessTokenCreds).build(); + GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); + + GrpcServiceParseException exception = assertThrows(GrpcServiceParseException.class, + () -> GrpcServiceConfig.fromProto(grpcService)); + assertThat(exception).hasMessageThat().contains("LocalCredentials are not yet supported."); + } + + @Test + public void fromProto_xdsCredentials_withInsecureFallback() throws GrpcServiceParseException { + Any insecureCreds = Any.pack(InsecureCredentials.getDefaultInstance()); + XdsCredentials xdsCreds = + XdsCredentials.newBuilder().setFallbackCredentials(insecureCreds).build(); + Any xdsCredsAny = Any.pack(xdsCreds); + Any accessTokenCreds = + Any.pack(AccessTokenCredentials.newBuilder().setToken("test_token").build()); + GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") + .addChannelCredentialsPlugin(xdsCredsAny).addCallCredentialsPlugin(accessTokenCreds) + .build(); + GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); + + GrpcServiceConfig config = GrpcServiceConfig.fromProto(grpcService); + + assertThat(config.googleGrpc().hashedChannelCredentials().channelCredentials()) + .isInstanceOf(io.grpc.ChannelCredentials.class); + assertThat(config.googleGrpc().hashedChannelCredentials().hash()) + .isEqualTo(xdsCredsAny.hashCode()); + } + + @Test + public void fromProto_tlsCredentials_notSupported() { + Any tlsCreds = Any + .pack(io.envoyproxy.envoy.extensions.grpc_service.channel_credentials.tls.v3.TlsCredentials + .getDefaultInstance()); + Any accessTokenCreds = + Any.pack(AccessTokenCredentials.newBuilder().setToken("test_token").build()); + GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") + .addChannelCredentialsPlugin(tlsCreds).addCallCredentialsPlugin(accessTokenCreds).build(); + GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); + + GrpcServiceParseException exception = assertThrows(GrpcServiceParseException.class, + () -> GrpcServiceConfig.fromProto(grpcService)); + assertThat(exception).hasMessageThat().contains("TlsCredentials are not yet supported."); + } + + @Test + public void fromProto_invalidChannelCredentialsProto() { + // Pack a Duration proto, but try to unpack it as GoogleDefaultCredentials + Any invalidCreds = Any.pack(com.google.protobuf.Duration.getDefaultInstance()); + Any accessTokenCreds = + Any.pack(AccessTokenCredentials.newBuilder().setToken("test_token").build()); + GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") + .addChannelCredentialsPlugin(invalidCreds).addCallCredentialsPlugin(accessTokenCreds) + .build(); + GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); + + GrpcServiceParseException exception = assertThrows(GrpcServiceParseException.class, + () -> GrpcServiceConfig.fromProto(grpcService)); + assertThat(exception).hasMessageThat() + .contains("No valid supported channel_credentials found. Errors: [Unsupported channel " + + "credentials type: type.googleapis.com/google.protobuf.Duration"); + } + + @Test + public void fromProto_invalidCallCredentialsProto() { + // Pack a Duration proto, but try to unpack it as AccessTokenCredentials + Any insecureCreds = Any.pack(InsecureCredentials.getDefaultInstance()); + Any invalidCallCredentials = Any.pack(Duration.getDefaultInstance()); + GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") + .addChannelCredentialsPlugin(insecureCreds).addCallCredentialsPlugin(invalidCallCredentials) + .build(); + GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); + + GrpcServiceParseException exception = assertThrows(GrpcServiceParseException.class, + () -> GrpcServiceConfig.fromProto(grpcService)); + assertThat(exception).hasMessageThat().contains("Unsupported call credentials type:"); + } +} + diff --git a/xds/src/test/java/io/grpc/xds/internal/grpcservice/InsecureGrpcChannelFactoryTest.java b/xds/src/test/java/io/grpc/xds/internal/grpcservice/InsecureGrpcChannelFactoryTest.java new file mode 100644 index 00000000000..8d7347f56c6 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/internal/grpcservice/InsecureGrpcChannelFactoryTest.java @@ -0,0 +1,57 @@ +/* + * 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.grpcservice; + +import static org.junit.Assert.assertNotNull; + +import io.grpc.CallCredentials; +import io.grpc.InsecureChannelCredentials; +import io.grpc.ManagedChannel; +import io.grpc.Metadata; +import io.grpc.xds.internal.grpcservice.GrpcServiceConfig.GoogleGrpcConfig; +import io.grpc.xds.internal.grpcservice.GrpcServiceConfig.HashedChannelCredentials; +import java.util.concurrent.Executor; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link InsecureGrpcChannelFactory}. */ +@RunWith(JUnit4.class) +public class InsecureGrpcChannelFactoryTest { + + private static final class NoOpCallCredentials extends CallCredentials { + @Override + public void applyRequestMetadata(RequestInfo requestInfo, Executor appExecutor, + MetadataApplier applier) { + applier.apply(new Metadata()); + } + } + + @Test + public void testCreateChannel() { + InsecureGrpcChannelFactory factory = InsecureGrpcChannelFactory.getInstance(); + GrpcServiceConfig config = GrpcServiceConfig.builder() + .googleGrpc(GoogleGrpcConfig.builder().target("localhost:8080") + .hashedChannelCredentials( + HashedChannelCredentials.of(InsecureChannelCredentials.create(), 0)) + .callCredentials(new NoOpCallCredentials()).build()) + .build(); + ManagedChannel channel = factory.createChannel(config); + assertNotNull(channel); + channel.shutdownNow(); + } +} diff --git a/xds/src/test/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesConfigTest.java b/xds/src/test/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesConfigTest.java new file mode 100644 index 00000000000..e2bda9cb836 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesConfigTest.java @@ -0,0 +1,84 @@ +/* + * 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.headermutations; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.regex.Pattern; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class HeaderMutationRulesConfigTest { + @Test + public void testBuilderDefaultValues() { + HeaderMutationRulesConfig config = HeaderMutationRulesConfig.builder().build(); + assertFalse(config.disallowAll()); + assertFalse(config.disallowIsError()); + assertThat(config.allowExpression()).isEmpty(); + assertThat(config.disallowExpression()).isEmpty(); + } + + @Test + public void testBuilder_setDisallowAll() { + HeaderMutationRulesConfig config = + HeaderMutationRulesConfig.builder().disallowAll(true).build(); + assertTrue(config.disallowAll()); + } + + @Test + public void testBuilder_setDisallowIsError() { + HeaderMutationRulesConfig config = + HeaderMutationRulesConfig.builder().disallowIsError(true).build(); + assertTrue(config.disallowIsError()); + } + + @Test + public void testBuilder_setAllowExpression() { + Pattern pattern = Pattern.compile("allow.*"); + HeaderMutationRulesConfig config = + HeaderMutationRulesConfig.builder().allowExpression(pattern).build(); + assertThat(config.allowExpression()).hasValue(pattern); + } + + @Test + public void testBuilder_setDisallowExpression() { + Pattern pattern = Pattern.compile("disallow.*"); + HeaderMutationRulesConfig config = + HeaderMutationRulesConfig.builder().disallowExpression(pattern).build(); + assertThat(config.disallowExpression()).hasValue(pattern); + } + + @Test + public void testBuilder_setAll() { + Pattern allowPattern = Pattern.compile("allow.*"); + Pattern disallowPattern = Pattern.compile("disallow.*"); + HeaderMutationRulesConfig config = HeaderMutationRulesConfig.builder() + .disallowAll(true) + .disallowIsError(true) + .allowExpression(allowPattern) + .disallowExpression(disallowPattern) + .build(); + assertTrue(config.disallowAll()); + assertTrue(config.disallowIsError()); + assertThat(config.allowExpression()).hasValue(allowPattern); + assertThat(config.disallowExpression()).hasValue(disallowPattern); + } +} From 5654c6496ccf0c731e88c4367d7c19cce76b8454 Mon Sep 17 00:00:00 2001 From: Saurav Date: Tue, 13 Jan 2026 04:11:34 +0000 Subject: [PATCH 02/10] Fixup: Address comments from #12492 --- .../io/grpc/xds/GrpcBootstrapperImpl.java | 105 ++++- .../java/io/grpc/xds/client/Bootstrapper.java | 10 + .../io/grpc/xds/client/BootstrapperImpl.java | 11 + .../io/grpc/xds/internal/MatcherParser.java | 21 + .../grpc/xds/internal/XdsHeaderValidator.java | 40 ++ .../xds/internal/extauthz/ExtAuthzConfig.java | 109 +---- .../extauthz/ExtAuthzConfigParser.java | 96 +++++ ...elFactory.java => ChannelCredsConfig.java} | 11 +- .../ConfiguredChannelCredentials.java | 35 ++ .../grpcservice/GrpcServiceConfig.java | 244 +---------- .../grpcservice/GrpcServiceConfigParser.java | 323 +++++++++++++++ .../grpcservice/GrpcServiceXdsContext.java | 71 ++++ .../GrpcServiceXdsContextProvider.java | 31 ++ .../xds/internal/grpcservice/HeaderValue.java | 44 ++ .../InsecureGrpcChannelFactory.java | 43 -- .../HeaderMutationRulesConfig.java | 2 +- .../HeaderMutationRulesParser.java | 55 +++ .../io/grpc/xds/GrpcBootstrapperImplTest.java | 55 +++ .../grpc/xds/internal/MatcherParserTest.java | 85 ++++ .../xds/internal/XdsHeaderValidatorTest.java | 64 +++ ...est.java => ExtAuthzConfigParserTest.java} | 130 +++--- .../GrpcServiceConfigParserTest.java | 390 ++++++++++++++++++ .../grpcservice/GrpcServiceConfigTest.java | 243 ----------- .../GrpcServiceXdsContextTestUtil.java | 30 ++ .../internal/grpcservice/HeaderValueTest.java | 49 +++ .../InsecureGrpcChannelFactoryTest.java | 57 --- .../HeaderMutationRulesConfigTest.java | 2 +- .../HeaderMutationRulesParserTest.java | 90 ++++ 28 files changed, 1688 insertions(+), 758 deletions(-) create mode 100644 xds/src/main/java/io/grpc/xds/internal/XdsHeaderValidator.java create mode 100644 xds/src/main/java/io/grpc/xds/internal/extauthz/ExtAuthzConfigParser.java rename xds/src/main/java/io/grpc/xds/internal/grpcservice/{GrpcServiceConfigChannelFactory.java => ChannelCredsConfig.java} (74%) create mode 100644 xds/src/main/java/io/grpc/xds/internal/grpcservice/ConfiguredChannelCredentials.java create mode 100644 xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParser.java create mode 100644 xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceXdsContext.java create mode 100644 xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceXdsContextProvider.java create mode 100644 xds/src/main/java/io/grpc/xds/internal/grpcservice/HeaderValue.java delete mode 100644 xds/src/main/java/io/grpc/xds/internal/grpcservice/InsecureGrpcChannelFactory.java create mode 100644 xds/src/main/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesParser.java create mode 100644 xds/src/test/java/io/grpc/xds/internal/MatcherParserTest.java create mode 100644 xds/src/test/java/io/grpc/xds/internal/XdsHeaderValidatorTest.java rename xds/src/test/java/io/grpc/xds/internal/extauthz/{ExtAuthzConfigTest.java => ExtAuthzConfigParserTest.java} (63%) create mode 100644 xds/src/test/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParserTest.java delete mode 100644 xds/src/test/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigTest.java create mode 100644 xds/src/test/java/io/grpc/xds/internal/grpcservice/GrpcServiceXdsContextTestUtil.java create mode 100644 xds/src/test/java/io/grpc/xds/internal/grpcservice/HeaderValueTest.java delete mode 100644 xds/src/test/java/io/grpc/xds/internal/grpcservice/InsecureGrpcChannelFactoryTest.java create mode 100644 xds/src/test/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesParserTest.java diff --git a/xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java b/xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java index 494e95a58f6..9420a87191d 100644 --- a/xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java +++ b/xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java @@ -19,14 +19,19 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; import com.google.errorprone.annotations.concurrent.GuardedBy; +import io.grpc.CallCredentials; import io.grpc.ChannelCredentials; import io.grpc.internal.JsonUtil; import io.grpc.xds.client.BootstrapperImpl; import io.grpc.xds.client.XdsInitializationException; import io.grpc.xds.client.XdsLogger; +import io.grpc.xds.internal.grpcservice.ChannelCredsConfig; +import io.grpc.xds.internal.grpcservice.ConfiguredChannelCredentials; +import io.grpc.xds.internal.grpcservice.GrpcServiceXdsContext; import java.io.IOException; import java.util.List; import java.util.Map; +import java.util.Optional; import javax.annotation.Nullable; class GrpcBootstrapperImpl extends BootstrapperImpl { @@ -97,7 +102,8 @@ protected String getJsonContent() throws XdsInitializationException, IOException @Override protected Object getImplSpecificConfig(Map serverConfig, String serverUri) throws XdsInitializationException { - return getChannelCredentials(serverConfig, serverUri); + ConfiguredChannelCredentials configuredChannel = getChannelCredentials(serverConfig, serverUri); + return configuredChannel != null ? configuredChannel.channelCredentials() : null; } @GuardedBy("GrpcBootstrapperImpl.class") @@ -120,26 +126,26 @@ static synchronized BootstrapInfo defaultBootstrap() throws XdsInitializationExc return defaultBootstrap; } - private static ChannelCredentials getChannelCredentials(Map serverConfig, - String serverUri) + private static ConfiguredChannelCredentials getChannelCredentials(Map serverConfig, + String serverUri) throws XdsInitializationException { List rawChannelCredsList = JsonUtil.getList(serverConfig, "channel_creds"); if (rawChannelCredsList == null || rawChannelCredsList.isEmpty()) { throw new XdsInitializationException( "Invalid bootstrap: server " + serverUri + " 'channel_creds' required"); } - ChannelCredentials channelCredentials = + ConfiguredChannelCredentials credentials = parseChannelCredentials(JsonUtil.checkObjectList(rawChannelCredsList), serverUri); - if (channelCredentials == null) { + if (credentials == null) { throw new XdsInitializationException( "Server " + serverUri + ": no supported channel credentials found"); } - return channelCredentials; + return credentials; } @Nullable - private static ChannelCredentials parseChannelCredentials(List> jsonList, - String serverUri) + private static ConfiguredChannelCredentials parseChannelCredentials(List> jsonList, + String serverUri) throws XdsInitializationException { for (Map channelCreds : jsonList) { String type = JsonUtil.getString(channelCreds, "type"); @@ -155,9 +161,90 @@ private static ChannelCredentials parseChannelCredentials(List> j config = ImmutableMap.of(); } - return provider.newChannelCredentials(config); + ChannelCredentials creds = provider.newChannelCredentials(config); + if (creds == null) { + continue; + } + return ConfiguredChannelCredentials.create(creds, new JsonChannelCredsConfig(type, config)); } } return null; } + + @Override + protected Optional parseAllowedGrpcServices( + Map rawAllowedGrpcServices) + throws XdsInitializationException { + ImmutableMap.Builder builder = + ImmutableMap.builder(); + for (String targetUri : rawAllowedGrpcServices.keySet()) { + Map serviceConfig = JsonUtil.getObject(rawAllowedGrpcServices, targetUri); + if (serviceConfig == null) { + throw new XdsInitializationException( + "Invalid allowed_grpc_services config for " + targetUri); + } + ConfiguredChannelCredentials configuredChannel = + getChannelCredentials(serviceConfig, targetUri); + + Optional callCredentials = Optional.empty(); + List rawCallCredsList = JsonUtil.getList(serviceConfig, "call_creds"); + if (rawCallCredsList != null && !rawCallCredsList.isEmpty()) { + callCredentials = + parseCallCredentials(JsonUtil.checkObjectList(rawCallCredsList), targetUri); + } + + GrpcServiceXdsContext.AllowedGrpcService.Builder b = GrpcServiceXdsContext.AllowedGrpcService + .builder().configuredChannelCredentials(configuredChannel); + callCredentials.ifPresent(b::callCredentials); + builder.put(targetUri, b.build()); + } + ImmutableMap parsed = builder.buildOrThrow(); + return parsed.isEmpty() ? Optional.empty() : Optional.of(parsed); + } + + @SuppressWarnings("unused") + private static Optional parseCallCredentials(List> jsonList, + String targetUri) + throws XdsInitializationException { + // TODO(sauravzg): Currently no xDS call credentials providers are implemented (no + // XdsCallCredentialsRegistry). + // As per A102/A97, we should just ignore unsupported call credentials types + // without throwing an exception. + return Optional.empty(); + } + + private static final class JsonChannelCredsConfig implements ChannelCredsConfig { + private final String type; + private final Map config; + + JsonChannelCredsConfig(String type, Map config) { + this.type = type; + this.config = config; + } + + @Override + public String type() { + return type; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + JsonChannelCredsConfig that = (JsonChannelCredsConfig) o; + return java.util.Objects.equals(type, that.type) + && java.util.Objects.equals(config, that.config); + } + + @Override + public int hashCode() { + return java.util.Objects.hash(type, config); + } + } + } + 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 1d526703299..32f4216d0cd 100644 --- a/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java +++ b/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java @@ -26,6 +26,7 @@ import io.grpc.xds.client.EnvoyProtoData.Node; import java.util.List; import java.util.Map; +import java.util.Optional; import javax.annotation.Nullable; /** @@ -205,6 +206,12 @@ public abstract static class BootstrapInfo { */ public abstract ImmutableMap authorities(); + /** + * Parsed allowed_grpc_services configuration. + * Returns an opaque object containing the parsed configuration. + */ + public abstract Optional allowedGrpcServices(); + @VisibleForTesting public static Builder builder() { return new AutoValue_Bootstrapper_BootstrapInfo.Builder() @@ -231,7 +238,10 @@ public abstract Builder clientDefaultListenerResourceNameTemplate( public abstract Builder authorities(Map authorities); + public abstract Builder allowedGrpcServices(Optional allowedGrpcServices); + public abstract BootstrapInfo build(); } } + } 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 b44e32bb2d9..e267a9cb985 100644 --- a/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java +++ b/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java @@ -239,9 +239,20 @@ protected BootstrapInfo.Builder bootstrapBuilder(Map rawData) builder.authorities(authorityInfoMapBuilder.buildOrThrow()); } + Map rawAllowedGrpcServices = JsonUtil.getObject(rawData, "allowed_grpc_services"); + if (rawAllowedGrpcServices != null) { + builder.allowedGrpcServices(parseAllowedGrpcServices(rawAllowedGrpcServices)); + } + return builder; } + protected java.util.Optional parseAllowedGrpcServices( + Map rawAllowedGrpcServices) + throws XdsInitializationException { + return java.util.Optional.empty(); + } + private List parseServerInfos(List rawServerConfigs, XdsLogger logger) throws XdsInitializationException { logger.log(XdsLogLevel.INFO, "Configured with {0} xDS servers", rawServerConfigs.size()); diff --git a/xds/src/main/java/io/grpc/xds/internal/MatcherParser.java b/xds/src/main/java/io/grpc/xds/internal/MatcherParser.java index fb291efc461..91b77b05d01 100644 --- a/xds/src/main/java/io/grpc/xds/internal/MatcherParser.java +++ b/xds/src/main/java/io/grpc/xds/internal/MatcherParser.java @@ -97,4 +97,25 @@ public static Matchers.StringMatcher parseStringMatcher( "Unknown StringMatcher match pattern: " + proto.getMatchPatternCase()); } } + + /** Translates envoy proto FractionalPercent to internal FractionMatcher. */ + public static Matchers.FractionMatcher parseFractionMatcher( + io.envoyproxy.envoy.type.v3.FractionalPercent proto) { + int denominator; + switch (proto.getDenominator()) { + case HUNDRED: + denominator = 100; + break; + case TEN_THOUSAND: + denominator = 10_000; + break; + case MILLION: + denominator = 1_000_000; + break; + case UNRECOGNIZED: + default: + throw new IllegalArgumentException("Unknown denominator type: " + proto.getDenominator()); + } + return Matchers.FractionMatcher.create(proto.getNumerator(), denominator); + } } diff --git a/xds/src/main/java/io/grpc/xds/internal/XdsHeaderValidator.java b/xds/src/main/java/io/grpc/xds/internal/XdsHeaderValidator.java new file mode 100644 index 00000000000..dbd459b017b --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/XdsHeaderValidator.java @@ -0,0 +1,40 @@ +/* + * 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; + +/** + * Utility for validating header keys and values against xDS and Envoy specifications. + */ +public final class XdsHeaderValidator { + + private XdsHeaderValidator() {} + + /** + * Returns whether the header parameter is valid. The length to check is either the + * length of the string value or the size of the binary raw value. + */ + public static boolean isValid(String key, int valueLength) { + if (key.isEmpty() || !key.equals(key.toLowerCase(java.util.Locale.ROOT)) || key.length() > 16384 + || key.equals("host") || key.startsWith(":")) { + return false; + } + if (valueLength > 16384) { + return false; + } + return true; + } +} diff --git a/xds/src/main/java/io/grpc/xds/internal/extauthz/ExtAuthzConfig.java b/xds/src/main/java/io/grpc/xds/internal/extauthz/ExtAuthzConfig.java index e826f501d9c..fec8e605d73 100644 --- a/xds/src/main/java/io/grpc/xds/internal/extauthz/ExtAuthzConfig.java +++ b/xds/src/main/java/io/grpc/xds/internal/extauthz/ExtAuthzConfig.java @@ -18,18 +18,11 @@ import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; -import io.envoyproxy.envoy.config.common.mutation_rules.v3.HeaderMutationRules; -import io.envoyproxy.envoy.extensions.filters.http.ext_authz.v3.ExtAuthz; import io.grpc.Status; -import io.grpc.internal.GrpcUtil; -import io.grpc.xds.internal.MatcherParser; import io.grpc.xds.internal.Matchers; import io.grpc.xds.internal.grpcservice.GrpcServiceConfig; -import io.grpc.xds.internal.grpcservice.GrpcServiceParseException; import io.grpc.xds.internal.headermutations.HeaderMutationRulesConfig; import java.util.Optional; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; /** * Represents the configuration for the external authorization (ext_authz) filter. This class @@ -42,64 +35,12 @@ public abstract class ExtAuthzConfig { /** Creates a new builder for creating {@link ExtAuthzConfig} instances. */ - public static Builder builder() { + public static Builder newBuilder() { return new AutoValue_ExtAuthzConfig.Builder().allowedHeaders(ImmutableList.of()) .disallowedHeaders(ImmutableList.of()).statusOnError(Status.PERMISSION_DENIED) .filterEnabled(Matchers.FractionMatcher.create(100, 100)); } - /** - * Parses the {@link io.envoyproxy.envoy.extensions.filters.http.ext_authz.v3.ExtAuthz} proto to - * create an {@link ExtAuthzConfig} instance. - * - * @param extAuthzProto The ext_authz proto to parse. - * @return An {@link ExtAuthzConfig} instance. - * @throws ExtAuthzParseException if the proto is invalid or contains unsupported features. - */ - public static ExtAuthzConfig fromProto(ExtAuthz extAuthzProto) throws ExtAuthzParseException { - if (!extAuthzProto.hasGrpcService()) { - throw new ExtAuthzParseException( - "unsupported ExtAuthz service type: only grpc_service is " + "supported"); - } - GrpcServiceConfig grpcServiceConfig; - try { - grpcServiceConfig = GrpcServiceConfig.fromProto(extAuthzProto.getGrpcService()); - } catch (GrpcServiceParseException e) { - throw new ExtAuthzParseException("Failed to parse GrpcService config: " + e.getMessage(), e); - } - Builder builder = builder().grpcService(grpcServiceConfig) - .failureModeAllow(extAuthzProto.getFailureModeAllow()) - .failureModeAllowHeaderAdd(extAuthzProto.getFailureModeAllowHeaderAdd()) - .includePeerCertificate(extAuthzProto.getIncludePeerCertificate()) - .denyAtDisable(extAuthzProto.getDenyAtDisable().getDefaultValue().getValue()); - - if (extAuthzProto.hasFilterEnabled()) { - builder.filterEnabled(parsePercent(extAuthzProto.getFilterEnabled().getDefaultValue())); - } - - if (extAuthzProto.hasStatusOnError()) { - builder.statusOnError( - GrpcUtil.httpStatusToGrpcStatus(extAuthzProto.getStatusOnError().getCodeValue())); - } - - if (extAuthzProto.hasAllowedHeaders()) { - builder.allowedHeaders(extAuthzProto.getAllowedHeaders().getPatternsList().stream() - .map(MatcherParser::parseStringMatcher).collect(ImmutableList.toImmutableList())); - } - - if (extAuthzProto.hasDisallowedHeaders()) { - builder.disallowedHeaders(extAuthzProto.getDisallowedHeaders().getPatternsList().stream() - .map(MatcherParser::parseStringMatcher).collect(ImmutableList.toImmutableList())); - } - - if (extAuthzProto.hasDecoderHeaderMutationRules()) { - builder.decoderHeaderMutationRules( - parseHeaderMutationRules(extAuthzProto.getDecoderHeaderMutationRules())); - } - - return builder.build(); - } - /** * The gRPC service configuration for the external authorization service. This is a required * field. @@ -155,7 +96,7 @@ public static ExtAuthzConfig fromProto(ExtAuthz extAuthzProto) throws ExtAuthzPa public abstract Matchers.FractionMatcher filterEnabled(); /** - * Specifies which request headers are sent to the authorization service. If not set, all headers + * Specifies which request headers are sent to the authorization service. If empty, all headers * are sent. * * @see ExtAuthz#getAllowedHeaders() @@ -201,50 +142,4 @@ public abstract static class Builder { public abstract ExtAuthzConfig build(); } - - - private static Matchers.FractionMatcher parsePercent( - io.envoyproxy.envoy.type.v3.FractionalPercent proto) throws ExtAuthzParseException { - int denominator; - switch (proto.getDenominator()) { - case HUNDRED: - denominator = 100; - break; - case TEN_THOUSAND: - denominator = 10_000; - break; - case MILLION: - denominator = 1_000_000; - break; - case UNRECOGNIZED: - default: - throw new ExtAuthzParseException("Unknown denominator type: " + proto.getDenominator()); - } - return Matchers.FractionMatcher.create(proto.getNumerator(), denominator); - } - - private static HeaderMutationRulesConfig parseHeaderMutationRules(HeaderMutationRules proto) - throws ExtAuthzParseException { - HeaderMutationRulesConfig.Builder builder = HeaderMutationRulesConfig.builder(); - builder.disallowAll(proto.getDisallowAll().getValue()); - builder.disallowIsError(proto.getDisallowIsError().getValue()); - if (proto.hasAllowExpression()) { - builder.allowExpression( - parseRegex(proto.getAllowExpression().getRegex(), "allow_expression")); - } - if (proto.hasDisallowExpression()) { - builder.disallowExpression( - parseRegex(proto.getDisallowExpression().getRegex(), "disallow_expression")); - } - return builder.build(); - } - - private static Pattern parseRegex(String regex, String fieldName) throws ExtAuthzParseException { - try { - return Pattern.compile(regex); - } catch (PatternSyntaxException e) { - throw new ExtAuthzParseException( - "Invalid regex pattern for " + fieldName + ": " + e.getMessage(), e); - } - } } diff --git a/xds/src/main/java/io/grpc/xds/internal/extauthz/ExtAuthzConfigParser.java b/xds/src/main/java/io/grpc/xds/internal/extauthz/ExtAuthzConfigParser.java new file mode 100644 index 00000000000..4e17763ae12 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/extauthz/ExtAuthzConfigParser.java @@ -0,0 +1,96 @@ +/* + * 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.extauthz; + +import com.google.common.collect.ImmutableList; +import io.envoyproxy.envoy.extensions.filters.http.ext_authz.v3.ExtAuthz; +import io.grpc.internal.GrpcUtil; +import io.grpc.xds.internal.MatcherParser; +import io.grpc.xds.internal.grpcservice.GrpcServiceConfig; +import io.grpc.xds.internal.grpcservice.GrpcServiceConfigParser; +import io.grpc.xds.internal.grpcservice.GrpcServiceParseException; +import io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextProvider; +import io.grpc.xds.internal.headermutations.HeaderMutationRulesParser; + + +/** + * Parser for {@link io.envoyproxy.envoy.extensions.filters.http.ext_authz.v3.ExtAuthz}. + */ +public final class ExtAuthzConfigParser { + + private ExtAuthzConfigParser() {} + + /** + * Parses the {@link io.envoyproxy.envoy.extensions.filters.http.ext_authz.v3.ExtAuthz} proto to + * create an {@link ExtAuthzConfig} instance. + * + * @param extAuthzProto The ext_authz proto to parse. + * @return An {@link ExtAuthzConfig} instance. + * @throws ExtAuthzParseException if the proto is invalid or contains unsupported features. + */ + public static ExtAuthzConfig parse( + ExtAuthz extAuthzProto, GrpcServiceXdsContextProvider contextProvider) + throws ExtAuthzParseException { + if (!extAuthzProto.hasGrpcService()) { + throw new ExtAuthzParseException( + "unsupported ExtAuthz service type: only grpc_service is supported"); + } + GrpcServiceConfig grpcServiceConfig; + try { + grpcServiceConfig = + GrpcServiceConfigParser.parse(extAuthzProto.getGrpcService(), contextProvider); + } catch (GrpcServiceParseException e) { + throw new ExtAuthzParseException("Failed to parse GrpcService config: " + e.getMessage(), e); + } + ExtAuthzConfig.Builder builder = ExtAuthzConfig.newBuilder().grpcService(grpcServiceConfig) + .failureModeAllow(extAuthzProto.getFailureModeAllow()) + .failureModeAllowHeaderAdd(extAuthzProto.getFailureModeAllowHeaderAdd()) + .includePeerCertificate(extAuthzProto.getIncludePeerCertificate()) + .denyAtDisable(extAuthzProto.getDenyAtDisable().getDefaultValue().getValue()); + + if (extAuthzProto.hasFilterEnabled()) { + try { + builder.filterEnabled( + MatcherParser.parseFractionMatcher(extAuthzProto.getFilterEnabled().getDefaultValue())); + } catch (IllegalArgumentException e) { + throw new ExtAuthzParseException(e.getMessage()); + } + } + + if (extAuthzProto.hasStatusOnError()) { + builder.statusOnError( + GrpcUtil.httpStatusToGrpcStatus(extAuthzProto.getStatusOnError().getCodeValue())); + } + + if (extAuthzProto.hasAllowedHeaders()) { + builder.allowedHeaders(extAuthzProto.getAllowedHeaders().getPatternsList().stream() + .map(MatcherParser::parseStringMatcher).collect(ImmutableList.toImmutableList())); + } + + if (extAuthzProto.hasDisallowedHeaders()) { + builder.disallowedHeaders(extAuthzProto.getDisallowedHeaders().getPatternsList().stream() + .map(MatcherParser::parseStringMatcher).collect(ImmutableList.toImmutableList())); + } + + if (extAuthzProto.hasDecoderHeaderMutationRules()) { + builder.decoderHeaderMutationRules( + HeaderMutationRulesParser.parse(extAuthzProto.getDecoderHeaderMutationRules())); + } + + return builder.build(); + } +} diff --git a/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigChannelFactory.java b/xds/src/main/java/io/grpc/xds/internal/grpcservice/ChannelCredsConfig.java similarity index 74% rename from xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigChannelFactory.java rename to xds/src/main/java/io/grpc/xds/internal/grpcservice/ChannelCredsConfig.java index 0d02989eaa3..1e7008ca8e2 100644 --- a/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigChannelFactory.java +++ b/xds/src/main/java/io/grpc/xds/internal/grpcservice/ChannelCredsConfig.java @@ -16,11 +16,12 @@ package io.grpc.xds.internal.grpcservice; -import io.grpc.ManagedChannel; - /** - * A factory for creating {@link ManagedChannel}s from a {@link GrpcServiceConfig}. + * Configuration for channel credentials. */ -public interface GrpcServiceConfigChannelFactory { - ManagedChannel createChannel(GrpcServiceConfig config); +public interface ChannelCredsConfig { + /** + * Returns the type of the credentials. + */ + String type(); } diff --git a/xds/src/main/java/io/grpc/xds/internal/grpcservice/ConfiguredChannelCredentials.java b/xds/src/main/java/io/grpc/xds/internal/grpcservice/ConfiguredChannelCredentials.java new file mode 100644 index 00000000000..bf541748cd8 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/grpcservice/ConfiguredChannelCredentials.java @@ -0,0 +1,35 @@ +/* + * 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.grpcservice; + +import com.google.auto.value.AutoValue; +import io.grpc.ChannelCredentials; + +/** + * Composition of {@link ChannelCredentials} and {@link ChannelCredsConfig}. + */ +@AutoValue +public abstract class ConfiguredChannelCredentials { + public abstract ChannelCredentials channelCredentials(); + + public abstract ChannelCredsConfig channelCredsConfig(); + + public static ConfiguredChannelCredentials create(ChannelCredentials creds, + ChannelCredsConfig config) { + return new AutoValue_ConfiguredChannelCredentials(creds, config); + } +} diff --git a/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfig.java b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfig.java index da9be978f87..ba0a9808025 100644 --- a/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfig.java +++ b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfig.java @@ -16,93 +16,30 @@ package io.grpc.xds.internal.grpcservice; -import com.google.auth.oauth2.AccessToken; -import com.google.auth.oauth2.OAuth2Credentials; import com.google.auto.value.AutoValue; -import com.google.common.io.BaseEncoding; -import com.google.protobuf.Any; -import com.google.protobuf.InvalidProtocolBufferException; -import io.envoyproxy.envoy.config.core.v3.GrpcService; -import io.envoyproxy.envoy.extensions.grpc_service.call_credentials.access_token.v3.AccessTokenCredentials; -import io.envoyproxy.envoy.extensions.grpc_service.channel_credentials.xds.v3.XdsCredentials; +import com.google.common.collect.ImmutableList; import io.grpc.CallCredentials; -import io.grpc.ChannelCredentials; -import io.grpc.InsecureChannelCredentials; -import io.grpc.Metadata; -import io.grpc.alts.GoogleDefaultChannelCredentials; -import io.grpc.auth.MoreCallCredentials; -import io.grpc.xds.XdsChannelCredentials; import java.time.Duration; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; import java.util.Optional; /** - * A Java representation of the {@link io.envoyproxy.envoy.config.core.v3.GrpcService} proto, - * designed for parsing and internal use within gRPC. This class encapsulates the configuration for - * a gRPC service, including target URI, credentials, and other settings. The parsing logic adheres - * to the specifications outlined in - * A102: xDS GrpcService Support. This class is immutable and uses the AutoValue library for its - * implementation. + * A Java representation of the {@link io.envoyproxy.envoy.config.core.v3.GrpcService} proto. This + * class encapsulates the configuration for a gRPC service, including target URI, credentials, and + * other settings. This class is immutable and uses the AutoValue library for its implementation. */ @AutoValue public abstract class GrpcServiceConfig { - public static Builder builder() { + public static Builder newBuilder() { return new AutoValue_GrpcServiceConfig.Builder(); } - /** - * Parses the {@link io.envoyproxy.envoy.config.core.v3.GrpcService} proto to create a - * {@link GrpcServiceConfig} instance. This method adheres to gRFC A102, which specifies that only - * the {@code google_grpc} target specifier is supported. Other fields like {@code timeout} and - * {@code initial_metadata} are also parsed as per the gRFC. - * - * @param grpcServiceProto The proto to parse. - * @return A {@link GrpcServiceConfig} instance. - * @throws GrpcServiceParseException if the proto is invalid or uses unsupported features. - */ - public static GrpcServiceConfig fromProto(GrpcService grpcServiceProto) - throws GrpcServiceParseException { - if (!grpcServiceProto.hasGoogleGrpc()) { - throw new GrpcServiceParseException( - "Unsupported: GrpcService must have GoogleGrpc, got: " + grpcServiceProto); - } - GoogleGrpcConfig googleGrpcConfig = - GoogleGrpcConfig.fromProto(grpcServiceProto.getGoogleGrpc()); - - Builder builder = GrpcServiceConfig.builder().googleGrpc(googleGrpcConfig); - - if (!grpcServiceProto.getInitialMetadataList().isEmpty()) { - Metadata initialMetadata = new Metadata(); - for (io.envoyproxy.envoy.config.core.v3.HeaderValue header : grpcServiceProto - .getInitialMetadataList()) { - String key = header.getKey(); - if (key.endsWith(Metadata.BINARY_HEADER_SUFFIX)) { - initialMetadata.put(Metadata.Key.of(key, Metadata.BINARY_BYTE_MARSHALLER), - BaseEncoding.base64().decode(header.getValue())); - } else { - initialMetadata.put(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER), - header.getValue()); - } - } - builder.initialMetadata(initialMetadata); - } - - if (grpcServiceProto.hasTimeout()) { - com.google.protobuf.Duration timeout = grpcServiceProto.getTimeout(); - builder.timeout(Duration.ofSeconds(timeout.getSeconds(), timeout.getNanos())); - } - return builder.build(); - } - public abstract GoogleGrpcConfig googleGrpc(); public abstract Optional timeout(); - public abstract Optional initialMetadata(); + public abstract ImmutableList initialMetadata(); @AutoValue.Builder public abstract static class Builder { @@ -110,7 +47,7 @@ public abstract static class Builder { public abstract Builder timeout(Duration timeout); - public abstract Builder initialMetadata(Metadata initialMetadata); + public abstract Builder initialMetadata(ImmutableList initialMetadata); public abstract GrpcServiceConfig build(); } @@ -119,190 +56,33 @@ public abstract static class Builder { * Represents the configuration for a Google gRPC service, as defined in the * {@link io.envoyproxy.envoy.config.core.v3.GrpcService.GoogleGrpc} proto. This class * encapsulates settings specific to Google's gRPC implementation, such as target URI and - * credentials. The parsing of this configuration is guided by gRFC A102, which specifies how gRPC - * clients should interpret the GrpcService proto. + * credentials. */ @AutoValue public abstract static class GoogleGrpcConfig { - private static final String TLS_CREDENTIALS_TYPE_URL = - "type.googleapis.com/envoy.extensions.grpc_service.channel_credentials." - + "tls.v3.TlsCredentials"; - private static final String LOCAL_CREDENTIALS_TYPE_URL = - "type.googleapis.com/envoy.extensions.grpc_service.channel_credentials." - + "local.v3.LocalCredentials"; - private static final String XDS_CREDENTIALS_TYPE_URL = - "type.googleapis.com/envoy.extensions.grpc_service.channel_credentials." - + "xds.v3.XdsCredentials"; - private static final String INSECURE_CREDENTIALS_TYPE_URL = - "type.googleapis.com/envoy.extensions.grpc_service.channel_credentials." - + "insecure.v3.InsecureCredentials"; - private static final String GOOGLE_DEFAULT_CREDENTIALS_TYPE_URL = - "type.googleapis.com/envoy.extensions.grpc_service.channel_credentials." - + "google_default.v3.GoogleDefaultCredentials"; - public static Builder builder() { return new AutoValue_GrpcServiceConfig_GoogleGrpcConfig.Builder(); } - /** - * Parses the {@link io.envoyproxy.envoy.config.core.v3.GrpcService.GoogleGrpc} proto to create - * a {@link GoogleGrpcConfig} instance. - * - * @param googleGrpcProto The proto to parse. - * @return A {@link GoogleGrpcConfig} instance. - * @throws GrpcServiceParseException if the proto is invalid. - */ - public static GoogleGrpcConfig fromProto(GrpcService.GoogleGrpc googleGrpcProto) - throws GrpcServiceParseException { - - HashedChannelCredentials channelCreds = - extractChannelCredentials(googleGrpcProto.getChannelCredentialsPluginList()); - - CallCredentials callCreds = - extractCallCredentials(googleGrpcProto.getCallCredentialsPluginList()); - - return GoogleGrpcConfig.builder().target(googleGrpcProto.getTargetUri()) - .hashedChannelCredentials(channelCreds).callCredentials(callCreds).build(); - } - public abstract String target(); - public abstract HashedChannelCredentials hashedChannelCredentials(); + public abstract ConfiguredChannelCredentials configuredChannelCredentials(); - public abstract CallCredentials callCredentials(); + public abstract Optional callCredentials(); @AutoValue.Builder public abstract static class Builder { public abstract Builder target(String target); - public abstract Builder hashedChannelCredentials(HashedChannelCredentials channelCredentials); + public abstract Builder configuredChannelCredentials( + ConfiguredChannelCredentials channelCredentials); public abstract Builder callCredentials(CallCredentials callCredentials); public abstract GoogleGrpcConfig build(); } - - private static T getFirstSupported(List configs, Parser parser, - String configName) throws GrpcServiceParseException { - List errors = new ArrayList<>(); - for (U config : configs) { - try { - return parser.parse(config); - } catch (GrpcServiceParseException e) { - errors.add(e.getMessage()); - } - } - throw new GrpcServiceParseException( - "No valid supported " + configName + " found. Errors: " + errors); - } - - private static HashedChannelCredentials channelCredsFromProto(Any cred) - throws GrpcServiceParseException { - String typeUrl = cred.getTypeUrl(); - try { - switch (typeUrl) { - case GOOGLE_DEFAULT_CREDENTIALS_TYPE_URL: - return HashedChannelCredentials.of(GoogleDefaultChannelCredentials.create(), - cred.hashCode()); - case INSECURE_CREDENTIALS_TYPE_URL: - return HashedChannelCredentials.of(InsecureChannelCredentials.create(), - cred.hashCode()); - case XDS_CREDENTIALS_TYPE_URL: - XdsCredentials xdsConfig = cred.unpack(XdsCredentials.class); - HashedChannelCredentials fallbackCreds = - channelCredsFromProto(xdsConfig.getFallbackCredentials()); - return HashedChannelCredentials.of( - XdsChannelCredentials.create(fallbackCreds.channelCredentials()), cred.hashCode()); - case LOCAL_CREDENTIALS_TYPE_URL: - // TODO(sauravzg) : What's the java alternative to LocalCredentials. - throw new GrpcServiceParseException("LocalCredentials are not yet supported."); - case TLS_CREDENTIALS_TYPE_URL: - // TODO(sauravzg) : How to instantiate a TlsChannelCredentials from TlsCredentials - // proto? - throw new GrpcServiceParseException("TlsCredentials are not yet supported."); - default: - throw new GrpcServiceParseException("Unsupported channel credentials type: " + typeUrl); - } - } catch (InvalidProtocolBufferException e) { - // TODO(sauravzg): Add unit tests when we have a solution for TLS creds. - // This code is as of writing unreachable because all channel credential message - // types except TLS are empty messages. - throw new GrpcServiceParseException( - "Failed to parse channel credentials: " + e.getMessage()); - } - } - - private static CallCredentials callCredsFromProto(Any cred) throws GrpcServiceParseException { - try { - AccessTokenCredentials accessToken = cred.unpack(AccessTokenCredentials.class); - // TODO(sauravzg): Verify if the current behavior is per spec.The `AccessTokenCredentials` - // config doesn't have any timeout/refresh, so set the token to never expire. - return MoreCallCredentials.from(OAuth2Credentials - .create(new AccessToken(accessToken.getToken(), new Date(Long.MAX_VALUE)))); - } catch (InvalidProtocolBufferException e) { - throw new GrpcServiceParseException( - "Unsupported call credentials type: " + cred.getTypeUrl()); - } - } - - private static HashedChannelCredentials extractChannelCredentials( - List channelCredentialPlugins) throws GrpcServiceParseException { - return getFirstSupported(channelCredentialPlugins, GoogleGrpcConfig::channelCredsFromProto, - "channel_credentials"); - } - - private static CallCredentials extractCallCredentials(List callCredentialPlugins) - throws GrpcServiceParseException { - return getFirstSupported(callCredentialPlugins, GoogleGrpcConfig::callCredsFromProto, - "call_credentials"); - } - } - - /** - * A container for {@link ChannelCredentials} and a hash for the purpose of caching. - */ - @AutoValue - public abstract static class HashedChannelCredentials { - /** - * Creates a new {@link HashedChannelCredentials} instance. - * - * @param creds The channel credentials. - * @param hash The hash of the credentials. - * @return A new {@link HashedChannelCredentials} instance. - */ - public static HashedChannelCredentials of(ChannelCredentials creds, int hash) { - return new AutoValue_GrpcServiceConfig_HashedChannelCredentials(creds, hash); - } - - /** - * Returns the channel credentials. - */ - public abstract ChannelCredentials channelCredentials(); - - /** - * Returns the hash of the credentials. - */ - public abstract int hash(); } - /** - * Defines a generic interface for parsing a configuration of type {@code U} into a result of type - * {@code T}. This functional interface is used to abstract the parsing logic for different parts - * of the GrpcService configuration. - * - * @param The type of the object that will be returned after parsing. - * @param The type of the configuration object that will be parsed. - */ - private interface Parser { - /** - * Parses the given configuration. - * - * @param config The configuration object to parse. - * @return The parsed object of type {@code T}. - * @throws GrpcServiceParseException if an error occurs during parsing. - */ - T parse(U config) throws GrpcServiceParseException; - } } diff --git a/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParser.java b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParser.java new file mode 100644 index 00000000000..7614484f396 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParser.java @@ -0,0 +1,323 @@ +/* + * 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.grpcservice; + +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.OAuth2Credentials; +import com.google.common.collect.ImmutableList; +import com.google.protobuf.Any; +import com.google.protobuf.InvalidProtocolBufferException; +import io.envoyproxy.envoy.config.core.v3.GrpcService; +import io.envoyproxy.envoy.extensions.grpc_service.call_credentials.access_token.v3.AccessTokenCredentials; +import io.envoyproxy.envoy.extensions.grpc_service.channel_credentials.xds.v3.XdsCredentials; +import io.grpc.CallCredentials; +import io.grpc.CompositeCallCredentials; +import io.grpc.InsecureChannelCredentials; +import io.grpc.Metadata; +import io.grpc.SecurityLevel; +import io.grpc.Status; +import io.grpc.alts.GoogleDefaultChannelCredentials; +import io.grpc.auth.MoreCallCredentials; +import io.grpc.xds.XdsChannelCredentials; +import io.grpc.xds.internal.XdsHeaderValidator; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Executor; + +/** + * Parser for {@link io.envoyproxy.envoy.config.core.v3.GrpcService} and related protos. + */ +public final class GrpcServiceConfigParser { + + static final String TLS_CREDENTIALS_TYPE_URL = + "type.googleapis.com/envoy.extensions.grpc_service.channel_credentials." + + "tls.v3.TlsCredentials"; + static final String LOCAL_CREDENTIALS_TYPE_URL = + "type.googleapis.com/envoy.extensions.grpc_service.channel_credentials." + + "local.v3.LocalCredentials"; + static final String XDS_CREDENTIALS_TYPE_URL = + "type.googleapis.com/envoy.extensions.grpc_service.channel_credentials." + + "xds.v3.XdsCredentials"; + static final String INSECURE_CREDENTIALS_TYPE_URL = + "type.googleapis.com/envoy.extensions.grpc_service.channel_credentials." + + "insecure.v3.InsecureCredentials"; + static final String GOOGLE_DEFAULT_CREDENTIALS_TYPE_URL = + "type.googleapis.com/envoy.extensions.grpc_service.channel_credentials." + + "google_default.v3.GoogleDefaultCredentials"; + + private GrpcServiceConfigParser() {} + + /** + * Parses the {@link io.envoyproxy.envoy.config.core.v3.GrpcService} proto to create a + * {@link GrpcServiceConfig} instance. + * + * @param grpcServiceProto The proto to parse. + * @return A {@link GrpcServiceConfig} instance. + * @throws GrpcServiceParseException if the proto is invalid or uses unsupported features. + */ + public static GrpcServiceConfig parse(GrpcService grpcServiceProto, + GrpcServiceXdsContextProvider contextProvider) + throws GrpcServiceParseException { + if (!grpcServiceProto.hasGoogleGrpc()) { + throw new GrpcServiceParseException( + "Unsupported: GrpcService must have GoogleGrpc, got: " + grpcServiceProto); + } + GrpcServiceConfig.GoogleGrpcConfig googleGrpcConfig = + parseGoogleGrpcConfig(grpcServiceProto.getGoogleGrpc(), contextProvider); + + GrpcServiceConfig.Builder builder = GrpcServiceConfig.newBuilder().googleGrpc(googleGrpcConfig); + + ImmutableList.Builder initialMetadata = ImmutableList.builder(); + for (io.envoyproxy.envoy.config.core.v3.HeaderValue header : grpcServiceProto + .getInitialMetadataList()) { + String key = header.getKey(); + if (key.endsWith(Metadata.BINARY_HEADER_SUFFIX)) { + if (!XdsHeaderValidator.isValid(key, header.getRawValue().size())) { + throw new GrpcServiceParseException("Invalid initial metadata header: " + key); + } + initialMetadata.add(HeaderValue.create(key, header.getRawValue())); + } else { + if (!XdsHeaderValidator.isValid(key, header.getValue().length())) { + throw new GrpcServiceParseException("Invalid initial metadata header: " + key); + } + initialMetadata.add(HeaderValue.create(key, header.getValue())); + } + } + builder.initialMetadata(initialMetadata.build()); + + if (grpcServiceProto.hasTimeout()) { + com.google.protobuf.Duration timeout = grpcServiceProto.getTimeout(); + if (timeout.getSeconds() < 0 || timeout.getNanos() < 0 + || (timeout.getSeconds() == 0 && timeout.getNanos() == 0)) { + throw new GrpcServiceParseException("Timeout must be strictly positive"); + } + builder.timeout(Duration.ofSeconds(timeout.getSeconds(), timeout.getNanos())); + } + return builder.build(); + } + + /** + * Parses the {@link io.envoyproxy.envoy.config.core.v3.GrpcService.GoogleGrpc} proto to create a + * {@link GrpcServiceConfig.GoogleGrpcConfig} instance. + * + * @param googleGrpcProto The proto to parse. + * @return A {@link GrpcServiceConfig.GoogleGrpcConfig} instance. + * @throws GrpcServiceParseException if the proto is invalid. + */ + public static GrpcServiceConfig.GoogleGrpcConfig parseGoogleGrpcConfig( + GrpcService.GoogleGrpc googleGrpcProto, GrpcServiceXdsContextProvider contextProvider) + throws GrpcServiceParseException { + + String targetUri = googleGrpcProto.getTargetUri(); + GrpcServiceXdsContext context = contextProvider.getContextForTarget(targetUri); + + if (!context.isTargetUriSchemeSupported()) { + throw new GrpcServiceParseException("Target URI scheme is not resolvable: " + targetUri); + } + + if (!context.isTrustedControlPlane()) { + Optional override = + context.validAllowedGrpcService(); + if (!override.isPresent()) { + throw new GrpcServiceParseException( + "Untrusted xDS server & URI not found in allowed_grpc_services: " + targetUri); + } + + GrpcServiceConfig.GoogleGrpcConfig.Builder builder = + GrpcServiceConfig.GoogleGrpcConfig.builder() + .target(targetUri) + .configuredChannelCredentials(override.get().configuredChannelCredentials()); + if (override.get().callCredentials().isPresent()) { + builder.callCredentials(override.get().callCredentials().get()); + } + return builder.build(); + } + + ConfiguredChannelCredentials channelCreds = null; + if (googleGrpcProto.getChannelCredentialsPluginCount() > 0) { + try { + channelCreds = extractChannelCredentials(googleGrpcProto.getChannelCredentialsPluginList()); + } catch (GrpcServiceParseException e) { + // Fall back to channel_credentials if plugins are not supported + } + } + + if (channelCreds == null) { + throw new GrpcServiceParseException("No valid supported channel_credentials found"); + } + + Optional callCreds = + extractCallCredentials(googleGrpcProto.getCallCredentialsPluginList()); + + GrpcServiceConfig.GoogleGrpcConfig.Builder builder = + GrpcServiceConfig.GoogleGrpcConfig.builder().target(googleGrpcProto.getTargetUri()) + .configuredChannelCredentials(channelCreds); + if (callCreds.isPresent()) { + builder.callCredentials(callCreds.get()); + } + return builder.build(); + } + + private static Optional channelCredsFromProto( + Any cred) throws GrpcServiceParseException { + String typeUrl = cred.getTypeUrl(); + try { + switch (typeUrl) { + case GOOGLE_DEFAULT_CREDENTIALS_TYPE_URL: + return Optional.of(ConfiguredChannelCredentials.create( + GoogleDefaultChannelCredentials.create(), + new ProtoChannelCredsConfig(typeUrl, cred))); + case INSECURE_CREDENTIALS_TYPE_URL: + return Optional.of(ConfiguredChannelCredentials.create( + InsecureChannelCredentials.create(), + new ProtoChannelCredsConfig(typeUrl, cred))); + case XDS_CREDENTIALS_TYPE_URL: + XdsCredentials xdsConfig = cred.unpack(XdsCredentials.class); + Optional fallbackCreds = + channelCredsFromProto(xdsConfig.getFallbackCredentials()); + if (!fallbackCreds.isPresent()) { + throw new GrpcServiceParseException( + "Unsupported fallback credentials type for XdsCredentials"); + } + return Optional.of(ConfiguredChannelCredentials.create( + XdsChannelCredentials.create(fallbackCreds.get().channelCredentials()), + new ProtoChannelCredsConfig(typeUrl, cred))); + case LOCAL_CREDENTIALS_TYPE_URL: + throw new UnsupportedOperationException( + "LocalCredentials are not supported in grpc-java. " + + "See https://github.com/grpc/grpc-java/issues/8928"); + case TLS_CREDENTIALS_TYPE_URL: + // For this PR, we establish this structural skeleton, + // but throw an UnsupportedOperationException until the exact stream conversions are + // merged. + throw new UnsupportedOperationException( + "TlsCredentials input stream construction pending."); + default: + return Optional.empty(); + } + } catch (InvalidProtocolBufferException e) { + throw new GrpcServiceParseException("Failed to parse channel credentials: " + e.getMessage()); + } + } + + private static ConfiguredChannelCredentials extractChannelCredentials( + List channelCredentialPlugins) throws GrpcServiceParseException { + for (Any cred : channelCredentialPlugins) { + Optional parsed = channelCredsFromProto(cred); + if (parsed.isPresent()) { + return parsed.get(); + } + } + throw new GrpcServiceParseException("No valid supported channel_credentials found"); + } + + private static Optional callCredsFromProto(Any cred) + throws GrpcServiceParseException { + if (cred.is(AccessTokenCredentials.class)) { + try { + AccessTokenCredentials accessToken = cred.unpack(AccessTokenCredentials.class); + if (accessToken.getToken().isEmpty()) { + throw new GrpcServiceParseException("Missing or empty access token in call credentials."); + } + return Optional + .of(new SecurityAwareAccessTokenCredentials(MoreCallCredentials.from(OAuth2Credentials + .create(new AccessToken(accessToken.getToken(), new Date(Long.MAX_VALUE)))))); + } catch (InvalidProtocolBufferException e) { + throw new GrpcServiceParseException( + "Failed to parse access token credentials: " + e.getMessage()); + } + } + return Optional.empty(); + } + + private static Optional extractCallCredentials(List callCredentialPlugins) + throws GrpcServiceParseException { + List creds = new ArrayList<>(); + for (Any cred : callCredentialPlugins) { + Optional parsed = callCredsFromProto(cred); + if (parsed.isPresent()) { + creds.add(parsed.get()); + } + } + return creds.stream().reduce(CompositeCallCredentials::new); + } + + private static final class SecurityAwareAccessTokenCredentials extends CallCredentials { + + private final CallCredentials delegate; + + SecurityAwareAccessTokenCredentials(CallCredentials delegate) { + this.delegate = delegate; + } + + @Override + public void applyRequestMetadata(RequestInfo requestInfo, Executor appExecutor, + MetadataApplier applier) { + if (requestInfo.getSecurityLevel() != SecurityLevel.PRIVACY_AND_INTEGRITY) { + applier.fail(Status.UNAUTHENTICATED.withDescription( + "OAuth2 credentials require connection with PRIVACY_AND_INTEGRITY security level")); + return; + } + delegate.applyRequestMetadata(requestInfo, appExecutor, applier); + } + } + + + + static final class ProtoChannelCredsConfig implements ChannelCredsConfig { + private final String type; + private final Any configProto; + + ProtoChannelCredsConfig(String type, Any configProto) { + this.type = type; + this.configProto = configProto; + } + + @Override + public String type() { + return type; + } + + Any configProto() { + return configProto; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ProtoChannelCredsConfig that = (ProtoChannelCredsConfig) o; + return java.util.Objects.equals(type, that.type) + && java.util.Objects.equals(configProto, that.configProto); + } + + @Override + public int hashCode() { + return java.util.Objects.hash(type, configProto); + } + } + + + +} diff --git a/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceXdsContext.java b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceXdsContext.java new file mode 100644 index 00000000000..77ae8cffe03 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceXdsContext.java @@ -0,0 +1,71 @@ +/* + * 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.grpcservice; + +import com.google.auto.value.AutoValue; +import io.grpc.CallCredentials; +import io.grpc.Internal; +import java.util.Optional; + +/** + * Contextual abstraction needed during xDS plugin parsing. + * Represents the context for a single target URI. + */ +@AutoValue +@Internal +public abstract class GrpcServiceXdsContext { + + public abstract boolean isTrustedControlPlane(); + + public abstract Optional validAllowedGrpcService(); + + public abstract boolean isTargetUriSchemeSupported(); + + public static GrpcServiceXdsContext create( + boolean isTrustedControlPlane, + Optional validAllowedGrpcService, + boolean isTargetUriSchemeSupported) { + return new AutoValue_GrpcServiceXdsContext( + isTrustedControlPlane, + validAllowedGrpcService, + isTargetUriSchemeSupported); + } + + /** + * Represents an allowed gRPC service configuration with local credentials. + */ + @AutoValue + public abstract static class AllowedGrpcService { + public abstract ConfiguredChannelCredentials configuredChannelCredentials(); + + public abstract Optional callCredentials(); + + public static Builder builder() { + return new AutoValue_GrpcServiceXdsContext_AllowedGrpcService.Builder(); + } + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder configuredChannelCredentials( + ConfiguredChannelCredentials credentials); + + public abstract Builder callCredentials(CallCredentials callCredentials); + + public abstract AllowedGrpcService build(); + } + } +} diff --git a/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceXdsContextProvider.java b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceXdsContextProvider.java new file mode 100644 index 00000000000..411a9e06977 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceXdsContextProvider.java @@ -0,0 +1,31 @@ +/* + * 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.grpcservice; + +import io.grpc.Internal; + +/** + * Provider interface to retrieve target-specific xDS context. + */ +@Internal +public interface GrpcServiceXdsContextProvider { + + /** + * Returns the `GrpcServiceXdsContext` for the given internal target URI. + */ + GrpcServiceXdsContext getContextForTarget(String targetUri); +} diff --git a/xds/src/main/java/io/grpc/xds/internal/grpcservice/HeaderValue.java b/xds/src/main/java/io/grpc/xds/internal/grpcservice/HeaderValue.java new file mode 100644 index 00000000000..1b7bb283744 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/grpcservice/HeaderValue.java @@ -0,0 +1,44 @@ +/* + * 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.grpcservice; + +import com.google.auto.value.AutoValue; +import com.google.protobuf.ByteString; +import java.util.Optional; + +/** + * Represents a header to be mutated or added as part of xDS configuration. + * Avoids direct dependency on Envoy's proto objects while providing an immutable representation. + */ +@AutoValue +public abstract class HeaderValue { + + public static HeaderValue create(String key, String value) { + return new AutoValue_HeaderValue(key, Optional.of(value), Optional.empty()); + } + + public static HeaderValue create(String key, ByteString rawValue) { + return new AutoValue_HeaderValue(key, Optional.empty(), Optional.of(rawValue)); + } + + + public abstract String key(); + + public abstract Optional value(); + + public abstract Optional rawValue(); +} diff --git a/xds/src/main/java/io/grpc/xds/internal/grpcservice/InsecureGrpcChannelFactory.java b/xds/src/main/java/io/grpc/xds/internal/grpcservice/InsecureGrpcChannelFactory.java deleted file mode 100644 index d6325d43be4..00000000000 --- a/xds/src/main/java/io/grpc/xds/internal/grpcservice/InsecureGrpcChannelFactory.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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.grpcservice; - -import io.grpc.Grpc; -import io.grpc.ManagedChannel; - -/** - * An insecure implementation of {@link GrpcServiceConfigChannelFactory} that creates a plaintext - * channel. This is a stub implementation for channel creation until the GrpcService trusted server - * implementation is completely implemented. - */ -public final class InsecureGrpcChannelFactory implements GrpcServiceConfigChannelFactory { - - private static final InsecureGrpcChannelFactory INSTANCE = new InsecureGrpcChannelFactory(); - - private InsecureGrpcChannelFactory() {} - - public static InsecureGrpcChannelFactory getInstance() { - return INSTANCE; - } - - @Override - public ManagedChannel createChannel(GrpcServiceConfig config) { - GrpcServiceConfig.GoogleGrpcConfig googleGrpc = config.googleGrpc(); - return Grpc.newChannelBuilder(googleGrpc.target(), - googleGrpc.hashedChannelCredentials().channelCredentials()).build(); - } -} diff --git a/xds/src/main/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesConfig.java b/xds/src/main/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesConfig.java index fd8048fdbd2..249a587ce53 100644 --- a/xds/src/main/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesConfig.java +++ b/xds/src/main/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesConfig.java @@ -17,9 +17,9 @@ package io.grpc.xds.internal.headermutations; import com.google.auto.value.AutoValue; +import com.google.re2j.Pattern; import io.envoyproxy.envoy.config.common.mutation_rules.v3.HeaderMutationRules; import java.util.Optional; -import java.util.regex.Pattern; /** * Represents the configuration for header mutation rules, as defined in the diff --git a/xds/src/main/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesParser.java b/xds/src/main/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesParser.java new file mode 100644 index 00000000000..b00db519d45 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesParser.java @@ -0,0 +1,55 @@ +/* + * 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.headermutations; + +import com.google.re2j.Pattern; +import com.google.re2j.PatternSyntaxException; +import io.envoyproxy.envoy.config.common.mutation_rules.v3.HeaderMutationRules; +import io.grpc.xds.internal.extauthz.ExtAuthzParseException; + +/** + * Parser for {@link io.envoyproxy.envoy.config.common.mutation_rules.v3.HeaderMutationRules}. + */ +public final class HeaderMutationRulesParser { + + private HeaderMutationRulesParser() {} + + public static HeaderMutationRulesConfig parse(HeaderMutationRules proto) + throws ExtAuthzParseException { + HeaderMutationRulesConfig.Builder builder = HeaderMutationRulesConfig.builder(); + builder.disallowAll(proto.getDisallowAll().getValue()); + builder.disallowIsError(proto.getDisallowIsError().getValue()); + if (proto.hasAllowExpression()) { + builder.allowExpression( + parseRegex(proto.getAllowExpression().getRegex(), "allow_expression")); + } + if (proto.hasDisallowExpression()) { + builder.disallowExpression( + parseRegex(proto.getDisallowExpression().getRegex(), "disallow_expression")); + } + return builder.build(); + } + + private static Pattern parseRegex(String regex, String fieldName) throws ExtAuthzParseException { + try { + return Pattern.compile(regex); + } catch (PatternSyntaxException e) { + throw new ExtAuthzParseException( + "Invalid regex pattern for " + fieldName + ": " + e.getMessage(), e); + } + } +} diff --git a/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java b/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java index 0a303b7255d..b72658a9bf6 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java @@ -37,6 +37,7 @@ import io.grpc.xds.client.EnvoyProtoData.Node; import io.grpc.xds.client.Locality; import io.grpc.xds.client.XdsInitializationException; +import io.grpc.xds.internal.grpcservice.GrpcServiceXdsContext.AllowedGrpcService; import java.io.IOException; import java.util.List; import java.util.Map; @@ -97,6 +98,60 @@ public void parseBootstrap_emptyServers_throws() { assertThat(e).hasMessageThat().isEqualTo("Invalid bootstrap: 'xds_servers' is empty"); } + @Test + public void parseBootstrap_allowedGrpcServices() throws XdsInitializationException { + String rawData = "{\n" + + " \"xds_servers\": [\n" + + " {\n" + + " \"server_uri\": \"" + SERVER_URI + "\",\n" + + " \"channel_creds\": [{\"type\": \"insecure\"}]\n" + + " }\n" + + " ],\n" + + " \"allowed_grpc_services\": {\n" + + " \"dns:///foo.com:443\": {\n" + + " \"channel_creds\": [{\"type\": \"insecure\"}],\n" + + " \"call_creds\": [{\"type\": \"access_token\"}]\n" + + " }\n" + + " }\n" + + "}"; + + bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData)); + BootstrapInfo info = bootstrapper.bootstrap(); + @SuppressWarnings("unchecked") + Map allowed = + (Map) info.allowedGrpcServices().get(); + + assertThat(allowed).isNotNull(); + assertThat(allowed).containsKey("dns:///foo.com:443"); + AllowedGrpcService service = allowed.get("dns:///foo.com:443"); + assertThat(service.configuredChannelCredentials().channelCredentials()) + .isInstanceOf(InsecureChannelCredentials.class); + assertThat(service.callCredentials().isPresent()).isFalse(); + } + + @Test + public void parseBootstrap_allowedGrpcServices_invalidChannelCreds() { + String rawData = "{\n" + + " \"xds_servers\": [\n" + + " {\n" + + " \"server_uri\": \"" + SERVER_URI + "\",\n" + + " \"channel_creds\": [{\"type\": \"insecure\"}]\n" + + " }\n" + + " ],\n" + + " \"allowed_grpc_services\": {\n" + + " \"dns:///foo.com:443\": {\n" + + " \"channel_creds\": []\n" + + " }\n" + + " }\n" + + "}"; + + bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData)); + XdsInitializationException e = assertThrows(XdsInitializationException.class, + bootstrapper::bootstrap); + assertThat(e).hasMessageThat() + .isEqualTo("Invalid bootstrap: server dns:///foo.com:443 'channel_creds' required"); + } + @Test public void parseBootstrap_singleXdsServer() throws XdsInitializationException { String rawData = "{\n" diff --git a/xds/src/test/java/io/grpc/xds/internal/MatcherParserTest.java b/xds/src/test/java/io/grpc/xds/internal/MatcherParserTest.java new file mode 100644 index 00000000000..86a6a95fd4b --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/internal/MatcherParserTest.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 com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import io.envoyproxy.envoy.type.matcher.v3.RegexMatcher; +import io.envoyproxy.envoy.type.matcher.v3.StringMatcher; +import io.envoyproxy.envoy.type.v3.FractionalPercent; +import io.envoyproxy.envoy.type.v3.FractionalPercent.DenominatorType; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class MatcherParserTest { + + @Test + public void parseStringMatcher_exact() { + StringMatcher proto = + StringMatcher.newBuilder().setExact("exact-match").setIgnoreCase(true).build(); + Matchers.StringMatcher matcher = MatcherParser.parseStringMatcher(proto); + assertThat(matcher).isNotNull(); + } + + @Test + public void parseStringMatcher_allTypes() { + MatcherParser.parseStringMatcher(StringMatcher.newBuilder().setExact("test").build()); + MatcherParser.parseStringMatcher(StringMatcher.newBuilder().setPrefix("test").build()); + MatcherParser.parseStringMatcher(StringMatcher.newBuilder().setSuffix("test").build()); + MatcherParser.parseStringMatcher(StringMatcher.newBuilder().setContains("test").build()); + MatcherParser.parseStringMatcher(StringMatcher.newBuilder() + .setSafeRegex(RegexMatcher.newBuilder().setRegex(".*").build()).build()); + } + + @Test + public void parseStringMatcher_unknownTypeThrows() { + StringMatcher unknownProto = StringMatcher.getDefaultInstance(); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> MatcherParser.parseStringMatcher(unknownProto)); + assertThat(exception).hasMessageThat().contains("Unknown StringMatcher match pattern"); + } + + @Test + public void parseFractionMatcher_denominators() { + Matchers.FractionMatcher hundred = MatcherParser.parseFractionMatcher(FractionalPercent + .newBuilder().setNumerator(1).setDenominator(DenominatorType.HUNDRED).build()); + assertThat(hundred.numerator()).isEqualTo(1); + assertThat(hundred.denominator()).isEqualTo(100); + + Matchers.FractionMatcher tenThousand = MatcherParser.parseFractionMatcher(FractionalPercent + .newBuilder().setNumerator(2).setDenominator(DenominatorType.TEN_THOUSAND).build()); + assertThat(tenThousand.numerator()).isEqualTo(2); + assertThat(tenThousand.denominator()).isEqualTo(10_000); + + Matchers.FractionMatcher million = MatcherParser.parseFractionMatcher(FractionalPercent + .newBuilder().setNumerator(3).setDenominator(DenominatorType.MILLION).build()); + assertThat(million.numerator()).isEqualTo(3); + assertThat(million.denominator()).isEqualTo(1_000_000); + } + + @Test + public void parseFractionMatcher_unknownDenominatorThrows() { + FractionalPercent unknownProto = + FractionalPercent.newBuilder().setDenominatorValue(999).build(); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> MatcherParser.parseFractionMatcher(unknownProto)); + assertThat(exception).hasMessageThat().contains("Unknown denominator type"); + } +} diff --git a/xds/src/test/java/io/grpc/xds/internal/XdsHeaderValidatorTest.java b/xds/src/test/java/io/grpc/xds/internal/XdsHeaderValidatorTest.java new file mode 100644 index 00000000000..c6c99c6d46f --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/internal/XdsHeaderValidatorTest.java @@ -0,0 +1,64 @@ +/* + * 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.truth.Truth.assertThat; + +import com.google.common.base.Strings; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class XdsHeaderValidatorTest { + + @Test + public void isValid_validKeyAndLength_returnsTrue() { + assertThat(XdsHeaderValidator.isValid("valid-key", 10)).isTrue(); + } + + @Test + public void isValid_emptyKey_returnsFalse() { + assertThat(XdsHeaderValidator.isValid("", 10)).isFalse(); + } + + @Test + public void isValid_uppercaseKey_returnsFalse() { + assertThat(XdsHeaderValidator.isValid("Invalid-Key", 10)).isFalse(); + } + + @Test + public void isValid_keyExceedsMaxLength_returnsFalse() { + String longKey = Strings.repeat("k", 16385); + assertThat(XdsHeaderValidator.isValid(longKey, 10)).isFalse(); + } + + @Test + public void isValid_valueExceedsMaxLength_returnsFalse() { + assertThat(XdsHeaderValidator.isValid("valid-key", 16385)).isFalse(); + } + + @Test + public void isValid_hostKey_returnsFalse() { + assertThat(XdsHeaderValidator.isValid("host", 10)).isFalse(); + } + + @Test + public void isValid_pseudoHeaderKey_returnsFalse() { + assertThat(XdsHeaderValidator.isValid(":method", 10)).isFalse(); + } +} diff --git a/xds/src/test/java/io/grpc/xds/internal/extauthz/ExtAuthzConfigTest.java b/xds/src/test/java/io/grpc/xds/internal/extauthz/ExtAuthzConfigParserTest.java similarity index 63% rename from xds/src/test/java/io/grpc/xds/internal/extauthz/ExtAuthzConfigTest.java rename to xds/src/test/java/io/grpc/xds/internal/extauthz/ExtAuthzConfigParserTest.java index 9b9a55b4079..373ad98552d 100644 --- a/xds/src/test/java/io/grpc/xds/internal/extauthz/ExtAuthzConfigTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/extauthz/ExtAuthzConfigParserTest.java @@ -42,12 +42,12 @@ import org.junit.runners.JUnit4; @RunWith(JUnit4.class) -public class ExtAuthzConfigTest { +public class ExtAuthzConfigParserTest { private static final Any GOOGLE_DEFAULT_CHANNEL_CREDS = Any.pack(GoogleDefaultCredentials.newBuilder().build()); private static final Any FAKE_ACCESS_TOKEN_CALL_CREDS = - Any.pack(AccessTokenCredentials.newBuilder().build()); + Any.pack(AccessTokenCredentials.newBuilder().setToken("fake-token").build()); private ExtAuthz.Builder extAuthzBuilder; @@ -63,10 +63,11 @@ public void setUp() { } @Test - public void fromProto_missingGrpcService_throws() { + public void parse_missingGrpcService_throws() { ExtAuthz extAuthz = ExtAuthz.newBuilder().build(); try { - ExtAuthzConfig.fromProto(extAuthz); + ExtAuthzConfigParser.parse(extAuthz, + io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextTestUtil.dummyProvider()); fail("Expected ExtAuthzParseException"); } catch (ExtAuthzParseException e) { assertThat(e).hasMessageThat() @@ -75,12 +76,13 @@ public void fromProto_missingGrpcService_throws() { } @Test - public void fromProto_invalidGrpcService_throws() { + public void parse_invalidGrpcService_throws() { ExtAuthz extAuthz = ExtAuthz.newBuilder() .setGrpcService(io.envoyproxy.envoy.config.core.v3.GrpcService.newBuilder().build()) .build(); try { - ExtAuthzConfig.fromProto(extAuthz); + ExtAuthzConfigParser.parse(extAuthz, + io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextTestUtil.dummyProvider()); fail("Expected ExtAuthzParseException"); } catch (ExtAuthzParseException e) { assertThat(e).hasMessageThat().startsWith("Failed to parse GrpcService config:"); @@ -88,13 +90,14 @@ public void fromProto_invalidGrpcService_throws() { } @Test - public void fromProto_invalidAllowExpression_throws() { + public void parse_invalidAllowExpression_throws() { ExtAuthz extAuthz = extAuthzBuilder .setDecoderHeaderMutationRules(HeaderMutationRules.newBuilder() .setAllowExpression(RegexMatcher.newBuilder().setRegex("[invalid").build()).build()) .build(); try { - ExtAuthzConfig.fromProto(extAuthz); + ExtAuthzConfigParser.parse(extAuthz, + io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextTestUtil.dummyProvider()); fail("Expected ExtAuthzParseException"); } catch (ExtAuthzParseException e) { assertThat(e).hasMessageThat().startsWith("Invalid regex pattern for allow_expression:"); @@ -102,13 +105,14 @@ public void fromProto_invalidAllowExpression_throws() { } @Test - public void fromProto_invalidDisallowExpression_throws() { + public void parse_invalidDisallowExpression_throws() { ExtAuthz extAuthz = extAuthzBuilder .setDecoderHeaderMutationRules(HeaderMutationRules.newBuilder() .setDisallowExpression(RegexMatcher.newBuilder().setRegex("[invalid").build()).build()) .build(); try { - ExtAuthzConfig.fromProto(extAuthz); + ExtAuthzConfigParser.parse(extAuthz, + io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextTestUtil.dummyProvider()); fail("Expected ExtAuthzParseException"); } catch (ExtAuthzParseException e) { assertThat(e).hasMessageThat().startsWith("Invalid regex pattern for disallow_expression:"); @@ -116,37 +120,40 @@ public void fromProto_invalidDisallowExpression_throws() { } @Test - public void fromProto_success() throws ExtAuthzParseException { - ExtAuthz extAuthz = extAuthzBuilder - .setGrpcService(extAuthzBuilder.getGrpcServiceBuilder() - .setTimeout(com.google.protobuf.Duration.newBuilder().setSeconds(5).build()) - .addInitialMetadata(HeaderValue.newBuilder().setKey("key").setValue("value").build()) - .build()) - .setFailureModeAllow(true).setFailureModeAllowHeaderAdd(true) - .setIncludePeerCertificate(true) - .setStatusOnError( - io.envoyproxy.envoy.type.v3.HttpStatus.newBuilder().setCodeValue(403).build()) - .setDenyAtDisable( - RuntimeFeatureFlag.newBuilder().setDefaultValue(BoolValue.of(true)).build()) - .setFilterEnabled(RuntimeFractionalPercent.newBuilder() - .setDefaultValue(FractionalPercent.newBuilder().setNumerator(50) - .setDenominator(DenominatorType.TEN_THOUSAND).build()) - .build()) - .setAllowedHeaders(ListStringMatcher.newBuilder() - .addPatterns(StringMatcher.newBuilder().setExact("allowed-header").build()).build()) - .setDisallowedHeaders(ListStringMatcher.newBuilder() - .addPatterns(StringMatcher.newBuilder().setPrefix("disallowed-").build()).build()) - .setDecoderHeaderMutationRules(HeaderMutationRules.newBuilder() - .setAllowExpression(RegexMatcher.newBuilder().setRegex("allow.*").build()) - .setDisallowExpression(RegexMatcher.newBuilder().setRegex("disallow.*").build()) - .setDisallowAll(BoolValue.of(true)).setDisallowIsError(BoolValue.of(true)).build()) - .build(); + public void parse_success() throws ExtAuthzParseException { + ExtAuthz extAuthz = + extAuthzBuilder + .setGrpcService(extAuthzBuilder.getGrpcServiceBuilder() + .setTimeout(com.google.protobuf.Duration.newBuilder().setSeconds(5).build()) + .addInitialMetadata( + HeaderValue.newBuilder().setKey("key").setValue("value").build()) + .build()) + .setFailureModeAllow(true).setFailureModeAllowHeaderAdd(true) + .setIncludePeerCertificate(true) + .setStatusOnError( + io.envoyproxy.envoy.type.v3.HttpStatus.newBuilder().setCodeValue(403).build()) + .setDenyAtDisable( + RuntimeFeatureFlag.newBuilder().setDefaultValue(BoolValue.of(true)).build()) + .setFilterEnabled(RuntimeFractionalPercent.newBuilder() + .setDefaultValue(FractionalPercent.newBuilder().setNumerator(50) + .setDenominator(DenominatorType.TEN_THOUSAND).build()) + .build()) + .setAllowedHeaders(ListStringMatcher.newBuilder() + .addPatterns(StringMatcher.newBuilder().setExact("allowed-header").build()).build()) + .setDisallowedHeaders(ListStringMatcher.newBuilder() + .addPatterns(StringMatcher.newBuilder().setPrefix("disallowed-").build()).build()) + .setDecoderHeaderMutationRules(HeaderMutationRules.newBuilder() + .setAllowExpression(RegexMatcher.newBuilder().setRegex("allow.*").build()) + .setDisallowExpression(RegexMatcher.newBuilder().setRegex("disallow.*").build()) + .setDisallowAll(BoolValue.of(true)).setDisallowIsError(BoolValue.of(true)).build()) + .build(); - ExtAuthzConfig config = ExtAuthzConfig.fromProto(extAuthz); + ExtAuthzConfig config = ExtAuthzConfigParser.parse(extAuthz, + io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextTestUtil.dummyProvider()); assertThat(config.grpcService().googleGrpc().target()).isEqualTo("test-cluster"); assertThat(config.grpcService().timeout().get().getSeconds()).isEqualTo(5); - assertThat(config.grpcService().initialMetadata().isPresent()).isTrue(); + assertThat(config.grpcService().initialMetadata()).isNotEmpty(); assertThat(config.failureModeAllow()).isTrue(); assertThat(config.failureModeAllowHeaderAdd()).isTrue(); assertThat(config.includePeerCertificate()).isTrue(); @@ -167,10 +174,11 @@ public void fromProto_success() throws ExtAuthzParseException { } @Test - public void fromProto_saneDefaults() throws ExtAuthzParseException { + public void parse_saneDefaults() throws ExtAuthzParseException { ExtAuthz extAuthz = extAuthzBuilder.build(); - ExtAuthzConfig config = ExtAuthzConfig.fromProto(extAuthz); + ExtAuthzConfig config = ExtAuthzConfigParser.parse(extAuthz, + io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextTestUtil.dummyProvider()); assertThat(config.failureModeAllow()).isFalse(); assertThat(config.failureModeAllowHeaderAdd()).isFalse(); @@ -184,13 +192,14 @@ public void fromProto_saneDefaults() throws ExtAuthzParseException { } @Test - public void fromProto_headerMutationRules_allowExpressionOnly() throws ExtAuthzParseException { + public void parse_headerMutationRules_allowExpressionOnly() throws ExtAuthzParseException { ExtAuthz extAuthz = extAuthzBuilder .setDecoderHeaderMutationRules(HeaderMutationRules.newBuilder() .setAllowExpression(RegexMatcher.newBuilder().setRegex("allow.*").build()).build()) .build(); - ExtAuthzConfig config = ExtAuthzConfig.fromProto(extAuthz); + ExtAuthzConfig config = ExtAuthzConfigParser.parse(extAuthz, + io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextTestUtil.dummyProvider()); assertThat(config.decoderHeaderMutationRules().isPresent()).isTrue(); HeaderMutationRulesConfig rules = config.decoderHeaderMutationRules().get(); @@ -199,14 +208,14 @@ public void fromProto_headerMutationRules_allowExpressionOnly() throws ExtAuthzP } @Test - public void fromProto_headerMutationRules_disallowExpressionOnly() throws ExtAuthzParseException { - ExtAuthz extAuthz = extAuthzBuilder - .setDecoderHeaderMutationRules(HeaderMutationRules.newBuilder() + public void parse_headerMutationRules_disallowExpressionOnly() throws ExtAuthzParseException { + ExtAuthz extAuthz = + extAuthzBuilder.setDecoderHeaderMutationRules(HeaderMutationRules.newBuilder() .setDisallowExpression(RegexMatcher.newBuilder().setRegex("disallow.*").build()) - .build()) - .build(); + .build()).build(); - ExtAuthzConfig config = ExtAuthzConfig.fromProto(extAuthz); + ExtAuthzConfig config = ExtAuthzConfigParser.parse(extAuthz, + io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextTestUtil.dummyProvider()); assertThat(config.decoderHeaderMutationRules().isPresent()).isTrue(); HeaderMutationRulesConfig rules = config.decoderHeaderMutationRules().get(); @@ -215,45 +224,46 @@ public void fromProto_headerMutationRules_disallowExpressionOnly() throws ExtAut } @Test - public void fromProto_filterEnabled_hundred() throws ExtAuthzParseException { + public void parse_filterEnabled_hundred() throws ExtAuthzParseException { ExtAuthz extAuthz = extAuthzBuilder .setFilterEnabled(RuntimeFractionalPercent.newBuilder().setDefaultValue(FractionalPercent .newBuilder().setNumerator(25).setDenominator(DenominatorType.HUNDRED).build()).build()) .build(); - ExtAuthzConfig config = ExtAuthzConfig.fromProto(extAuthz); + ExtAuthzConfig config = ExtAuthzConfigParser.parse(extAuthz, + io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextTestUtil.dummyProvider()); assertThat(config.filterEnabled()).isEqualTo(Matchers.FractionMatcher.create(25, 100)); } @Test - public void fromProto_filterEnabled_million() throws ExtAuthzParseException { + public void parse_filterEnabled_million() throws ExtAuthzParseException { ExtAuthz extAuthz = extAuthzBuilder .setFilterEnabled( RuntimeFractionalPercent.newBuilder().setDefaultValue(FractionalPercent.newBuilder() .setNumerator(123456).setDenominator(DenominatorType.MILLION).build()).build()) .build(); - ExtAuthzConfig config = ExtAuthzConfig.fromProto(extAuthz); + ExtAuthzConfig config = ExtAuthzConfigParser.parse(extAuthz, + io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextTestUtil.dummyProvider()); assertThat(config.filterEnabled()) .isEqualTo(Matchers.FractionMatcher.create(123456, 1_000_000)); } @Test - public void fromProto_filterEnabled_unrecognizedDenominator() { - ExtAuthz extAuthz = extAuthzBuilder - .setFilterEnabled(RuntimeFractionalPercent.newBuilder() - .setDefaultValue( - FractionalPercent.newBuilder().setNumerator(1).setDenominatorValue(4).build()) - .build()) - .build(); + public void parse_filterEnabled_unrecognizedDenominator() { + ExtAuthz extAuthz = extAuthzBuilder.setFilterEnabled(RuntimeFractionalPercent.newBuilder() + .setDefaultValue( + FractionalPercent.newBuilder().setNumerator(1).setDenominatorValue(4).build()) + .build()).build(); try { - ExtAuthzConfig.fromProto(extAuthz); + ExtAuthzConfigParser.parse(extAuthz, + io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextTestUtil.dummyProvider()); fail("Expected ExtAuthzParseException"); } catch (ExtAuthzParseException e) { assertThat(e).hasMessageThat().isEqualTo("Unknown denominator type: UNRECOGNIZED"); } } -} \ No newline at end of file +} diff --git a/xds/src/test/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParserTest.java b/xds/src/test/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParserTest.java new file mode 100644 index 00000000000..1a7634aadf7 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParserTest.java @@ -0,0 +1,390 @@ +/* + * 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.grpcservice; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.protobuf.Any; +import com.google.protobuf.Duration; +import io.envoyproxy.envoy.config.core.v3.GrpcService; +import io.envoyproxy.envoy.config.core.v3.HeaderValue; +import io.envoyproxy.envoy.extensions.grpc_service.call_credentials.access_token.v3.AccessTokenCredentials; +import io.envoyproxy.envoy.extensions.grpc_service.channel_credentials.google_default.v3.GoogleDefaultCredentials; +import io.envoyproxy.envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials; +import io.envoyproxy.envoy.extensions.grpc_service.channel_credentials.local.v3.LocalCredentials; +import io.envoyproxy.envoy.extensions.grpc_service.channel_credentials.xds.v3.XdsCredentials; +import io.grpc.InsecureChannelCredentials; +import io.grpc.xds.internal.grpcservice.GrpcServiceXdsContext.AllowedGrpcService; +import java.nio.charset.StandardCharsets; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class GrpcServiceConfigParserTest { + + private static final String CALL_CREDENTIALS_CLASS_NAME = + "io.grpc.xds.internal.grpcservice.GrpcServiceConfigParser" + + "$SecurityAwareAccessTokenCredentials"; + + @Test + public void parse_success() throws GrpcServiceParseException { + Any insecureCreds = Any.pack(InsecureCredentials.getDefaultInstance()); + Any accessTokenCreds = + Any.pack(AccessTokenCredentials.newBuilder().setToken("test_token").build()); + GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") + .addChannelCredentialsPlugin(insecureCreds).addCallCredentialsPlugin(accessTokenCreds) + .build(); + HeaderValue asciiHeader = + HeaderValue.newBuilder().setKey("test_key").setValue("test_value").build(); + HeaderValue binaryHeader = + HeaderValue.newBuilder().setKey("test_key-bin").setRawValue(com.google.protobuf.ByteString + .copyFrom("test_value_binary".getBytes(StandardCharsets.UTF_8))).build(); + Duration timeout = Duration.newBuilder().setSeconds(10).build(); + GrpcService grpcService = + GrpcService.newBuilder().setGoogleGrpc(googleGrpc).addInitialMetadata(asciiHeader) + .addInitialMetadata(binaryHeader).setTimeout(timeout).build(); + + GrpcServiceConfig config = GrpcServiceConfigParser.parse(grpcService, + io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextTestUtil.dummyProvider()); + + // Assert target URI + assertThat(config.googleGrpc().target()).isEqualTo("test_uri"); + + // Assert channel credentials + assertThat(config.googleGrpc().configuredChannelCredentials().channelCredentials()) + .isInstanceOf(InsecureChannelCredentials.class); + GrpcServiceConfigParser.ProtoChannelCredsConfig credsConfig = + (GrpcServiceConfigParser.ProtoChannelCredsConfig) + config.googleGrpc().configuredChannelCredentials().channelCredsConfig(); + assertThat(credsConfig.configProto()).isEqualTo(insecureCreds); + + // Assert call credentials + assertThat(config.googleGrpc().callCredentials().isPresent()).isTrue(); + assertThat(config.googleGrpc().callCredentials().get().getClass().getName()) + .isEqualTo(CALL_CREDENTIALS_CLASS_NAME); + + // Assert initial metadata + assertThat(config.initialMetadata()).isNotEmpty(); + assertThat(config.initialMetadata().get(0).key()).isEqualTo("test_key"); + assertThat(config.initialMetadata().get(0).value().get()).isEqualTo("test_value"); + assertThat(config.initialMetadata().get(1).key()).isEqualTo("test_key-bin"); + assertThat(config.initialMetadata().get(1).rawValue().get().toByteArray()) + .isEqualTo("test_value_binary".getBytes(StandardCharsets.UTF_8)); + + // Assert timeout + assertThat(config.timeout().isPresent()).isTrue(); + assertThat(config.timeout().get()).isEqualTo(java.time.Duration.ofSeconds(10)); + } + + @Test + public void parse_minimalSuccess_defaults() throws GrpcServiceParseException { + Any insecureCreds = Any.pack(InsecureCredentials.getDefaultInstance()); + Any accessTokenCreds = + Any.pack(AccessTokenCredentials.newBuilder().setToken("test_token").build()); + GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") + .addChannelCredentialsPlugin(insecureCreds).addCallCredentialsPlugin(accessTokenCreds) + .build(); + GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); + + GrpcServiceConfig config = GrpcServiceConfigParser.parse(grpcService, + io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextTestUtil.dummyProvider()); + + assertThat(config.googleGrpc().target()).isEqualTo("test_uri"); + assertThat(config.initialMetadata()).isEmpty(); + assertThat(config.timeout().isPresent()).isFalse(); + } + + @Test + public void parse_missingGoogleGrpc() { + GrpcService grpcService = GrpcService.newBuilder().build(); + GrpcServiceParseException exception = assertThrows(GrpcServiceParseException.class, + () -> GrpcServiceConfigParser.parse(grpcService, + io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextTestUtil + .dummyProvider())); + assertThat(exception).hasMessageThat() + .startsWith("Unsupported: GrpcService must have GoogleGrpc, got: "); + } + + @Test + public void parse_emptyCallCredentials() throws GrpcServiceParseException { + Any insecureCreds = Any.pack(InsecureCredentials.getDefaultInstance()); + GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") + .addChannelCredentialsPlugin(insecureCreds).build(); + GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); + + GrpcServiceConfig config = GrpcServiceConfigParser.parse(grpcService, + io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextTestUtil.dummyProvider()); + + assertThat(config.googleGrpc().callCredentials().isPresent()).isFalse(); + } + + @Test + public void parse_emptyChannelCredentials() { + Any accessTokenCreds = + Any.pack(AccessTokenCredentials.newBuilder().setToken("test_token").build()); + GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") + .addCallCredentialsPlugin(accessTokenCreds).build(); + GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); + GrpcServiceParseException exception = assertThrows(GrpcServiceParseException.class, + () -> GrpcServiceConfigParser.parse(grpcService, + io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextTestUtil + .dummyProvider())); + assertThat(exception).hasMessageThat() + .isEqualTo("No valid supported channel_credentials found"); + } + + @Test + public void parse_googleDefaultCredentials() throws GrpcServiceParseException { + Any googleDefaultCreds = Any.pack(GoogleDefaultCredentials.getDefaultInstance()); + Any accessTokenCreds = + Any.pack(AccessTokenCredentials.newBuilder().setToken("test_token").build()); + GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") + .addChannelCredentialsPlugin(googleDefaultCreds).addCallCredentialsPlugin(accessTokenCreds) + .build(); + GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); + + GrpcServiceConfig config = GrpcServiceConfigParser.parse(grpcService, + io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextTestUtil.dummyProvider()); + + assertThat(config.googleGrpc().configuredChannelCredentials().channelCredentials()) + .isInstanceOf(io.grpc.CompositeChannelCredentials.class); + GrpcServiceConfigParser.ProtoChannelCredsConfig credsConfig = + (GrpcServiceConfigParser.ProtoChannelCredsConfig) + config.googleGrpc().configuredChannelCredentials().channelCredsConfig(); + assertThat(credsConfig.configProto()).isEqualTo(googleDefaultCreds); + } + + @Test + public void parse_localCredentials() throws GrpcServiceParseException { + Any localCreds = Any.pack(LocalCredentials.getDefaultInstance()); + Any accessTokenCreds = + Any.pack(AccessTokenCredentials.newBuilder().setToken("test_token").build()); + GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") + .addChannelCredentialsPlugin(localCreds).addCallCredentialsPlugin(accessTokenCreds).build(); + GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); + + UnsupportedOperationException exception = assertThrows(UnsupportedOperationException.class, + () -> GrpcServiceConfigParser.parse(grpcService, + io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextTestUtil + .dummyProvider())); + assertThat(exception).hasMessageThat() + .contains("LocalCredentials are not supported in grpc-java"); + } + + @Test + public void parse_xdsCredentials_withInsecureFallback() throws GrpcServiceParseException { + Any insecureCreds = Any.pack(InsecureCredentials.getDefaultInstance()); + XdsCredentials xdsCreds = + XdsCredentials.newBuilder().setFallbackCredentials(insecureCreds).build(); + Any xdsCredsAny = Any.pack(xdsCreds); + Any accessTokenCreds = + Any.pack(AccessTokenCredentials.newBuilder().setToken("test_token").build()); + GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") + .addChannelCredentialsPlugin(xdsCredsAny).addCallCredentialsPlugin(accessTokenCreds) + .build(); + GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); + + GrpcServiceConfig config = GrpcServiceConfigParser.parse(grpcService, + io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextTestUtil.dummyProvider()); + + assertThat(config.googleGrpc().configuredChannelCredentials().channelCredentials()) + .isInstanceOf(io.grpc.ChannelCredentials.class); + GrpcServiceConfigParser.ProtoChannelCredsConfig credsConfig = + (GrpcServiceConfigParser.ProtoChannelCredsConfig) + config.googleGrpc().configuredChannelCredentials().channelCredsConfig(); + assertThat(credsConfig.configProto()).isEqualTo(xdsCredsAny); + } + + @Test + public void parse_tlsCredentials_notSupported() { + Any tlsCreds = Any + .pack(io.envoyproxy.envoy.extensions.grpc_service.channel_credentials.tls.v3.TlsCredentials + .getDefaultInstance()); + Any accessTokenCreds = + Any.pack(AccessTokenCredentials.newBuilder().setToken("test_token").build()); + GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") + .addChannelCredentialsPlugin(tlsCreds).addCallCredentialsPlugin(accessTokenCreds).build(); + GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); + + UnsupportedOperationException exception = assertThrows(UnsupportedOperationException.class, + () -> GrpcServiceConfigParser.parse(grpcService, + io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextTestUtil + .dummyProvider())); + assertThat(exception).hasMessageThat() + .contains("TlsCredentials input stream construction pending"); + } + + @Test + public void parse_invalidChannelCredentialsProto() { + // Pack a Duration proto, but try to unpack it as GoogleDefaultCredentials + Any invalidCreds = Any.pack(com.google.protobuf.Duration.getDefaultInstance()); + Any accessTokenCreds = + Any.pack(AccessTokenCredentials.newBuilder().setToken("test_token").build()); + GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") + .addChannelCredentialsPlugin(invalidCreds).addCallCredentialsPlugin(accessTokenCreds) + .build(); + GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); + + GrpcServiceParseException exception = assertThrows(GrpcServiceParseException.class, + () -> GrpcServiceConfigParser.parse(grpcService, + io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextTestUtil + .dummyProvider())); + assertThat(exception).hasMessageThat().contains("No valid supported channel_credentials found"); + } + + @Test + public void parse_ignoredUnsupportedCallCredentialsProto() throws GrpcServiceParseException { + // Pack a Duration proto, but try to unpack it as AccessTokenCredentials + Any insecureCreds = Any.pack(InsecureCredentials.getDefaultInstance()); + Any invalidCallCredentials = Any.pack(Duration.getDefaultInstance()); + GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") + .addChannelCredentialsPlugin(insecureCreds).addCallCredentialsPlugin(invalidCallCredentials) + .build(); + GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); + + GrpcServiceConfig config = GrpcServiceConfigParser.parse(grpcService, + io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextTestUtil.dummyProvider()); + assertThat(config.googleGrpc().callCredentials().isPresent()).isFalse(); + } + + @Test + public void parse_invalidAccessTokenCallCredentialsProto() { + Any insecureCreds = Any.pack(InsecureCredentials.getDefaultInstance()); + Any invalidCallCredentials = Any.pack(AccessTokenCredentials.newBuilder().setToken("").build()); + GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") + .addChannelCredentialsPlugin(insecureCreds).addCallCredentialsPlugin(invalidCallCredentials) + .build(); + GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); + + GrpcServiceParseException exception = assertThrows(GrpcServiceParseException.class, + () -> GrpcServiceConfigParser.parse(grpcService, + io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextTestUtil + .dummyProvider())); + assertThat(exception).hasMessageThat() + .contains("Missing or empty access token in call credentials"); + } + + @Test + public void parse_multipleCallCredentials() throws GrpcServiceParseException { + Any insecureCreds = Any.pack(InsecureCredentials.getDefaultInstance()); + Any accessTokenCreds1 = + Any.pack(AccessTokenCredentials.newBuilder().setToken("token1").build()); + Any accessTokenCreds2 = + Any.pack(AccessTokenCredentials.newBuilder().setToken("token2").build()); + GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") + .addChannelCredentialsPlugin(insecureCreds).addCallCredentialsPlugin(accessTokenCreds1) + .addCallCredentialsPlugin(accessTokenCreds2).build(); + GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); + + GrpcServiceConfig config = GrpcServiceConfigParser.parse(grpcService, + io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextTestUtil.dummyProvider()); + + assertThat(config.googleGrpc().callCredentials().isPresent()).isTrue(); + assertThat(config.googleGrpc().callCredentials().get()) + .isInstanceOf(io.grpc.CompositeCallCredentials.class); + } + + @Test + public void parse_untrustedControlPlane_withoutOverride() { + Any insecureCreds = Any.pack(InsecureCredentials.getDefaultInstance()); + GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") + .addChannelCredentialsPlugin(insecureCreds).build(); + GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); + + GrpcServiceXdsContext untrustedContext = + GrpcServiceXdsContext.create(false, java.util.Optional.empty(), true); + + GrpcServiceParseException exception = assertThrows(GrpcServiceParseException.class, + () -> GrpcServiceConfigParser.parse(grpcService, targetUri -> untrustedContext)); + assertThat(exception).hasMessageThat() + .contains("Untrusted xDS server & URI not found in allowed_grpc_services"); + } + + @Test + public void parse_untrustedControlPlane_withOverride() throws GrpcServiceParseException { + // The proto credentials (insecure) should be ignored in favor of the override (google default) + Any insecureCreds = Any.pack(InsecureCredentials.getDefaultInstance()); + GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") + .addChannelCredentialsPlugin(insecureCreds).build(); + GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); + + ConfiguredChannelCredentials overrideChannelCreds = ConfiguredChannelCredentials.create( + io.grpc.alts.GoogleDefaultChannelCredentials.create(), + new GrpcServiceConfigParser.ProtoChannelCredsConfig( + GrpcServiceConfigParser.GOOGLE_DEFAULT_CREDENTIALS_TYPE_URL, + Any.pack(GoogleDefaultCredentials.getDefaultInstance()))); + AllowedGrpcService override = AllowedGrpcService.builder() + .configuredChannelCredentials(overrideChannelCreds).build(); + + GrpcServiceXdsContext untrustedContext = + GrpcServiceXdsContext.create(false, java.util.Optional.of(override), true); + + GrpcServiceConfig config = + GrpcServiceConfigParser.parse(grpcService, targetUri -> untrustedContext); + + // Assert channel credentials are the override, not the proto's insecure creds + assertThat(config.googleGrpc().configuredChannelCredentials().channelCredentials()) + .isInstanceOf(io.grpc.CompositeChannelCredentials.class); + } + + @Test + public void parse_invalidTimeout() { + Any insecureCreds = Any.pack(InsecureCredentials.getDefaultInstance()); + GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") + .addChannelCredentialsPlugin(insecureCreds).build(); + + // Negative timeout + Duration timeout = Duration.newBuilder().setSeconds(-10).build(); + GrpcService grpcService = GrpcService.newBuilder() + .setGoogleGrpc(googleGrpc).setTimeout(timeout).build(); + + GrpcServiceParseException exception = assertThrows(GrpcServiceParseException.class, + () -> GrpcServiceConfigParser.parse(grpcService, + GrpcServiceXdsContextTestUtil.dummyProvider())); + assertThat(exception).hasMessageThat() + .contains("Timeout must be strictly positive"); + + // Zero timeout + timeout = Duration.newBuilder().setSeconds(0).setNanos(0).build(); + GrpcService grpcServiceZero = GrpcService.newBuilder() + .setGoogleGrpc(googleGrpc).setTimeout(timeout).build(); + + exception = assertThrows(GrpcServiceParseException.class, + () -> GrpcServiceConfigParser.parse(grpcServiceZero, + GrpcServiceXdsContextTestUtil.dummyProvider())); + assertThat(exception).hasMessageThat() + .contains("Timeout must be strictly positive"); + } + + @Test + public void parseGoogleGrpcConfig_unsupportedScheme() { + Any insecureCreds = Any.pack(InsecureCredentials.getDefaultInstance()); + GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("unknown://test") + .addChannelCredentialsPlugin(insecureCreds).build(); + + GrpcServiceXdsContext context = + GrpcServiceXdsContext.create(true, java.util.Optional.empty(), false); + + GrpcServiceParseException exception = assertThrows(GrpcServiceParseException.class, + () -> GrpcServiceConfigParser.parseGoogleGrpcConfig(googleGrpc, targetUri -> context)); + assertThat(exception).hasMessageThat() + .contains("Target URI scheme is not resolvable"); + } +} diff --git a/xds/src/test/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigTest.java b/xds/src/test/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigTest.java deleted file mode 100644 index 7a506220973..00000000000 --- a/xds/src/test/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigTest.java +++ /dev/null @@ -1,243 +0,0 @@ -/* - * 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.grpcservice; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertThrows; - -import com.google.common.io.BaseEncoding; -import com.google.protobuf.Any; -import com.google.protobuf.Duration; -import io.envoyproxy.envoy.config.core.v3.GrpcService; -import io.envoyproxy.envoy.config.core.v3.HeaderValue; -import io.envoyproxy.envoy.extensions.grpc_service.call_credentials.access_token.v3.AccessTokenCredentials; -import io.envoyproxy.envoy.extensions.grpc_service.channel_credentials.google_default.v3.GoogleDefaultCredentials; -import io.envoyproxy.envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials; -import io.envoyproxy.envoy.extensions.grpc_service.channel_credentials.local.v3.LocalCredentials; -import io.envoyproxy.envoy.extensions.grpc_service.channel_credentials.xds.v3.XdsCredentials; -import io.grpc.InsecureChannelCredentials; -import io.grpc.Metadata; -import java.nio.charset.StandardCharsets; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class GrpcServiceConfigTest { - - @Test - public void fromProto_success() throws GrpcServiceParseException { - Any insecureCreds = Any.pack(InsecureCredentials.getDefaultInstance()); - Any accessTokenCreds = - Any.pack(AccessTokenCredentials.newBuilder().setToken("test_token").build()); - GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") - .addChannelCredentialsPlugin(insecureCreds).addCallCredentialsPlugin(accessTokenCreds) - .build(); - HeaderValue asciiHeader = - HeaderValue.newBuilder().setKey("test_key").setValue("test_value").build(); - HeaderValue binaryHeader = HeaderValue.newBuilder().setKey("test_key-bin") - .setValue( - BaseEncoding.base64().encode("test_value_binary".getBytes(StandardCharsets.UTF_8))) - .build(); - Duration timeout = Duration.newBuilder().setSeconds(10).build(); - GrpcService grpcService = - GrpcService.newBuilder().setGoogleGrpc(googleGrpc).addInitialMetadata(asciiHeader) - .addInitialMetadata(binaryHeader).setTimeout(timeout).build(); - - GrpcServiceConfig config = GrpcServiceConfig.fromProto(grpcService); - - // Assert target URI - assertThat(config.googleGrpc().target()).isEqualTo("test_uri"); - - // Assert channel credentials - assertThat(config.googleGrpc().hashedChannelCredentials().channelCredentials()) - .isInstanceOf(InsecureChannelCredentials.class); - assertThat(config.googleGrpc().hashedChannelCredentials().hash()) - .isEqualTo(insecureCreds.hashCode()); - - // Assert call credentials - assertThat(config.googleGrpc().callCredentials().getClass().getName()) - .isEqualTo("io.grpc.auth.GoogleAuthLibraryCallCredentials"); - - // Assert initial metadata - assertThat(config.initialMetadata().isPresent()).isTrue(); - assertThat(config.initialMetadata().get() - .get(Metadata.Key.of("test_key", Metadata.ASCII_STRING_MARSHALLER))) - .isEqualTo("test_value"); - assertThat(config.initialMetadata().get() - .get(Metadata.Key.of("test_key-bin", Metadata.BINARY_BYTE_MARSHALLER))) - .isEqualTo("test_value_binary".getBytes(StandardCharsets.UTF_8)); - - // Assert timeout - assertThat(config.timeout().isPresent()).isTrue(); - assertThat(config.timeout().get()).isEqualTo(java.time.Duration.ofSeconds(10)); - } - - @Test - public void fromProto_minimalSuccess_defaults() throws GrpcServiceParseException { - Any insecureCreds = Any.pack(InsecureCredentials.getDefaultInstance()); - Any accessTokenCreds = - Any.pack(AccessTokenCredentials.newBuilder().setToken("test_token").build()); - GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") - .addChannelCredentialsPlugin(insecureCreds).addCallCredentialsPlugin(accessTokenCreds) - .build(); - GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); - - GrpcServiceConfig config = GrpcServiceConfig.fromProto(grpcService); - - assertThat(config.googleGrpc().target()).isEqualTo("test_uri"); - assertThat(config.initialMetadata().isPresent()).isFalse(); - assertThat(config.timeout().isPresent()).isFalse(); - } - - @Test - public void fromProto_missingGoogleGrpc() { - GrpcService grpcService = GrpcService.newBuilder().build(); - GrpcServiceParseException exception = assertThrows(GrpcServiceParseException.class, - () -> GrpcServiceConfig.fromProto(grpcService)); - assertThat(exception).hasMessageThat() - .startsWith("Unsupported: GrpcService must have GoogleGrpc, got: "); - } - - @Test - public void fromProto_emptyCallCredentials() { - Any insecureCreds = Any.pack(InsecureCredentials.getDefaultInstance()); - GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") - .addChannelCredentialsPlugin(insecureCreds).build(); - GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); - GrpcServiceParseException exception = assertThrows(GrpcServiceParseException.class, - () -> GrpcServiceConfig.fromProto(grpcService)); - assertThat(exception).hasMessageThat() - .isEqualTo("No valid supported call_credentials found. Errors: []"); - } - - @Test - public void fromProto_emptyChannelCredentials() { - Any accessTokenCreds = - Any.pack(AccessTokenCredentials.newBuilder().setToken("test_token").build()); - GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") - .addCallCredentialsPlugin(accessTokenCreds).build(); - GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); - GrpcServiceParseException exception = assertThrows(GrpcServiceParseException.class, - () -> GrpcServiceConfig.fromProto(grpcService)); - assertThat(exception).hasMessageThat() - .isEqualTo("No valid supported channel_credentials found. Errors: []"); - } - - @Test - public void fromProto_googleDefaultCredentials() throws GrpcServiceParseException { - Any googleDefaultCreds = Any.pack(GoogleDefaultCredentials.getDefaultInstance()); - Any accessTokenCreds = - Any.pack(AccessTokenCredentials.newBuilder().setToken("test_token").build()); - GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") - .addChannelCredentialsPlugin(googleDefaultCreds).addCallCredentialsPlugin(accessTokenCreds) - .build(); - GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); - - GrpcServiceConfig config = GrpcServiceConfig.fromProto(grpcService); - - assertThat(config.googleGrpc().hashedChannelCredentials().channelCredentials()) - .isInstanceOf(io.grpc.CompositeChannelCredentials.class); - assertThat(config.googleGrpc().hashedChannelCredentials().hash()) - .isEqualTo(googleDefaultCreds.hashCode()); - } - - @Test - public void fromProto_localCredentials() throws GrpcServiceParseException { - Any localCreds = Any.pack(LocalCredentials.getDefaultInstance()); - Any accessTokenCreds = - Any.pack(AccessTokenCredentials.newBuilder().setToken("test_token").build()); - GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") - .addChannelCredentialsPlugin(localCreds).addCallCredentialsPlugin(accessTokenCreds).build(); - GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); - - GrpcServiceParseException exception = assertThrows(GrpcServiceParseException.class, - () -> GrpcServiceConfig.fromProto(grpcService)); - assertThat(exception).hasMessageThat().contains("LocalCredentials are not yet supported."); - } - - @Test - public void fromProto_xdsCredentials_withInsecureFallback() throws GrpcServiceParseException { - Any insecureCreds = Any.pack(InsecureCredentials.getDefaultInstance()); - XdsCredentials xdsCreds = - XdsCredentials.newBuilder().setFallbackCredentials(insecureCreds).build(); - Any xdsCredsAny = Any.pack(xdsCreds); - Any accessTokenCreds = - Any.pack(AccessTokenCredentials.newBuilder().setToken("test_token").build()); - GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") - .addChannelCredentialsPlugin(xdsCredsAny).addCallCredentialsPlugin(accessTokenCreds) - .build(); - GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); - - GrpcServiceConfig config = GrpcServiceConfig.fromProto(grpcService); - - assertThat(config.googleGrpc().hashedChannelCredentials().channelCredentials()) - .isInstanceOf(io.grpc.ChannelCredentials.class); - assertThat(config.googleGrpc().hashedChannelCredentials().hash()) - .isEqualTo(xdsCredsAny.hashCode()); - } - - @Test - public void fromProto_tlsCredentials_notSupported() { - Any tlsCreds = Any - .pack(io.envoyproxy.envoy.extensions.grpc_service.channel_credentials.tls.v3.TlsCredentials - .getDefaultInstance()); - Any accessTokenCreds = - Any.pack(AccessTokenCredentials.newBuilder().setToken("test_token").build()); - GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") - .addChannelCredentialsPlugin(tlsCreds).addCallCredentialsPlugin(accessTokenCreds).build(); - GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); - - GrpcServiceParseException exception = assertThrows(GrpcServiceParseException.class, - () -> GrpcServiceConfig.fromProto(grpcService)); - assertThat(exception).hasMessageThat().contains("TlsCredentials are not yet supported."); - } - - @Test - public void fromProto_invalidChannelCredentialsProto() { - // Pack a Duration proto, but try to unpack it as GoogleDefaultCredentials - Any invalidCreds = Any.pack(com.google.protobuf.Duration.getDefaultInstance()); - Any accessTokenCreds = - Any.pack(AccessTokenCredentials.newBuilder().setToken("test_token").build()); - GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") - .addChannelCredentialsPlugin(invalidCreds).addCallCredentialsPlugin(accessTokenCreds) - .build(); - GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); - - GrpcServiceParseException exception = assertThrows(GrpcServiceParseException.class, - () -> GrpcServiceConfig.fromProto(grpcService)); - assertThat(exception).hasMessageThat() - .contains("No valid supported channel_credentials found. Errors: [Unsupported channel " - + "credentials type: type.googleapis.com/google.protobuf.Duration"); - } - - @Test - public void fromProto_invalidCallCredentialsProto() { - // Pack a Duration proto, but try to unpack it as AccessTokenCredentials - Any insecureCreds = Any.pack(InsecureCredentials.getDefaultInstance()); - Any invalidCallCredentials = Any.pack(Duration.getDefaultInstance()); - GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder().setTargetUri("test_uri") - .addChannelCredentialsPlugin(insecureCreds).addCallCredentialsPlugin(invalidCallCredentials) - .build(); - GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); - - GrpcServiceParseException exception = assertThrows(GrpcServiceParseException.class, - () -> GrpcServiceConfig.fromProto(grpcService)); - assertThat(exception).hasMessageThat().contains("Unsupported call credentials type:"); - } -} - diff --git a/xds/src/test/java/io/grpc/xds/internal/grpcservice/GrpcServiceXdsContextTestUtil.java b/xds/src/test/java/io/grpc/xds/internal/grpcservice/GrpcServiceXdsContextTestUtil.java new file mode 100644 index 00000000000..efcbce0c8cf --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/internal/grpcservice/GrpcServiceXdsContextTestUtil.java @@ -0,0 +1,30 @@ +/* + * 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.grpcservice; + +import java.util.Optional; + +/** + * Utility for creating dummy contexts/providers in tests. + */ +public final class GrpcServiceXdsContextTestUtil { + private GrpcServiceXdsContextTestUtil() {} + + public static GrpcServiceXdsContextProvider dummyProvider() { + return targetUri -> GrpcServiceXdsContext.create(true, Optional.empty(), true); + } +} diff --git a/xds/src/test/java/io/grpc/xds/internal/grpcservice/HeaderValueTest.java b/xds/src/test/java/io/grpc/xds/internal/grpcservice/HeaderValueTest.java new file mode 100644 index 00000000000..b55e6ae76f7 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/internal/grpcservice/HeaderValueTest.java @@ -0,0 +1,49 @@ +/* + * 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.grpcservice; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.protobuf.ByteString; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class HeaderValueTest { + + @Test + public void create_withStringValue_success() { + HeaderValue headerValue = HeaderValue.create("key1", "value1"); + assertThat(headerValue.key()).isEqualTo("key1"); + assertThat(headerValue.value().isPresent()).isTrue(); + assertThat(headerValue.value().get()).isEqualTo("value1"); + assertThat(headerValue.rawValue().isPresent()).isFalse(); + } + + @Test + public void create_withByteStringValue_success() { + ByteString rawValue = ByteString.copyFromUtf8("raw_value"); + HeaderValue headerValue = HeaderValue.create("key2", rawValue); + assertThat(headerValue.key()).isEqualTo("key2"); + assertThat(headerValue.rawValue().isPresent()).isTrue(); + assertThat(headerValue.rawValue().get()).isEqualTo(rawValue); + assertThat(headerValue.value().isPresent()).isFalse(); + } + + +} diff --git a/xds/src/test/java/io/grpc/xds/internal/grpcservice/InsecureGrpcChannelFactoryTest.java b/xds/src/test/java/io/grpc/xds/internal/grpcservice/InsecureGrpcChannelFactoryTest.java deleted file mode 100644 index 8d7347f56c6..00000000000 --- a/xds/src/test/java/io/grpc/xds/internal/grpcservice/InsecureGrpcChannelFactoryTest.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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.grpcservice; - -import static org.junit.Assert.assertNotNull; - -import io.grpc.CallCredentials; -import io.grpc.InsecureChannelCredentials; -import io.grpc.ManagedChannel; -import io.grpc.Metadata; -import io.grpc.xds.internal.grpcservice.GrpcServiceConfig.GoogleGrpcConfig; -import io.grpc.xds.internal.grpcservice.GrpcServiceConfig.HashedChannelCredentials; -import java.util.concurrent.Executor; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** Unit tests for {@link InsecureGrpcChannelFactory}. */ -@RunWith(JUnit4.class) -public class InsecureGrpcChannelFactoryTest { - - private static final class NoOpCallCredentials extends CallCredentials { - @Override - public void applyRequestMetadata(RequestInfo requestInfo, Executor appExecutor, - MetadataApplier applier) { - applier.apply(new Metadata()); - } - } - - @Test - public void testCreateChannel() { - InsecureGrpcChannelFactory factory = InsecureGrpcChannelFactory.getInstance(); - GrpcServiceConfig config = GrpcServiceConfig.builder() - .googleGrpc(GoogleGrpcConfig.builder().target("localhost:8080") - .hashedChannelCredentials( - HashedChannelCredentials.of(InsecureChannelCredentials.create(), 0)) - .callCredentials(new NoOpCallCredentials()).build()) - .build(); - ManagedChannel channel = factory.createChannel(config); - assertNotNull(channel); - channel.shutdownNow(); - } -} diff --git a/xds/src/test/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesConfigTest.java b/xds/src/test/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesConfigTest.java index e2bda9cb836..9f5cb75460f 100644 --- a/xds/src/test/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesConfigTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesConfigTest.java @@ -20,7 +20,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import java.util.regex.Pattern; +import com.google.re2j.Pattern; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; diff --git a/xds/src/test/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesParserTest.java b/xds/src/test/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesParserTest.java new file mode 100644 index 00000000000..c572d5e80fc --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesParserTest.java @@ -0,0 +1,90 @@ +/* + * 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.headermutations; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.protobuf.BoolValue; +import io.envoyproxy.envoy.config.common.mutation_rules.v3.HeaderMutationRules; +import io.envoyproxy.envoy.type.matcher.v3.RegexMatcher; +import io.grpc.xds.internal.extauthz.ExtAuthzParseException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class HeaderMutationRulesParserTest { + + @Test + public void parse_protoWithAllFields_success() throws Exception { + HeaderMutationRules proto = HeaderMutationRules.newBuilder() + .setAllowExpression(RegexMatcher.newBuilder().setRegex("allow-.*")) + .setDisallowExpression(RegexMatcher.newBuilder().setRegex("disallow-.*")) + .setDisallowAll(BoolValue.newBuilder().setValue(true).build()) + .setDisallowIsError(BoolValue.newBuilder().setValue(true).build()) + .build(); + + HeaderMutationRulesConfig config = HeaderMutationRulesParser.parse(proto); + + assertThat(config.allowExpression().isPresent()).isTrue(); + assertThat(config.allowExpression().get().pattern()).isEqualTo("allow-.*"); + + assertThat(config.disallowExpression().isPresent()).isTrue(); + assertThat(config.disallowExpression().get().pattern()).isEqualTo("disallow-.*"); + + assertThat(config.disallowAll()).isTrue(); + assertThat(config.disallowIsError()).isTrue(); + } + + @Test + public void parse_protoWithNoExpressions_success() throws Exception { + HeaderMutationRules proto = HeaderMutationRules.newBuilder().build(); + + HeaderMutationRulesConfig config = HeaderMutationRulesParser.parse(proto); + + assertThat(config.allowExpression().isPresent()).isFalse(); + assertThat(config.disallowExpression().isPresent()).isFalse(); + assertThat(config.disallowAll()).isFalse(); + assertThat(config.disallowIsError()).isFalse(); + } + + @Test + public void parse_invalidRegexAllowExpression_throwsExtAuthzParseException() { + HeaderMutationRules proto = HeaderMutationRules.newBuilder() + .setAllowExpression(RegexMatcher.newBuilder().setRegex("allow-[")) + .build(); + + ExtAuthzParseException exception = assertThrows( + ExtAuthzParseException.class, () -> HeaderMutationRulesParser.parse(proto)); + + assertThat(exception).hasMessageThat().contains("Invalid regex pattern for allow_expression"); + } + + @Test + public void parse_invalidRegexDisallowExpression_throwsExtAuthzParseException() { + HeaderMutationRules proto = HeaderMutationRules.newBuilder() + .setDisallowExpression(RegexMatcher.newBuilder().setRegex("disallow-[")) + .build(); + + ExtAuthzParseException exception = assertThrows( + ExtAuthzParseException.class, () -> HeaderMutationRulesParser.parse(proto)); + + assertThat(exception).hasMessageThat() + .contains("Invalid regex pattern for disallow_expression"); + } +} From 5ed699362a475b76706269564fe593355ecd84f9 Mon Sep 17 00:00:00 2001 From: Saurav Date: Thu, 12 Mar 2026 13:59:19 +0000 Subject: [PATCH 03/10] Fixup: 12492 Split HeaderValueValidationUtils to GrpcService to match the updated requirements --- .../grpc/xds/internal/XdsHeaderValidator.java | 40 --------- .../grpcservice/GrpcServiceConfigParser.java | 16 ++-- .../HeaderValueValidationUtils.java | 67 ++++++++++++++ .../xds/internal/XdsHeaderValidatorTest.java | 64 -------------- .../HeaderValueValidationUtilsTest.java | 87 +++++++++++++++++++ 5 files changed, 161 insertions(+), 113 deletions(-) delete mode 100644 xds/src/main/java/io/grpc/xds/internal/XdsHeaderValidator.java create mode 100644 xds/src/main/java/io/grpc/xds/internal/grpcservice/HeaderValueValidationUtils.java delete mode 100644 xds/src/test/java/io/grpc/xds/internal/XdsHeaderValidatorTest.java create mode 100644 xds/src/test/java/io/grpc/xds/internal/grpcservice/HeaderValueValidationUtilsTest.java diff --git a/xds/src/main/java/io/grpc/xds/internal/XdsHeaderValidator.java b/xds/src/main/java/io/grpc/xds/internal/XdsHeaderValidator.java deleted file mode 100644 index dbd459b017b..00000000000 --- a/xds/src/main/java/io/grpc/xds/internal/XdsHeaderValidator.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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; - -/** - * Utility for validating header keys and values against xDS and Envoy specifications. - */ -public final class XdsHeaderValidator { - - private XdsHeaderValidator() {} - - /** - * Returns whether the header parameter is valid. The length to check is either the - * length of the string value or the size of the binary raw value. - */ - public static boolean isValid(String key, int valueLength) { - if (key.isEmpty() || !key.equals(key.toLowerCase(java.util.Locale.ROOT)) || key.length() > 16384 - || key.equals("host") || key.startsWith(":")) { - return false; - } - if (valueLength > 16384) { - return false; - } - return true; - } -} diff --git a/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParser.java b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParser.java index 7614484f396..a4616893ae4 100644 --- a/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParser.java +++ b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParser.java @@ -33,7 +33,6 @@ import io.grpc.alts.GoogleDefaultChannelCredentials; import io.grpc.auth.MoreCallCredentials; import io.grpc.xds.XdsChannelCredentials; -import io.grpc.xds.internal.XdsHeaderValidator; import java.time.Duration; import java.util.ArrayList; import java.util.Date; @@ -88,17 +87,16 @@ public static GrpcServiceConfig parse(GrpcService grpcServiceProto, for (io.envoyproxy.envoy.config.core.v3.HeaderValue header : grpcServiceProto .getInitialMetadataList()) { String key = header.getKey(); + HeaderValue headerValue; if (key.endsWith(Metadata.BINARY_HEADER_SUFFIX)) { - if (!XdsHeaderValidator.isValid(key, header.getRawValue().size())) { - throw new GrpcServiceParseException("Invalid initial metadata header: " + key); - } - initialMetadata.add(HeaderValue.create(key, header.getRawValue())); + headerValue = HeaderValue.create(key, header.getRawValue()); } else { - if (!XdsHeaderValidator.isValid(key, header.getValue().length())) { - throw new GrpcServiceParseException("Invalid initial metadata header: " + key); - } - initialMetadata.add(HeaderValue.create(key, header.getValue())); + headerValue = HeaderValue.create(key, header.getValue()); + } + if (HeaderValueValidationUtils.shouldIgnore(headerValue)) { + throw new GrpcServiceParseException("Invalid initial metadata header: " + key); } + initialMetadata.add(headerValue); } builder.initialMetadata(initialMetadata.build()); diff --git a/xds/src/main/java/io/grpc/xds/internal/grpcservice/HeaderValueValidationUtils.java b/xds/src/main/java/io/grpc/xds/internal/grpcservice/HeaderValueValidationUtils.java new file mode 100644 index 00000000000..5e1eff04792 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/grpcservice/HeaderValueValidationUtils.java @@ -0,0 +1,67 @@ +/* + * 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.grpcservice; + +import java.util.Locale; + +/** + * Utility class for validating HTTP headers. + */ +public final class HeaderValueValidationUtils { + public static final int MAX_HEADER_LENGTH = 16384; + + private HeaderValueValidationUtils() {} + + /** + * Returns true if the header key should be ignored for mutations or validation. + * + * @param key The header key (e.g., "content-type") + */ + public static boolean shouldIgnore(String key) { + if (key.isEmpty() || key.length() > MAX_HEADER_LENGTH) { + return true; + } + if (!key.equals(key.toLowerCase(Locale.ROOT))) { + return true; + } + if (key.startsWith("grpc-")) { + return true; + } + if (key.startsWith(":") || key.equals("host")) { + return true; + } + return false; + } + + /** + * Returns true if the header value should be ignored. + * + * @param header The HeaderValue containing key and values + */ + public static boolean shouldIgnore(HeaderValue header) { + if (shouldIgnore(header.key())) { + return true; + } + if (header.value().isPresent() && header.value().get().length() > MAX_HEADER_LENGTH) { + return true; + } + if (header.rawValue().isPresent() && header.rawValue().get().size() > MAX_HEADER_LENGTH) { + return true; + } + return false; + } +} diff --git a/xds/src/test/java/io/grpc/xds/internal/XdsHeaderValidatorTest.java b/xds/src/test/java/io/grpc/xds/internal/XdsHeaderValidatorTest.java deleted file mode 100644 index c6c99c6d46f..00000000000 --- a/xds/src/test/java/io/grpc/xds/internal/XdsHeaderValidatorTest.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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.truth.Truth.assertThat; - -import com.google.common.base.Strings; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class XdsHeaderValidatorTest { - - @Test - public void isValid_validKeyAndLength_returnsTrue() { - assertThat(XdsHeaderValidator.isValid("valid-key", 10)).isTrue(); - } - - @Test - public void isValid_emptyKey_returnsFalse() { - assertThat(XdsHeaderValidator.isValid("", 10)).isFalse(); - } - - @Test - public void isValid_uppercaseKey_returnsFalse() { - assertThat(XdsHeaderValidator.isValid("Invalid-Key", 10)).isFalse(); - } - - @Test - public void isValid_keyExceedsMaxLength_returnsFalse() { - String longKey = Strings.repeat("k", 16385); - assertThat(XdsHeaderValidator.isValid(longKey, 10)).isFalse(); - } - - @Test - public void isValid_valueExceedsMaxLength_returnsFalse() { - assertThat(XdsHeaderValidator.isValid("valid-key", 16385)).isFalse(); - } - - @Test - public void isValid_hostKey_returnsFalse() { - assertThat(XdsHeaderValidator.isValid("host", 10)).isFalse(); - } - - @Test - public void isValid_pseudoHeaderKey_returnsFalse() { - assertThat(XdsHeaderValidator.isValid(":method", 10)).isFalse(); - } -} diff --git a/xds/src/test/java/io/grpc/xds/internal/grpcservice/HeaderValueValidationUtilsTest.java b/xds/src/test/java/io/grpc/xds/internal/grpcservice/HeaderValueValidationUtilsTest.java new file mode 100644 index 00000000000..993abfdc545 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/internal/grpcservice/HeaderValueValidationUtilsTest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2026 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.grpcservice; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.protobuf.ByteString; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Unit tests for {@link HeaderValueValidationUtils}. + */ +@RunWith(JUnit4.class) +public class HeaderValueValidationUtilsTest { + + @Test + public void shouldIgnore_string_emptyKey() { + assertThat(HeaderValueValidationUtils.shouldIgnore("")).isTrue(); + } + + @Test + public void shouldIgnore_string_tooLongKey() { + String longKey = new String(new char[16385]).replace('\0', 'a'); + assertThat(HeaderValueValidationUtils.shouldIgnore(longKey)).isTrue(); + } + + @Test + public void shouldIgnore_string_notLowercase() { + assertThat(HeaderValueValidationUtils.shouldIgnore("Content-Type")).isTrue(); + } + + @Test + public void shouldIgnore_string_grpcPrefix() { + assertThat(HeaderValueValidationUtils.shouldIgnore("grpc-timeout")).isTrue(); + } + + @Test + public void shouldIgnore_string_systemHeader_colon() { + assertThat(HeaderValueValidationUtils.shouldIgnore(":authority")).isTrue(); + } + + @Test + public void shouldIgnore_string_systemHeader_host() { + assertThat(HeaderValueValidationUtils.shouldIgnore("host")).isTrue(); + } + + @Test + public void shouldIgnore_string_valid() { + assertThat(HeaderValueValidationUtils.shouldIgnore("content-type")).isFalse(); + } + + @Test + public void shouldIgnore_headerValue_tooLongValue() { + String longValue = new String(new char[16385]).replace('\0', 'v'); + HeaderValue header = HeaderValue.create("content-type", longValue); + assertThat(HeaderValueValidationUtils.shouldIgnore(header)).isTrue(); + } + + @Test + public void shouldIgnore_headerValue_tooLongRawValue() { + ByteString longRawValue = ByteString.copyFrom(new byte[16385]); + HeaderValue header = HeaderValue.create("content-type", longRawValue); + assertThat(HeaderValueValidationUtils.shouldIgnore(header)).isTrue(); + } + + @Test + public void shouldIgnore_headerValue_valid() { + HeaderValue header = HeaderValue.create("content-type", "application/grpc"); + assertThat(HeaderValueValidationUtils.shouldIgnore(header)).isFalse(); + } +} From 35f395632324c6baf3129d3d3831043f0edfdaa7 Mon Sep 17 00:00:00 2001 From: Saurav Date: Sun, 15 Mar 2026 21:35:57 +0000 Subject: [PATCH 04/10] Fixup: 12492 Remove artifact from old channelcreds implementation and address nits. --- .../grpcservice/GrpcServiceConfigParser.java | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParser.java b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParser.java index a4616893ae4..aaafde5c24c 100644 --- a/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParser.java +++ b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParser.java @@ -148,18 +148,8 @@ public static GrpcServiceConfig.GoogleGrpcConfig parseGoogleGrpcConfig( return builder.build(); } - ConfiguredChannelCredentials channelCreds = null; - if (googleGrpcProto.getChannelCredentialsPluginCount() > 0) { - try { - channelCreds = extractChannelCredentials(googleGrpcProto.getChannelCredentialsPluginList()); - } catch (GrpcServiceParseException e) { - // Fall back to channel_credentials if plugins are not supported - } - } - - if (channelCreds == null) { - throw new GrpcServiceParseException("No valid supported channel_credentials found"); - } + ConfiguredChannelCredentials channelCreds = + extractChannelCredentials(googleGrpcProto.getChannelCredentialsPluginList()); Optional callCreds = extractCallCredentials(googleGrpcProto.getCallCredentialsPluginList()); @@ -277,8 +267,6 @@ public void applyRequestMetadata(RequestInfo requestInfo, Executor appExecutor, } } - - static final class ProtoChannelCredsConfig implements ChannelCredsConfig { private final String type; private final Any configProto; From 1d4e48b8ab4fd828f43ae92168f2174cac4ff84a Mon Sep 17 00:00:00 2001 From: Saurav Date: Tue, 17 Mar 2026 06:50:48 +0000 Subject: [PATCH 05/10] Fixup 12492: Use builder instead of newBuilder --- .../main/java/io/grpc/xds/internal/extauthz/ExtAuthzConfig.java | 2 +- .../io/grpc/xds/internal/extauthz/ExtAuthzConfigParser.java | 2 +- .../io/grpc/xds/internal/grpcservice/GrpcServiceConfig.java | 2 +- .../grpc/xds/internal/grpcservice/GrpcServiceConfigParser.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/internal/extauthz/ExtAuthzConfig.java b/xds/src/main/java/io/grpc/xds/internal/extauthz/ExtAuthzConfig.java index fec8e605d73..5aeb44c6e2a 100644 --- a/xds/src/main/java/io/grpc/xds/internal/extauthz/ExtAuthzConfig.java +++ b/xds/src/main/java/io/grpc/xds/internal/extauthz/ExtAuthzConfig.java @@ -35,7 +35,7 @@ public abstract class ExtAuthzConfig { /** Creates a new builder for creating {@link ExtAuthzConfig} instances. */ - public static Builder newBuilder() { + public static Builder builder() { return new AutoValue_ExtAuthzConfig.Builder().allowedHeaders(ImmutableList.of()) .disallowedHeaders(ImmutableList.of()).statusOnError(Status.PERMISSION_DENIED) .filterEnabled(Matchers.FractionMatcher.create(100, 100)); diff --git a/xds/src/main/java/io/grpc/xds/internal/extauthz/ExtAuthzConfigParser.java b/xds/src/main/java/io/grpc/xds/internal/extauthz/ExtAuthzConfigParser.java index 4e17763ae12..04962e49aa7 100644 --- a/xds/src/main/java/io/grpc/xds/internal/extauthz/ExtAuthzConfigParser.java +++ b/xds/src/main/java/io/grpc/xds/internal/extauthz/ExtAuthzConfigParser.java @@ -56,7 +56,7 @@ public static ExtAuthzConfig parse( } catch (GrpcServiceParseException e) { throw new ExtAuthzParseException("Failed to parse GrpcService config: " + e.getMessage(), e); } - ExtAuthzConfig.Builder builder = ExtAuthzConfig.newBuilder().grpcService(grpcServiceConfig) + ExtAuthzConfig.Builder builder = ExtAuthzConfig.builder().grpcService(grpcServiceConfig) .failureModeAllow(extAuthzProto.getFailureModeAllow()) .failureModeAllowHeaderAdd(extAuthzProto.getFailureModeAllowHeaderAdd()) .includePeerCertificate(extAuthzProto.getIncludePeerCertificate()) diff --git a/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfig.java b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfig.java index ba0a9808025..57df9aa0f10 100644 --- a/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfig.java +++ b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfig.java @@ -31,7 +31,7 @@ @AutoValue public abstract class GrpcServiceConfig { - public static Builder newBuilder() { + public static Builder builder() { return new AutoValue_GrpcServiceConfig.Builder(); } diff --git a/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParser.java b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParser.java index aaafde5c24c..208fffc9bae 100644 --- a/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParser.java +++ b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParser.java @@ -81,7 +81,7 @@ public static GrpcServiceConfig parse(GrpcService grpcServiceProto, GrpcServiceConfig.GoogleGrpcConfig googleGrpcConfig = parseGoogleGrpcConfig(grpcServiceProto.getGoogleGrpc(), contextProvider); - GrpcServiceConfig.Builder builder = GrpcServiceConfig.newBuilder().googleGrpc(googleGrpcConfig); + GrpcServiceConfig.Builder builder = GrpcServiceConfig.builder().googleGrpc(googleGrpcConfig); ImmutableList.Builder initialMetadata = ImmutableList.builder(); for (io.envoyproxy.envoy.config.core.v3.HeaderValue header : grpcServiceProto From c1b95f1fbc66ec576a4734d350f7199f0ad50188 Mon Sep 17 00:00:00 2001 From: Saurav Date: Tue, 17 Mar 2026 08:04:31 +0000 Subject: [PATCH 06/10] Fixup: 12492 Fix callcredentials to not apply instead of erroring out and add test coverage --- .../grpcservice/GrpcServiceConfigParser.java | 10 +- .../GrpcServiceConfigParserTest.java | 137 ++++++++++++++++++ 2 files changed, 141 insertions(+), 6 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParser.java b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParser.java index 208fffc9bae..49b8c0a9365 100644 --- a/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParser.java +++ b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParser.java @@ -29,7 +29,6 @@ import io.grpc.InsecureChannelCredentials; import io.grpc.Metadata; import io.grpc.SecurityLevel; -import io.grpc.Status; import io.grpc.alts.GoogleDefaultChannelCredentials; import io.grpc.auth.MoreCallCredentials; import io.grpc.xds.XdsChannelCredentials; @@ -258,12 +257,11 @@ private static final class SecurityAwareAccessTokenCredentials extends CallCrede @Override public void applyRequestMetadata(RequestInfo requestInfo, Executor appExecutor, MetadataApplier applier) { - if (requestInfo.getSecurityLevel() != SecurityLevel.PRIVACY_AND_INTEGRITY) { - applier.fail(Status.UNAUTHENTICATED.withDescription( - "OAuth2 credentials require connection with PRIVACY_AND_INTEGRITY security level")); - return; + if (requestInfo.getSecurityLevel() == SecurityLevel.PRIVACY_AND_INTEGRITY) { + delegate.applyRequestMetadata(requestInfo, appExecutor, applier); + } else { + applier.apply(new Metadata()); } - delegate.applyRequestMetadata(requestInfo, appExecutor, applier); } } diff --git a/xds/src/test/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParserTest.java b/xds/src/test/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParserTest.java index 1a7634aadf7..20d129b7d3b 100644 --- a/xds/src/test/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParserTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParserTest.java @@ -387,4 +387,141 @@ public void parseGoogleGrpcConfig_unsupportedScheme() { assertThat(exception).hasMessageThat() .contains("Target URI scheme is not resolvable"); } + + static class RecordingMetadataApplier extends io.grpc.CallCredentials.MetadataApplier { + boolean applied = false; + boolean failed = false; + io.grpc.Metadata appliedHeaders = null; + + @Override + public void apply(io.grpc.Metadata headers) { + applied = true; + appliedHeaders = headers; + } + + @Override + public void fail(io.grpc.Status status) { + failed = true; + } + } + + static class FakeRequestInfo extends io.grpc.CallCredentials.RequestInfo { + private final io.grpc.SecurityLevel securityLevel; + private final io.grpc.MethodDescriptor methodDescriptor; + + FakeRequestInfo(io.grpc.SecurityLevel securityLevel) { + this.securityLevel = securityLevel; + this.methodDescriptor = io.grpc.MethodDescriptor.newBuilder() + .setType(io.grpc.MethodDescriptor.MethodType.UNARY) + .setFullMethodName("test_service/test_method") + .setRequestMarshaller(new NoopMarshaller()) + .setResponseMarshaller(new NoopMarshaller()) + .build(); + } + + private static class NoopMarshaller implements io.grpc.MethodDescriptor.Marshaller { + @Override + public java.io.InputStream stream(T value) { + return null; + } + + @Override + public T parse(java.io.InputStream stream) { + return null; + } + } + + @Override + public io.grpc.MethodDescriptor getMethodDescriptor() { + return methodDescriptor; + } + + @Override + public io.grpc.SecurityLevel getSecurityLevel() { + return securityLevel; + } + + @Override + public String getAuthority() { + return "dummy-authority"; + } + + @Override + public io.grpc.Attributes getTransportAttrs() { + return io.grpc.Attributes.EMPTY; + } + } + + + @Test + public void securityAwareCredentials_secureConnection_appliesToken() throws Exception { + Any insecureCreds = Any.pack(InsecureCredentials.getDefaultInstance()); + Any accessTokenCreds = + Any.pack(AccessTokenCredentials.newBuilder().setToken("test_token").build()); + GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("test_uri") + .addChannelCredentialsPlugin(insecureCreds) + .addCallCredentialsPlugin(accessTokenCreds) + .build(); + GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); + + GrpcServiceConfig config = GrpcServiceConfigParser.parse(grpcService, + io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextTestUtil.dummyProvider()); + + io.grpc.CallCredentials creds = config.googleGrpc().callCredentials().get(); + RecordingMetadataApplier applier = new RecordingMetadataApplier(); + java.util.concurrent.CountDownLatch latch = new java.util.concurrent.CountDownLatch(1); + + creds.applyRequestMetadata( + new FakeRequestInfo(io.grpc.SecurityLevel.PRIVACY_AND_INTEGRITY), + Runnable::run, // Use direct executor to avoid async issues in test + new io.grpc.CallCredentials.MetadataApplier() { + @Override + public void apply(io.grpc.Metadata headers) { + applier.apply(headers); + latch.countDown(); + } + + @Override + public void fail(io.grpc.Status status) { + applier.fail(status); + latch.countDown(); + } + }); + + latch.await(5, java.util.concurrent.TimeUnit.SECONDS); + assertThat(applier.applied).isTrue(); + assertThat(applier.appliedHeaders.get( + io.grpc.Metadata.Key.of("Authorization", io.grpc.Metadata.ASCII_STRING_MARSHALLER))) + .isEqualTo("Bearer test_token"); + } + + @Test + public void securityAwareCredentials_insecureConnection_appliesEmptyMetadata() throws Exception { + Any insecureCreds = Any.pack(InsecureCredentials.getDefaultInstance()); + Any accessTokenCreds = + Any.pack(AccessTokenCredentials.newBuilder().setToken("test_token").build()); + GrpcService.GoogleGrpc googleGrpc = GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("test_uri") + .addChannelCredentialsPlugin(insecureCreds) + .addCallCredentialsPlugin(accessTokenCreds) + .build(); + GrpcService grpcService = GrpcService.newBuilder().setGoogleGrpc(googleGrpc).build(); + + GrpcServiceConfig config = GrpcServiceConfigParser.parse(grpcService, + io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextTestUtil.dummyProvider()); + + io.grpc.CallCredentials creds = config.googleGrpc().callCredentials().get(); + RecordingMetadataApplier applier = new RecordingMetadataApplier(); + + creds.applyRequestMetadata( + new FakeRequestInfo(io.grpc.SecurityLevel.NONE), + Runnable::run, + applier); + + assertThat(applier.applied).isTrue(); + assertThat(applier.appliedHeaders.get( + io.grpc.Metadata.Key.of("Authorization", io.grpc.Metadata.ASCII_STRING_MARSHALLER))) + .isNull(); + } } From b28bd3cc060527c700e02ffaad6e36b52eb0dbef Mon Sep 17 00:00:00 2001 From: Saurav Date: Thu, 19 Mar 2026 18:56:29 +0000 Subject: [PATCH 07/10] Fixup 12492: Refactor `allowedGrpcService` to be non optional and fix bug Makes `allowedGrpcServices` to be a non-optional struct instead of an `Optional>` since it's essentially an immuatable hash map, making it preferable to use an empty instance instead of null. Change a small bug where we continued instead of return when parsing bootstrap credentials. --- .../io/grpc/xds/GrpcBootstrapperImpl.java | 20 +++++---- .../java/io/grpc/xds/client/Bootstrapper.java | 9 ++-- .../io/grpc/xds/client/BootstrapperImpl.java | 8 ++-- .../grpcservice/AllowedGrpcService.java | 44 +++++++++++++++++++ .../grpcservice/AllowedGrpcServices.java | 37 ++++++++++++++++ .../grpcservice/GrpcServiceConfigParser.java | 2 +- .../grpcservice/GrpcServiceXdsContext.java | 24 ---------- .../io/grpc/xds/GrpcBootstrapperImplTest.java | 12 +++-- .../GrpcServiceConfigParserTest.java | 1 - 9 files changed, 107 insertions(+), 50 deletions(-) create mode 100644 xds/src/main/java/io/grpc/xds/internal/grpcservice/AllowedGrpcService.java create mode 100644 xds/src/main/java/io/grpc/xds/internal/grpcservice/AllowedGrpcServices.java diff --git a/xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java b/xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java index 9420a87191d..5f9065875bb 100644 --- a/xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java +++ b/xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java @@ -25,9 +25,10 @@ import io.grpc.xds.client.BootstrapperImpl; import io.grpc.xds.client.XdsInitializationException; import io.grpc.xds.client.XdsLogger; +import io.grpc.xds.internal.grpcservice.AllowedGrpcService; +import io.grpc.xds.internal.grpcservice.AllowedGrpcServices; import io.grpc.xds.internal.grpcservice.ChannelCredsConfig; import io.grpc.xds.internal.grpcservice.ConfiguredChannelCredentials; -import io.grpc.xds.internal.grpcservice.GrpcServiceXdsContext; import java.io.IOException; import java.util.List; import java.util.Map; @@ -163,7 +164,7 @@ private static ConfiguredChannelCredentials parseChannelCredentials(List parseAllowedGrpcServices( + protected Object parseAllowedGrpcServices( Map rawAllowedGrpcServices) throws XdsInitializationException { - ImmutableMap.Builder builder = + if (rawAllowedGrpcServices == null || rawAllowedGrpcServices.isEmpty()) { + return AllowedGrpcServices.empty(); + } + + ImmutableMap.Builder builder = ImmutableMap.builder(); for (String targetUri : rawAllowedGrpcServices.keySet()) { Map serviceConfig = JsonUtil.getObject(rawAllowedGrpcServices, targetUri); @@ -193,13 +198,12 @@ protected Optional parseAllowedGrpcServices( parseCallCredentials(JsonUtil.checkObjectList(rawCallCredsList), targetUri); } - GrpcServiceXdsContext.AllowedGrpcService.Builder b = GrpcServiceXdsContext.AllowedGrpcService - .builder().configuredChannelCredentials(configuredChannel); + AllowedGrpcService.Builder b = AllowedGrpcService.builder() + .configuredChannelCredentials(configuredChannel); callCredentials.ifPresent(b::callCredentials); builder.put(targetUri, b.build()); } - ImmutableMap parsed = builder.buildOrThrow(); - return parsed.isEmpty() ? Optional.empty() : Optional.of(parsed); + return AllowedGrpcServices.create(builder.build()); } @SuppressWarnings("unused") 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 32f4216d0cd..56e1de7f93c 100644 --- a/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java +++ b/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java @@ -24,9 +24,9 @@ import com.google.common.collect.ImmutableMap; import io.grpc.Internal; import io.grpc.xds.client.EnvoyProtoData.Node; +import io.grpc.xds.internal.grpcservice.AllowedGrpcServices; import java.util.List; import java.util.Map; -import java.util.Optional; import javax.annotation.Nullable; /** @@ -210,13 +210,14 @@ public abstract static class BootstrapInfo { * Parsed allowed_grpc_services configuration. * Returns an opaque object containing the parsed configuration. */ - public abstract Optional allowedGrpcServices(); + public abstract Object allowedGrpcServices(); @VisibleForTesting public static Builder builder() { return new AutoValue_Bootstrapper_BootstrapInfo.Builder() .clientDefaultListenerResourceNameTemplate("%s") - .authorities(ImmutableMap.of()); + .authorities(ImmutableMap.of()) + .allowedGrpcServices(AllowedGrpcServices.empty()); } @AutoValue.Builder @@ -238,7 +239,7 @@ public abstract Builder clientDefaultListenerResourceNameTemplate( public abstract Builder authorities(Map authorities); - public abstract Builder allowedGrpcServices(Optional allowedGrpcServices); + public abstract Builder allowedGrpcServices(Object allowedGrpcServices); public abstract BootstrapInfo build(); } 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 e267a9cb985..548fcda520b 100644 --- a/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java +++ b/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java @@ -240,17 +240,15 @@ protected BootstrapInfo.Builder bootstrapBuilder(Map rawData) } Map rawAllowedGrpcServices = JsonUtil.getObject(rawData, "allowed_grpc_services"); - if (rawAllowedGrpcServices != null) { - builder.allowedGrpcServices(parseAllowedGrpcServices(rawAllowedGrpcServices)); - } + builder.allowedGrpcServices(parseAllowedGrpcServices(rawAllowedGrpcServices)); return builder; } - protected java.util.Optional parseAllowedGrpcServices( + protected Object parseAllowedGrpcServices( Map rawAllowedGrpcServices) throws XdsInitializationException { - return java.util.Optional.empty(); + return io.grpc.xds.internal.grpcservice.AllowedGrpcServices.empty(); } private List parseServerInfos(List rawServerConfigs, XdsLogger logger) diff --git a/xds/src/main/java/io/grpc/xds/internal/grpcservice/AllowedGrpcService.java b/xds/src/main/java/io/grpc/xds/internal/grpcservice/AllowedGrpcService.java new file mode 100644 index 00000000000..ca2f548ed4d --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/grpcservice/AllowedGrpcService.java @@ -0,0 +1,44 @@ +/* + * Copyright 2026 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.grpcservice; + +import com.google.auto.value.AutoValue; +import io.grpc.CallCredentials; +import java.util.Optional; + +/** + * Represents an allowed gRPC service configuration with local credentials. + */ +@AutoValue +public abstract class AllowedGrpcService { + public abstract ConfiguredChannelCredentials configuredChannelCredentials(); + + public abstract Optional callCredentials(); + + public static Builder builder() { + return new AutoValue_AllowedGrpcService.Builder(); + } + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder configuredChannelCredentials(ConfiguredChannelCredentials credentials); + + public abstract Builder callCredentials(CallCredentials callCredentials); + + public abstract AllowedGrpcService build(); + } +} diff --git a/xds/src/main/java/io/grpc/xds/internal/grpcservice/AllowedGrpcServices.java b/xds/src/main/java/io/grpc/xds/internal/grpcservice/AllowedGrpcServices.java new file mode 100644 index 00000000000..71213305888 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/grpcservice/AllowedGrpcServices.java @@ -0,0 +1,37 @@ +/* + * Copyright 2026 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.grpcservice; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableMap; +import java.util.Map; + +/** + * Wrapper for allowed gRPC services keyed by target URI. + */ +@AutoValue +public abstract class AllowedGrpcServices { + public abstract ImmutableMap services(); + + public static AllowedGrpcServices create(Map services) { + return new AutoValue_AllowedGrpcServices(ImmutableMap.copyOf(services)); + } + + public static AllowedGrpcServices empty() { + return create(ImmutableMap.of()); + } +} diff --git a/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParser.java b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParser.java index 49b8c0a9365..b4681e063b3 100644 --- a/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParser.java +++ b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParser.java @@ -130,7 +130,7 @@ public static GrpcServiceConfig.GoogleGrpcConfig parseGoogleGrpcConfig( } if (!context.isTrustedControlPlane()) { - Optional override = + Optional override = context.validAllowedGrpcService(); if (!override.isPresent()) { throw new GrpcServiceParseException( diff --git a/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceXdsContext.java b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceXdsContext.java index 77ae8cffe03..424d18fc34a 100644 --- a/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceXdsContext.java +++ b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceXdsContext.java @@ -17,7 +17,6 @@ package io.grpc.xds.internal.grpcservice; import com.google.auto.value.AutoValue; -import io.grpc.CallCredentials; import io.grpc.Internal; import java.util.Optional; @@ -45,27 +44,4 @@ public static GrpcServiceXdsContext create( isTargetUriSchemeSupported); } - /** - * Represents an allowed gRPC service configuration with local credentials. - */ - @AutoValue - public abstract static class AllowedGrpcService { - public abstract ConfiguredChannelCredentials configuredChannelCredentials(); - - public abstract Optional callCredentials(); - - public static Builder builder() { - return new AutoValue_GrpcServiceXdsContext_AllowedGrpcService.Builder(); - } - - @AutoValue.Builder - public abstract static class Builder { - public abstract Builder configuredChannelCredentials( - ConfiguredChannelCredentials credentials); - - public abstract Builder callCredentials(CallCredentials callCredentials); - - public abstract AllowedGrpcService build(); - } - } } diff --git a/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java b/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java index b72658a9bf6..8b9461861a9 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java @@ -37,7 +37,8 @@ import io.grpc.xds.client.EnvoyProtoData.Node; import io.grpc.xds.client.Locality; import io.grpc.xds.client.XdsInitializationException; -import io.grpc.xds.internal.grpcservice.GrpcServiceXdsContext.AllowedGrpcService; +import io.grpc.xds.internal.grpcservice.AllowedGrpcService; +import io.grpc.xds.internal.grpcservice.AllowedGrpcServices; import java.io.IOException; import java.util.List; import java.util.Map; @@ -117,13 +118,10 @@ public void parseBootstrap_allowedGrpcServices() throws XdsInitializationExcepti bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData)); BootstrapInfo info = bootstrapper.bootstrap(); - @SuppressWarnings("unchecked") - Map allowed = - (Map) info.allowedGrpcServices().get(); - + AllowedGrpcServices allowed = (AllowedGrpcServices) info.allowedGrpcServices(); assertThat(allowed).isNotNull(); - assertThat(allowed).containsKey("dns:///foo.com:443"); - AllowedGrpcService service = allowed.get("dns:///foo.com:443"); + assertThat(allowed.services()).containsKey("dns:///foo.com:443"); + AllowedGrpcService service = allowed.services().get("dns:///foo.com:443"); assertThat(service.configuredChannelCredentials().channelCredentials()) .isInstanceOf(InsecureChannelCredentials.class); assertThat(service.callCredentials().isPresent()).isFalse(); diff --git a/xds/src/test/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParserTest.java b/xds/src/test/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParserTest.java index 20d129b7d3b..39310a2dc63 100644 --- a/xds/src/test/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParserTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParserTest.java @@ -29,7 +29,6 @@ import io.envoyproxy.envoy.extensions.grpc_service.channel_credentials.local.v3.LocalCredentials; import io.envoyproxy.envoy.extensions.grpc_service.channel_credentials.xds.v3.XdsCredentials; import io.grpc.InsecureChannelCredentials; -import io.grpc.xds.internal.grpcservice.GrpcServiceXdsContext.AllowedGrpcService; import java.nio.charset.StandardCharsets; import org.junit.Test; import org.junit.runner.RunWith; From 779b8edb4faf3ceb85a183fd4b7bfd7eb55d2f67 Mon Sep 17 00:00:00 2001 From: Saurav Date: Wed, 25 Mar 2026 06:16:24 +0000 Subject: [PATCH 08/10] Fixup 12492: Address copilot comments --- .../extauthz/ExtAuthzConfigParser.java | 9 ++++- .../grpcservice/GrpcServiceConfigParser.java | 8 ++-- .../HeaderValueValidationUtils.java | 10 ++--- .../HeaderMutationRulesConfig.java | 2 +- .../HeaderMutationRulesParseException.java | 32 +++++++++++++++ .../HeaderMutationRulesParser.java | 8 ++-- .../HeaderValueValidationUtilsTest.java | 40 +++++++++---------- .../HeaderMutationRulesParserTest.java | 14 +++---- 8 files changed, 80 insertions(+), 43 deletions(-) create mode 100644 xds/src/main/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesParseException.java diff --git a/xds/src/main/java/io/grpc/xds/internal/extauthz/ExtAuthzConfigParser.java b/xds/src/main/java/io/grpc/xds/internal/extauthz/ExtAuthzConfigParser.java index 04962e49aa7..bd0f28aca0e 100644 --- a/xds/src/main/java/io/grpc/xds/internal/extauthz/ExtAuthzConfigParser.java +++ b/xds/src/main/java/io/grpc/xds/internal/extauthz/ExtAuthzConfigParser.java @@ -24,6 +24,7 @@ import io.grpc.xds.internal.grpcservice.GrpcServiceConfigParser; import io.grpc.xds.internal.grpcservice.GrpcServiceParseException; import io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextProvider; +import io.grpc.xds.internal.headermutations.HeaderMutationRulesParseException; import io.grpc.xds.internal.headermutations.HeaderMutationRulesParser; @@ -87,8 +88,12 @@ public static ExtAuthzConfig parse( } if (extAuthzProto.hasDecoderHeaderMutationRules()) { - builder.decoderHeaderMutationRules( - HeaderMutationRulesParser.parse(extAuthzProto.getDecoderHeaderMutationRules())); + try { + builder.decoderHeaderMutationRules( + HeaderMutationRulesParser.parse(extAuthzProto.getDecoderHeaderMutationRules())); + } catch (HeaderMutationRulesParseException e) { + throw new ExtAuthzParseException(e.getMessage(), e); + } } return builder.build(); diff --git a/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParser.java b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParser.java index b4681e063b3..59f48a390da 100644 --- a/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParser.java +++ b/xds/src/main/java/io/grpc/xds/internal/grpcservice/GrpcServiceConfigParser.java @@ -21,6 +21,7 @@ import com.google.common.collect.ImmutableList; import com.google.protobuf.Any; import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.Durations; import io.envoyproxy.envoy.config.core.v3.GrpcService; import io.envoyproxy.envoy.extensions.grpc_service.call_credentials.access_token.v3.AccessTokenCredentials; import io.envoyproxy.envoy.extensions.grpc_service.channel_credentials.xds.v3.XdsCredentials; @@ -92,7 +93,7 @@ public static GrpcServiceConfig parse(GrpcService grpcServiceProto, } else { headerValue = HeaderValue.create(key, header.getValue()); } - if (HeaderValueValidationUtils.shouldIgnore(headerValue)) { + if (HeaderValueValidationUtils.isDisallowed(headerValue)) { throw new GrpcServiceParseException("Invalid initial metadata header: " + key); } initialMetadata.add(headerValue); @@ -101,9 +102,8 @@ public static GrpcServiceConfig parse(GrpcService grpcServiceProto, if (grpcServiceProto.hasTimeout()) { com.google.protobuf.Duration timeout = grpcServiceProto.getTimeout(); - if (timeout.getSeconds() < 0 || timeout.getNanos() < 0 - || (timeout.getSeconds() == 0 && timeout.getNanos() == 0)) { - throw new GrpcServiceParseException("Timeout must be strictly positive"); + if (!Durations.isValid(timeout) || Durations.compare(timeout, Durations.ZERO) <= 0) { + throw new GrpcServiceParseException("Timeout must be strictly positive and valid"); } builder.timeout(Duration.ofSeconds(timeout.getSeconds(), timeout.getNanos())); } diff --git a/xds/src/main/java/io/grpc/xds/internal/grpcservice/HeaderValueValidationUtils.java b/xds/src/main/java/io/grpc/xds/internal/grpcservice/HeaderValueValidationUtils.java index 5e1eff04792..ff0df11bdc5 100644 --- a/xds/src/main/java/io/grpc/xds/internal/grpcservice/HeaderValueValidationUtils.java +++ b/xds/src/main/java/io/grpc/xds/internal/grpcservice/HeaderValueValidationUtils.java @@ -27,11 +27,11 @@ public final class HeaderValueValidationUtils { private HeaderValueValidationUtils() {} /** - * Returns true if the header key should be ignored for mutations or validation. + * Returns true if the header key is disallowed for mutations or validation. * * @param key The header key (e.g., "content-type") */ - public static boolean shouldIgnore(String key) { + public static boolean isDisallowed(String key) { if (key.isEmpty() || key.length() > MAX_HEADER_LENGTH) { return true; } @@ -48,12 +48,12 @@ public static boolean shouldIgnore(String key) { } /** - * Returns true if the header value should be ignored. + * Returns true if the header value is disallowed. * * @param header The HeaderValue containing key and values */ - public static boolean shouldIgnore(HeaderValue header) { - if (shouldIgnore(header.key())) { + public static boolean isDisallowed(HeaderValue header) { + if (isDisallowed(header.key())) { return true; } if (header.value().isPresent() && header.value().get().length() > MAX_HEADER_LENGTH) { diff --git a/xds/src/main/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesConfig.java b/xds/src/main/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesConfig.java index 249a587ce53..b16ec7948ed 100644 --- a/xds/src/main/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesConfig.java +++ b/xds/src/main/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesConfig.java @@ -55,7 +55,7 @@ public static Builder builder() { public abstract boolean disallowAll(); /** - * If true, disallows any header mutation that would result in an invalid header value. + * If true, a disallowed header mutation will result in an error instead of being ignored. * * @see HeaderMutationRules#getDisallowIsError() */ diff --git a/xds/src/main/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesParseException.java b/xds/src/main/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesParseException.java new file mode 100644 index 00000000000..3782e84a54b --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesParseException.java @@ -0,0 +1,32 @@ +/* + * 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.headermutations; + +/** + * Exception thrown when parsing header mutation rules fails. + */ +public final class HeaderMutationRulesParseException extends Exception { + private static final long serialVersionUID = 1L; + + public HeaderMutationRulesParseException(String message) { + super(message); + } + + public HeaderMutationRulesParseException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/xds/src/main/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesParser.java b/xds/src/main/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesParser.java index b00db519d45..f6bb2ec508d 100644 --- a/xds/src/main/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesParser.java +++ b/xds/src/main/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesParser.java @@ -19,7 +19,6 @@ import com.google.re2j.Pattern; import com.google.re2j.PatternSyntaxException; import io.envoyproxy.envoy.config.common.mutation_rules.v3.HeaderMutationRules; -import io.grpc.xds.internal.extauthz.ExtAuthzParseException; /** * Parser for {@link io.envoyproxy.envoy.config.common.mutation_rules.v3.HeaderMutationRules}. @@ -29,7 +28,7 @@ public final class HeaderMutationRulesParser { private HeaderMutationRulesParser() {} public static HeaderMutationRulesConfig parse(HeaderMutationRules proto) - throws ExtAuthzParseException { + throws HeaderMutationRulesParseException { HeaderMutationRulesConfig.Builder builder = HeaderMutationRulesConfig.builder(); builder.disallowAll(proto.getDisallowAll().getValue()); builder.disallowIsError(proto.getDisallowIsError().getValue()); @@ -44,11 +43,12 @@ public static HeaderMutationRulesConfig parse(HeaderMutationRules proto) return builder.build(); } - private static Pattern parseRegex(String regex, String fieldName) throws ExtAuthzParseException { + private static Pattern parseRegex(String regex, String fieldName) + throws HeaderMutationRulesParseException { try { return Pattern.compile(regex); } catch (PatternSyntaxException e) { - throw new ExtAuthzParseException( + throw new HeaderMutationRulesParseException( "Invalid regex pattern for " + fieldName + ": " + e.getMessage(), e); } } diff --git a/xds/src/test/java/io/grpc/xds/internal/grpcservice/HeaderValueValidationUtilsTest.java b/xds/src/test/java/io/grpc/xds/internal/grpcservice/HeaderValueValidationUtilsTest.java index 993abfdc545..c4658f3f305 100644 --- a/xds/src/test/java/io/grpc/xds/internal/grpcservice/HeaderValueValidationUtilsTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/grpcservice/HeaderValueValidationUtilsTest.java @@ -30,58 +30,58 @@ public class HeaderValueValidationUtilsTest { @Test - public void shouldIgnore_string_emptyKey() { - assertThat(HeaderValueValidationUtils.shouldIgnore("")).isTrue(); + public void isDisallowed_string_emptyKey() { + assertThat(HeaderValueValidationUtils.isDisallowed("")).isTrue(); } @Test - public void shouldIgnore_string_tooLongKey() { + public void isDisallowed_string_tooLongKey() { String longKey = new String(new char[16385]).replace('\0', 'a'); - assertThat(HeaderValueValidationUtils.shouldIgnore(longKey)).isTrue(); + assertThat(HeaderValueValidationUtils.isDisallowed(longKey)).isTrue(); } @Test - public void shouldIgnore_string_notLowercase() { - assertThat(HeaderValueValidationUtils.shouldIgnore("Content-Type")).isTrue(); + public void isDisallowed_string_notLowercase() { + assertThat(HeaderValueValidationUtils.isDisallowed("Content-Type")).isTrue(); } @Test - public void shouldIgnore_string_grpcPrefix() { - assertThat(HeaderValueValidationUtils.shouldIgnore("grpc-timeout")).isTrue(); + public void isDisallowed_string_grpcPrefix() { + assertThat(HeaderValueValidationUtils.isDisallowed("grpc-timeout")).isTrue(); } @Test - public void shouldIgnore_string_systemHeader_colon() { - assertThat(HeaderValueValidationUtils.shouldIgnore(":authority")).isTrue(); + public void isDisallowed_string_systemHeader_colon() { + assertThat(HeaderValueValidationUtils.isDisallowed(":authority")).isTrue(); } @Test - public void shouldIgnore_string_systemHeader_host() { - assertThat(HeaderValueValidationUtils.shouldIgnore("host")).isTrue(); + public void isDisallowed_string_systemHeader_host() { + assertThat(HeaderValueValidationUtils.isDisallowed("host")).isTrue(); } @Test - public void shouldIgnore_string_valid() { - assertThat(HeaderValueValidationUtils.shouldIgnore("content-type")).isFalse(); + public void isDisallowed_string_valid() { + assertThat(HeaderValueValidationUtils.isDisallowed("content-type")).isFalse(); } @Test - public void shouldIgnore_headerValue_tooLongValue() { + public void isDisallowed_headerValue_tooLongValue() { String longValue = new String(new char[16385]).replace('\0', 'v'); HeaderValue header = HeaderValue.create("content-type", longValue); - assertThat(HeaderValueValidationUtils.shouldIgnore(header)).isTrue(); + assertThat(HeaderValueValidationUtils.isDisallowed(header)).isTrue(); } @Test - public void shouldIgnore_headerValue_tooLongRawValue() { + public void isDisallowed_headerValue_tooLongRawValue() { ByteString longRawValue = ByteString.copyFrom(new byte[16385]); HeaderValue header = HeaderValue.create("content-type", longRawValue); - assertThat(HeaderValueValidationUtils.shouldIgnore(header)).isTrue(); + assertThat(HeaderValueValidationUtils.isDisallowed(header)).isTrue(); } @Test - public void shouldIgnore_headerValue_valid() { + public void isDisallowed_headerValue_valid() { HeaderValue header = HeaderValue.create("content-type", "application/grpc"); - assertThat(HeaderValueValidationUtils.shouldIgnore(header)).isFalse(); + assertThat(HeaderValueValidationUtils.isDisallowed(header)).isFalse(); } } diff --git a/xds/src/test/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesParserTest.java b/xds/src/test/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesParserTest.java index c572d5e80fc..e880c197450 100644 --- a/xds/src/test/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesParserTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/headermutations/HeaderMutationRulesParserTest.java @@ -22,7 +22,7 @@ import com.google.protobuf.BoolValue; import io.envoyproxy.envoy.config.common.mutation_rules.v3.HeaderMutationRules; import io.envoyproxy.envoy.type.matcher.v3.RegexMatcher; -import io.grpc.xds.internal.extauthz.ExtAuthzParseException; +import io.grpc.xds.internal.headermutations.HeaderMutationRulesParseException; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -64,25 +64,25 @@ public void parse_protoWithNoExpressions_success() throws Exception { } @Test - public void parse_invalidRegexAllowExpression_throwsExtAuthzParseException() { + public void parse_invalidRegexAllowExpression_throwsHeaderMutationRulesParseException() { HeaderMutationRules proto = HeaderMutationRules.newBuilder() .setAllowExpression(RegexMatcher.newBuilder().setRegex("allow-[")) .build(); - ExtAuthzParseException exception = assertThrows( - ExtAuthzParseException.class, () -> HeaderMutationRulesParser.parse(proto)); + HeaderMutationRulesParseException exception = assertThrows( + HeaderMutationRulesParseException.class, () -> HeaderMutationRulesParser.parse(proto)); assertThat(exception).hasMessageThat().contains("Invalid regex pattern for allow_expression"); } @Test - public void parse_invalidRegexDisallowExpression_throwsExtAuthzParseException() { + public void parse_invalidRegexDisallowExpression_throwsHeaderMutationRulesParseException() { HeaderMutationRules proto = HeaderMutationRules.newBuilder() .setDisallowExpression(RegexMatcher.newBuilder().setRegex("disallow-[")) .build(); - ExtAuthzParseException exception = assertThrows( - ExtAuthzParseException.class, () -> HeaderMutationRulesParser.parse(proto)); + HeaderMutationRulesParseException exception = assertThrows( + HeaderMutationRulesParseException.class, () -> HeaderMutationRulesParser.parse(proto)); assertThat(exception).hasMessageThat() .contains("Invalid regex pattern for disallow_expression"); From 804cb09f939e991c6bc6da917e877545aa9b4eef Mon Sep 17 00:00:00 2001 From: Saurav Date: Wed, 25 Mar 2026 12:43:55 +0000 Subject: [PATCH 09/10] Fixup 12492: Eliminate bootstrap dependency on grpc --- xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java | 8 ++++---- xds/src/main/java/io/grpc/xds/client/Bootstrapper.java | 8 ++++---- .../main/java/io/grpc/xds/client/BootstrapperImpl.java | 8 +++++--- .../test/java/io/grpc/xds/GrpcBootstrapperImplTest.java | 2 +- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java b/xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java index 5f9065875bb..3dd3c5c5885 100644 --- a/xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java +++ b/xds/src/main/java/io/grpc/xds/GrpcBootstrapperImpl.java @@ -173,11 +173,11 @@ private static ConfiguredChannelCredentials parseChannelCredentials(List rawAllowedGrpcServices) + protected Optional parseAllowedGrpcServices( + @Nullable Map rawAllowedGrpcServices) throws XdsInitializationException { if (rawAllowedGrpcServices == null || rawAllowedGrpcServices.isEmpty()) { - return AllowedGrpcServices.empty(); + return Optional.of(AllowedGrpcServices.empty()); } ImmutableMap.Builder builder = @@ -203,7 +203,7 @@ protected Object parseAllowedGrpcServices( callCredentials.ifPresent(b::callCredentials); builder.put(targetUri, b.build()); } - return AllowedGrpcServices.create(builder.build()); + return Optional.of(AllowedGrpcServices.create(builder.build())); } @SuppressWarnings("unused") 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 56e1de7f93c..b348b927675 100644 --- a/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java +++ b/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java @@ -24,9 +24,9 @@ import com.google.common.collect.ImmutableMap; import io.grpc.Internal; import io.grpc.xds.client.EnvoyProtoData.Node; -import io.grpc.xds.internal.grpcservice.AllowedGrpcServices; import java.util.List; import java.util.Map; +import java.util.Optional; import javax.annotation.Nullable; /** @@ -210,14 +210,14 @@ public abstract static class BootstrapInfo { * Parsed allowed_grpc_services configuration. * Returns an opaque object containing the parsed configuration. */ - public abstract Object allowedGrpcServices(); + public abstract Optional allowedGrpcServices(); @VisibleForTesting public static Builder builder() { return new AutoValue_Bootstrapper_BootstrapInfo.Builder() .clientDefaultListenerResourceNameTemplate("%s") .authorities(ImmutableMap.of()) - .allowedGrpcServices(AllowedGrpcServices.empty()); + .allowedGrpcServices(Optional.empty()); } @AutoValue.Builder @@ -239,7 +239,7 @@ public abstract Builder clientDefaultListenerResourceNameTemplate( public abstract Builder authorities(Map authorities); - public abstract Builder allowedGrpcServices(Object allowedGrpcServices); + public abstract Builder allowedGrpcServices(Optional allowedGrpcServices); public abstract BootstrapInfo build(); } 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 548fcda520b..37fe5a5ee37 100644 --- a/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java +++ b/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java @@ -34,6 +34,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; +import javax.annotation.Nullable; /** * A {@link Bootstrapper} implementation that reads xDS configurations from local file system. @@ -245,10 +247,10 @@ protected BootstrapInfo.Builder bootstrapBuilder(Map rawData) return builder; } - protected Object parseAllowedGrpcServices( - Map rawAllowedGrpcServices) + protected Optional parseAllowedGrpcServices( + @Nullable Map rawAllowedGrpcServices) throws XdsInitializationException { - return io.grpc.xds.internal.grpcservice.AllowedGrpcServices.empty(); + return Optional.empty(); } private List parseServerInfos(List rawServerConfigs, XdsLogger logger) diff --git a/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java b/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java index 8b9461861a9..aaf424277a4 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java @@ -118,7 +118,7 @@ public void parseBootstrap_allowedGrpcServices() throws XdsInitializationExcepti bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData)); BootstrapInfo info = bootstrapper.bootstrap(); - AllowedGrpcServices allowed = (AllowedGrpcServices) info.allowedGrpcServices(); + AllowedGrpcServices allowed = (AllowedGrpcServices) info.allowedGrpcServices().get(); assertThat(allowed).isNotNull(); assertThat(allowed.services()).containsKey("dns:///foo.com:443"); AllowedGrpcService service = allowed.services().get("dns:///foo.com:443"); From 1dcec9af9cf41f891115f172e51f0bbf8c183d47 Mon Sep 17 00:00:00 2001 From: Saurav Date: Tue, 17 Mar 2026 09:00:43 +0000 Subject: [PATCH 10/10] feat(xds): Allow injecting bootstrap info into xDS Filter API for config parsing Extend the xDS Filter API to support injecting bootstrap information into filters during configuration parsing. This allows filters to access context information (e.g., allowed gRPC services) from the resource loading layer during configuration validation and parsing. - Update `Filter.Provider.parseFilterConfig` and `parseFilterConfigOverride` to accept a `FilterContext` parameter. - Introduce `BootstrapInfoGrpcServiceContextProvider` to encapsulate bootstrap info for context resolution. - Update `XdsListenerResource` and `XdsRouteConfigureResource` to construct and pass `FilterContext` during configuration parsing. - Update sub-filters (`FaultFilter`, `RbacFilter`, `GcpAuthenticationFilter`, `RouterFilter`) to match the updated `FilterContext` signature. Known Gaps & Limitations: 1. **MetricHolder**: Propagation of `MetricHolder` is not supported with this approach currently and is planned for support in a later phase. 2. **NameResolverRegistry**: Propagation is deferred for consistency. While it could be passed from `XdsNameResolver` on the client side, there is no equivalent mechanism on the server side. To ensure consistent behavior, `DefaultRegistry` is used when validating schemes and creating channels. --- ...otstrapInfoGrpcServiceContextProvider.java | 73 +++++++++ .../main/java/io/grpc/xds/FaultFilter.java | 8 +- xds/src/main/java/io/grpc/xds/Filter.java | 27 +++- .../io/grpc/xds/GcpAuthenticationFilter.java | 7 +- xds/src/main/java/io/grpc/xds/RbacFilter.java | 6 +- .../main/java/io/grpc/xds/RouterFilter.java | 5 +- .../java/io/grpc/xds/XdsListenerResource.java | 14 +- .../grpc/xds/XdsRouteConfigureResource.java | 20 ++- ...rapInfoGrpcServiceContextProviderTest.java | 139 ++++++++++++++++++ .../java/io/grpc/xds/FaultFilterTest.java | 14 +- .../grpc/xds/GcpAuthenticationFilterTest.java | 22 ++- .../grpc/xds/GrpcXdsClientImplDataTest.java | 56 ++++--- .../test/java/io/grpc/xds/RbacFilterTest.java | 27 +++- .../test/java/io/grpc/xds/StatefulFilter.java | 5 +- .../test/java/io/grpc/xds/XdsTestUtils.java | 7 +- 15 files changed, 369 insertions(+), 61 deletions(-) create mode 100644 xds/src/main/java/io/grpc/xds/BootstrapInfoGrpcServiceContextProvider.java create mode 100644 xds/src/test/java/io/grpc/xds/BootstrapInfoGrpcServiceContextProviderTest.java diff --git a/xds/src/main/java/io/grpc/xds/BootstrapInfoGrpcServiceContextProvider.java b/xds/src/main/java/io/grpc/xds/BootstrapInfoGrpcServiceContextProvider.java new file mode 100644 index 00000000000..864108ff431 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/BootstrapInfoGrpcServiceContextProvider.java @@ -0,0 +1,73 @@ +/* + * Copyright 2026 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.NameResolverRegistry; +import io.grpc.xds.client.Bootstrapper.BootstrapInfo; +import io.grpc.xds.client.Bootstrapper.ServerInfo; +import io.grpc.xds.internal.grpcservice.AllowedGrpcService; +import io.grpc.xds.internal.grpcservice.AllowedGrpcServices; +import io.grpc.xds.internal.grpcservice.GrpcServiceXdsContext; +import io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextProvider; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Optional; + +/** + * Concrete implementation of {@link GrpcServiceXdsContextProvider} that uses + * {@link BootstrapInfo} data to resolve context. + */ +final class BootstrapInfoGrpcServiceContextProvider + implements GrpcServiceXdsContextProvider { + + private final boolean isTrustedControlPlane; + private final AllowedGrpcServices allowedGrpcServices; + private final NameResolverRegistry nameResolverRegistry; + + BootstrapInfoGrpcServiceContextProvider(BootstrapInfo bootstrapInfo, ServerInfo serverInfo) { + this.isTrustedControlPlane = serverInfo.isTrustedXdsServer(); + this.allowedGrpcServices = bootstrapInfo.allowedGrpcServices() + .filter(AllowedGrpcServices.class::isInstance) + .map(AllowedGrpcServices.class::cast) + .orElse(AllowedGrpcServices.empty()); + this.nameResolverRegistry = NameResolverRegistry.getDefaultRegistry(); + } + + @Override + public GrpcServiceXdsContext getContextForTarget(String targetUri) { + Optional validAllowedGrpcService = + Optional.ofNullable(allowedGrpcServices.services().get(targetUri)); + + boolean isTargetUriSchemeSupported = false; + try { + URI uri = new URI(targetUri); + String scheme = uri.getScheme(); + if (scheme != null) { + isTargetUriSchemeSupported = + nameResolverRegistry.getProviderForScheme(scheme) != null; + } + } catch (URISyntaxException e) { + // Fallback or ignore if not a valid URI + } + + return GrpcServiceXdsContext.create( + isTrustedControlPlane, + validAllowedGrpcService, + isTargetUriSchemeSupported + ); + } +} diff --git a/xds/src/main/java/io/grpc/xds/FaultFilter.java b/xds/src/main/java/io/grpc/xds/FaultFilter.java index 0f3bb5b0557..e0533889d74 100644 --- a/xds/src/main/java/io/grpc/xds/FaultFilter.java +++ b/xds/src/main/java/io/grpc/xds/FaultFilter.java @@ -104,7 +104,8 @@ public FaultFilter newInstance(String name) { } @Override - public ConfigOrError parseFilterConfig(Message rawProtoMessage) { + public ConfigOrError parseFilterConfig( + Message rawProtoMessage, FilterContext context) { HTTPFault httpFaultProto; if (!(rawProtoMessage instanceof Any)) { return ConfigOrError.fromError("Invalid config type: " + rawProtoMessage.getClass()); @@ -119,8 +120,9 @@ public ConfigOrError parseFilterConfig(Message rawProtoMessage) { } @Override - public ConfigOrError parseFilterConfigOverride(Message rawProtoMessage) { - return parseFilterConfig(rawProtoMessage); + public ConfigOrError parseFilterConfigOverride( + Message rawProtoMessage, FilterContext context) { + return parseFilterConfig(rawProtoMessage, context); } private static ConfigOrError parseHttpFault(HTTPFault httpFault) { diff --git a/xds/src/main/java/io/grpc/xds/Filter.java b/xds/src/main/java/io/grpc/xds/Filter.java index 416d929becf..0fa5b8af128 100644 --- a/xds/src/main/java/io/grpc/xds/Filter.java +++ b/xds/src/main/java/io/grpc/xds/Filter.java @@ -16,10 +16,12 @@ package io.grpc.xds; + import com.google.common.base.MoreObjects; import com.google.protobuf.Message; import io.grpc.ClientInterceptor; import io.grpc.ServerInterceptor; +import io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextProvider; import java.io.Closeable; import java.util.Objects; import java.util.concurrent.ScheduledExecutorService; @@ -93,13 +95,15 @@ default boolean isServerFilter() { * Parses the top-level filter config from raw proto message. The message may be either a {@link * com.google.protobuf.Any} or a {@link com.google.protobuf.Struct}. */ - ConfigOrError parseFilterConfig(Message rawProtoMessage); + ConfigOrError parseFilterConfig( + Message rawProtoMessage, FilterContext context); /** * Parses the per-filter override filter config from raw proto message. The message may be * either a {@link com.google.protobuf.Any} or a {@link com.google.protobuf.Struct}. */ - ConfigOrError parseFilterConfigOverride(Message rawProtoMessage); + ConfigOrError parseFilterConfigOverride( + Message rawProtoMessage, FilterContext context); } /** Uses the FilterConfigs produced above to produce an HTTP filter interceptor for clients. */ @@ -125,6 +129,25 @@ default ServerInterceptor buildServerInterceptor( @Override default void close() {} + /** Context carrying dynamic metadata for a filter. */ + @com.google.auto.value.AutoValue + abstract class FilterContext { + public abstract GrpcServiceXdsContextProvider grpcServiceContextProvider(); + + public static Builder builder() { + return new AutoValue_Filter_FilterContext.Builder(); + } + + + @com.google.auto.value.AutoValue.Builder + public abstract static class Builder { + public abstract Builder grpcServiceContextProvider( + GrpcServiceXdsContextProvider provider); + + public abstract FilterContext build(); + } + } + /** Filter config with instance name. */ final class NamedFilterConfig { // filter instance name diff --git a/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java b/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java index 8ec02f4f809..78d20edec46 100644 --- a/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java +++ b/xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.java @@ -86,7 +86,8 @@ public GcpAuthenticationFilter newInstance(String name) { } @Override - public ConfigOrError parseFilterConfig(Message rawProtoMessage) { + public ConfigOrError parseFilterConfig( + Message rawProtoMessage, FilterContext context) { GcpAuthnFilterConfig gcpAuthnProto; if (!(rawProtoMessage instanceof Any)) { return ConfigOrError.fromError("Invalid config type: " + rawProtoMessage.getClass()); @@ -121,8 +122,8 @@ public ConfigOrError parseFilterConfig(Message rawProto @Override public ConfigOrError parseFilterConfigOverride( - Message rawProtoMessage) { - return parseFilterConfig(rawProtoMessage); + Message rawProtoMessage, FilterContext context) { + return parseFilterConfig(rawProtoMessage, context); } } diff --git a/xds/src/main/java/io/grpc/xds/RbacFilter.java b/xds/src/main/java/io/grpc/xds/RbacFilter.java index 91df1e68802..035bfd06607 100644 --- a/xds/src/main/java/io/grpc/xds/RbacFilter.java +++ b/xds/src/main/java/io/grpc/xds/RbacFilter.java @@ -94,7 +94,8 @@ public RbacFilter newInstance(String name) { } @Override - public ConfigOrError parseFilterConfig(Message rawProtoMessage) { + public ConfigOrError parseFilterConfig( + Message rawProtoMessage, FilterContext context) { RBAC rbacProto; if (!(rawProtoMessage instanceof Any)) { return ConfigOrError.fromError("Invalid config type: " + rawProtoMessage.getClass()); @@ -109,7 +110,8 @@ public ConfigOrError parseFilterConfig(Message rawProtoMessage) { } @Override - public ConfigOrError parseFilterConfigOverride(Message rawProtoMessage) { + public ConfigOrError parseFilterConfigOverride( + Message rawProtoMessage, FilterContext context) { RBACPerRoute rbacPerRoute; if (!(rawProtoMessage instanceof Any)) { return ConfigOrError.fromError("Invalid config type: " + rawProtoMessage.getClass()); diff --git a/xds/src/main/java/io/grpc/xds/RouterFilter.java b/xds/src/main/java/io/grpc/xds/RouterFilter.java index 504c4213149..c80e57c9010 100644 --- a/xds/src/main/java/io/grpc/xds/RouterFilter.java +++ b/xds/src/main/java/io/grpc/xds/RouterFilter.java @@ -61,13 +61,14 @@ public RouterFilter newInstance(String name) { } @Override - public ConfigOrError parseFilterConfig(Message rawProtoMessage) { + public ConfigOrError parseFilterConfig( + Message rawProtoMessage, FilterContext context) { return ConfigOrError.fromConfig(ROUTER_CONFIG); } @Override public ConfigOrError parseFilterConfigOverride( - Message rawProtoMessage) { + Message rawProtoMessage, FilterContext context) { return ConfigOrError.fromError("Router Filter should not have override config"); } } diff --git a/xds/src/main/java/io/grpc/xds/XdsListenerResource.java b/xds/src/main/java/io/grpc/xds/XdsListenerResource.java index 041b659b4c3..4aff4a7f2ad 100644 --- a/xds/src/main/java/io/grpc/xds/XdsListenerResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsListenerResource.java @@ -527,7 +527,7 @@ static io.grpc.xds.HttpConnectionManager parseHttpConnectionManager( "HttpConnectionManager contains duplicate HttpFilter: " + filterName); } StructOrError filterConfig = - parseHttpFilter(httpFilter, filterRegistry, isForClient); + parseHttpFilter(httpFilter, filterRegistry, isForClient, args); if ((i == proto.getHttpFiltersCount() - 1) && (filterConfig == null || !isTerminalFilter(filterConfig.getStruct()))) { throw new ResourceInvalidException("The last HttpFilter must be a terminal filter: " @@ -581,7 +581,8 @@ private static boolean isTerminalFilter(Filter.FilterConfig filterConfig) { @Nullable // Returns null if the filter is optional but not supported. static StructOrError parseHttpFilter( io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter - httpFilter, FilterRegistry filterRegistry, boolean isForClient) { + httpFilter, FilterRegistry filterRegistry, boolean isForClient, + XdsResourceType.Args args) { String filterName = httpFilter.getName(); boolean isOptional = httpFilter.getIsOptional(); if (!httpFilter.hasTypedConfig()) { @@ -616,7 +617,14 @@ static StructOrError parseHttpFilter( "HttpFilter [" + filterName + "](" + typeUrl + ") is required but unsupported for " + ( isForClient ? "client" : "server")); } - ConfigOrError filterConfig = provider.parseFilterConfig(rawConfig); + + BootstrapInfoGrpcServiceContextProvider contextProvider = + new BootstrapInfoGrpcServiceContextProvider(args.getBootstrapInfo(), args.getServerInfo()); + Filter.FilterContext filterContext = Filter.FilterContext.builder() + .grpcServiceContextProvider(contextProvider) + .build(); + ConfigOrError filterConfig = + provider.parseFilterConfig(rawConfig, filterContext); if (filterConfig.errorDetail != null) { return StructOrError.fromError( "Invalid filter config for HttpFilter [" + filterName + "]: " + filterConfig.errorDetail); diff --git a/xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java b/xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java index 24ec0659b42..0bb0c48cd65 100644 --- a/xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsRouteConfigureResource.java @@ -198,7 +198,7 @@ private static StructOrError parseVirtualHost( routes.add(route.getStruct()); } StructOrError> overrideConfigs = - parseOverrideFilterConfigs(proto.getTypedPerFilterConfigMap(), filterRegistry); + parseOverrideFilterConfigs(proto.getTypedPerFilterConfigMap(), filterRegistry, args); if (overrideConfigs.getErrorDetail() != null) { return StructOrError.fromError( "VirtualHost [" + proto.getName() + "] contains invalid HttpFilter config: " @@ -210,7 +210,13 @@ private static StructOrError parseVirtualHost( @VisibleForTesting static StructOrError> parseOverrideFilterConfigs( - Map rawFilterConfigMap, FilterRegistry filterRegistry) { + Map rawFilterConfigMap, FilterRegistry filterRegistry, + XdsResourceType.Args args) { + BootstrapInfoGrpcServiceContextProvider grpcServiceContextProvider = + new BootstrapInfoGrpcServiceContextProvider(args.getBootstrapInfo(), args.getServerInfo()); + Filter.FilterContext context = Filter.FilterContext.builder() + .grpcServiceContextProvider(grpcServiceContextProvider) + .build(); Map overrideConfigs = new HashMap<>(); for (String name : rawFilterConfigMap.keySet()) { Any anyConfig = rawFilterConfigMap.get(name); @@ -254,7 +260,7 @@ static StructOrError> parseOverrideFilterConfigs( "HttpFilter [" + name + "](" + typeUrl + ") is required but unsupported"); } ConfigOrError filterConfig = - provider.parseFilterConfigOverride(rawConfig); + provider.parseFilterConfigOverride(rawConfig, context); if (filterConfig.errorDetail != null) { return StructOrError.fromError( "Invalid filter config for HttpFilter [" + name + "]: " + filterConfig.errorDetail); @@ -281,7 +287,7 @@ static StructOrError parseRoute( } StructOrError> overrideConfigsOrError = - parseOverrideFilterConfigs(proto.getTypedPerFilterConfigMap(), filterRegistry); + parseOverrideFilterConfigs(proto.getTypedPerFilterConfigMap(), filterRegistry, args); if (overrideConfigsOrError.getErrorDetail() != null) { return StructOrError.fromError( "Route [" + proto.getName() + "] contains invalid HttpFilter config: " @@ -490,7 +496,7 @@ static StructOrError parseRouteAction( for (io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight clusterWeight : clusterWeights) { StructOrError clusterWeightOrError = - parseClusterWeight(clusterWeight, filterRegistry); + parseClusterWeight(clusterWeight, filterRegistry, args); if (clusterWeightOrError.getErrorDetail() != null) { return StructOrError.fromError("RouteAction contains invalid ClusterWeight: " + clusterWeightOrError.getErrorDetail()); @@ -599,9 +605,9 @@ private static StructOrError parseRet @VisibleForTesting static StructOrError parseClusterWeight( io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight proto, - FilterRegistry filterRegistry) { + FilterRegistry filterRegistry, XdsResourceType.Args args) { StructOrError> overrideConfigs = - parseOverrideFilterConfigs(proto.getTypedPerFilterConfigMap(), filterRegistry); + parseOverrideFilterConfigs(proto.getTypedPerFilterConfigMap(), filterRegistry, args); if (overrideConfigs.getErrorDetail() != null) { return StructOrError.fromError( "ClusterWeight [" + proto.getName() + "] contains invalid HttpFilter config: " diff --git a/xds/src/test/java/io/grpc/xds/BootstrapInfoGrpcServiceContextProviderTest.java b/xds/src/test/java/io/grpc/xds/BootstrapInfoGrpcServiceContextProviderTest.java new file mode 100644 index 00000000000..ab42f634daa --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/BootstrapInfoGrpcServiceContextProviderTest.java @@ -0,0 +1,139 @@ +/* + * Copyright 2026 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.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import io.grpc.ChannelCredentials; +import io.grpc.InsecureChannelCredentials; +import io.grpc.xds.client.Bootstrapper.BootstrapInfo; +import io.grpc.xds.client.Bootstrapper.ServerInfo; +import io.grpc.xds.client.EnvoyProtoData; +import io.grpc.xds.internal.grpcservice.AllowedGrpcService; +import io.grpc.xds.internal.grpcservice.AllowedGrpcServices; +import io.grpc.xds.internal.grpcservice.ChannelCredsConfig; +import io.grpc.xds.internal.grpcservice.ConfiguredChannelCredentials; +import io.grpc.xds.internal.grpcservice.GrpcServiceXdsContext; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Unit tests for {@link BootstrapInfoGrpcServiceContextProvider}. + */ +@RunWith(JUnit4.class) +public class BootstrapInfoGrpcServiceContextProviderTest { + + private static final ChannelCredentials CREDENTIALS = InsecureChannelCredentials.create(); + private static final ChannelCredsConfig DUMMY_CONFIG = () -> "dummy"; + private static final EnvoyProtoData.Node DUMMY_NODE = + EnvoyProtoData.Node.newBuilder().setId("node-id").build(); + + private static final BootstrapInfo DUMMY_BOOTSTRAP = BootstrapInfo.builder() + .servers(ImmutableList.of()) + .node(DUMMY_NODE) + .build(); + + private static ServerInfo createServerInfo(boolean isTrusted) { + return ServerInfo.create("xds:///any", CREDENTIALS, false, isTrusted, false, false); + } + + @Test + public void getContextForTarget_trustedServer() { + ServerInfo serverInfo = createServerInfo(true); + BootstrapInfoGrpcServiceContextProvider provider = + new BootstrapInfoGrpcServiceContextProvider(DUMMY_BOOTSTRAP, serverInfo); + + GrpcServiceXdsContext context = provider.getContextForTarget("xds:///any"); + assertThat(context.isTrustedControlPlane()).isTrue(); + } + + @Test + public void getContextForTarget_untrustedServer() { + ServerInfo serverInfo = createServerInfo(false); + BootstrapInfoGrpcServiceContextProvider provider = + new BootstrapInfoGrpcServiceContextProvider(DUMMY_BOOTSTRAP, serverInfo); + + GrpcServiceXdsContext context = provider.getContextForTarget("xds:///any"); + assertThat(context.isTrustedControlPlane()).isFalse(); + } + + @Test + public void getContextForTarget_allowedGrpcServices() { + ConfiguredChannelCredentials creds = ConfiguredChannelCredentials.create( + CREDENTIALS, DUMMY_CONFIG); + AllowedGrpcService allowedService = AllowedGrpcService.builder() + .configuredChannelCredentials(creds) + .build(); + + Map servicesMap = new HashMap<>(); + servicesMap.put("xds:///target1", allowedService); + AllowedGrpcServices allowedGrpcServices = AllowedGrpcServices.create(servicesMap); + + BootstrapInfo bootstrapInfo = BootstrapInfo.builder() + .servers(ImmutableList.of()) + .node(DUMMY_NODE) + .allowedGrpcServices(Optional.of(allowedGrpcServices)) + .build(); + + BootstrapInfoGrpcServiceContextProvider provider = + new BootstrapInfoGrpcServiceContextProvider(bootstrapInfo, createServerInfo(false)); + + GrpcServiceXdsContext context = provider.getContextForTarget("xds:///target1"); + assertThat(context.validAllowedGrpcService().isPresent()).isTrue(); + assertThat(context.validAllowedGrpcService().get()).isEqualTo(allowedService); + + // Target not in map + GrpcServiceXdsContext context2 = provider.getContextForTarget("xds:///target2"); + assertThat(context2.validAllowedGrpcService().isPresent()).isFalse(); + } + + @Test + public void getContextForTarget_schemeSupported() { + BootstrapInfoGrpcServiceContextProvider provider = + new BootstrapInfoGrpcServiceContextProvider(DUMMY_BOOTSTRAP, createServerInfo(false)); + + assertThat(provider.getContextForTarget("dns:///foo").isTargetUriSchemeSupported()).isTrue(); + assertThat(provider.getContextForTarget("unknown:///foo").isTargetUriSchemeSupported()) + .isFalse(); + } + + @Test + public void getContextForTarget_invalidUri() { + BootstrapInfoGrpcServiceContextProvider provider = + new BootstrapInfoGrpcServiceContextProvider(DUMMY_BOOTSTRAP, createServerInfo(false)); + + GrpcServiceXdsContext context = provider.getContextForTarget("invalid:uri:with:colons"); + assertThat(context.isTargetUriSchemeSupported()).isFalse(); + } + + @Test + public void getContextForTarget_invalidAllowedGrpcServicesTypeFallbackToEmpty() { + BootstrapInfo bootstrapInfo = BootstrapInfo.builder().servers(ImmutableList.of()) + .node(DUMMY_NODE).allowedGrpcServices(Optional.of("invalid_type_string")).build(); + + BootstrapInfoGrpcServiceContextProvider provider = + new BootstrapInfoGrpcServiceContextProvider(bootstrapInfo, createServerInfo(false)); + + GrpcServiceXdsContext context = provider.getContextForTarget("xds:///any"); + assertThat(context.validAllowedGrpcService().isPresent()).isFalse(); + } +} diff --git a/xds/src/test/java/io/grpc/xds/FaultFilterTest.java b/xds/src/test/java/io/grpc/xds/FaultFilterTest.java index 8f0a33951b0..f74e39e727f 100644 --- a/xds/src/test/java/io/grpc/xds/FaultFilterTest.java +++ b/xds/src/test/java/io/grpc/xds/FaultFilterTest.java @@ -17,6 +17,7 @@ package io.grpc.xds; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; import com.google.protobuf.Any; import io.envoyproxy.envoy.extensions.filters.http.fault.v3.FaultAbort; @@ -26,6 +27,7 @@ import io.envoyproxy.envoy.type.v3.FractionalPercent.DenominatorType; import io.grpc.Status.Code; import io.grpc.internal.GrpcUtil; +import io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextProvider; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -45,11 +47,14 @@ public void filterType_clientOnly() { public void parseFaultAbort_convertHttpStatus() { Any rawConfig = Any.pack( HTTPFault.newBuilder().setAbort(FaultAbort.newBuilder().setHttpStatus(404)).build()); - FaultConfig faultConfig = FILTER_PROVIDER.parseFilterConfig(rawConfig).config; + FaultConfig faultConfig = FILTER_PROVIDER.parseFilterConfig( + rawConfig, getFilterContext()).config; assertThat(faultConfig.faultAbort().status().getCode()) .isEqualTo(GrpcUtil.httpStatusToGrpcStatus(404).getCode()); - FaultConfig faultConfigOverride = FILTER_PROVIDER.parseFilterConfigOverride(rawConfig).config; + FaultConfig faultConfigOverride = + FILTER_PROVIDER.parseFilterConfigOverride( + rawConfig, getFilterContext()).config; assertThat(faultConfigOverride.faultAbort().status().getCode()) .isEqualTo(GrpcUtil.httpStatusToGrpcStatus(404).getCode()); } @@ -95,4 +100,9 @@ public void parseFaultAbort_withGrpcStatus() { .isEqualTo(FaultConfig.FractionalPercent.DenominatorType.MILLION); assertThat(faultAbort.status().getCode()).isEqualTo(Code.DEADLINE_EXCEEDED); } + + private static Filter.FilterContext getFilterContext() { + return Filter.FilterContext.builder() + .grpcServiceContextProvider(mock(GrpcServiceXdsContextProvider.class)).build(); + } } diff --git a/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java b/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java index f252c6f4ec1..2a1ee36d0e9 100644 --- a/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java +++ b/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java @@ -68,6 +68,7 @@ import io.grpc.xds.client.Locality; import io.grpc.xds.client.XdsResourceType; import io.grpc.xds.client.XdsResourceType.ResourceInvalidException; +import io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextProvider; import java.io.IOException; import java.util.Collections; import java.util.HashMap; @@ -112,8 +113,8 @@ public void testParseFilterConfig_withValidConfig() { .build(); Any anyMessage = Any.pack(config); - ConfigOrError result = FILTER_PROVIDER.parseFilterConfig(anyMessage); - + ConfigOrError result = + FILTER_PROVIDER.parseFilterConfig(anyMessage, getFilterContext()); assertNotNull(result.config); assertNull(result.errorDetail); assertEquals(20L, result.config.getCacheSize()); @@ -126,8 +127,8 @@ public void testParseFilterConfig_withZeroCacheSize() { .build(); Any anyMessage = Any.pack(config); - ConfigOrError result = FILTER_PROVIDER.parseFilterConfig(anyMessage); - + ConfigOrError result = + FILTER_PROVIDER.parseFilterConfig(anyMessage, getFilterContext()); assertNull(result.config); assertNotNull(result.errorDetail); assertTrue(result.errorDetail.contains("cache_config.cache_size must be greater than zero")); @@ -137,7 +138,7 @@ public void testParseFilterConfig_withZeroCacheSize() { public void testParseFilterConfig_withInvalidMessageType() { Message invalidMessage = Empty.getDefaultInstance(); ConfigOrError result = - FILTER_PROVIDER.parseFilterConfig(invalidMessage); + FILTER_PROVIDER.parseFilterConfig(invalidMessage, getFilterContext()); assertNull(result.config); assertThat(result.errorDetail).contains("Invalid config type"); @@ -468,8 +469,9 @@ private static LdsUpdate getLdsUpdate() { private static RdsUpdate getRdsUpdate() { RouteConfiguration routeConfiguration = buildRouteConfiguration("my-server", RDS_NAME, CLUSTER_NAME); - XdsResourceType.Args args = new XdsResourceType.Args( - XdsTestUtils.EMPTY_BOOTSTRAPPER_SERVER_INFO, "0", "0", null, null, null); + XdsResourceType.Args args = + new XdsResourceType.Args(XdsTestUtils.EMPTY_BOOTSTRAPPER_SERVER_INFO, "0", "0", + XdsTestUtils.EMPTY_BOOTSTRAP, null, null); try { return XdsRouteConfigureResource.getInstance().doParse(args, routeConfiguration); } catch (ResourceInvalidException ex) { @@ -521,4 +523,10 @@ private static CdsUpdate getCdsUpdateWithIncorrectAudienceWrapper() throws IOExc .lbPolicyConfig(getWrrLbConfigAsMap()); return cdsUpdate.parsedMetadata(parsedMetadata.build()).build(); } + + private static Filter.FilterContext getFilterContext() { + return Filter.FilterContext.builder() + .grpcServiceContextProvider(Mockito.mock(GrpcServiceXdsContextProvider.class)) + .build(); + } } diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java index be29e5e719f..7d88f9ebf94 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java @@ -1049,7 +1049,9 @@ public void parseClusterWeight() { .setWeight(UInt32Value.newBuilder().setValue(30)) .build(); ClusterWeight clusterWeight = - XdsRouteConfigureResource.parseClusterWeight(proto, filterRegistry).getStruct(); + XdsRouteConfigureResource + .parseClusterWeight(proto, filterRegistry, getXdsResourceTypeArgs(true)) + .getStruct(); assertThat(clusterWeight.name()).isEqualTo("cluster-foo"); assertThat(clusterWeight.weight()).isEqualTo(30); } @@ -1255,7 +1257,8 @@ public void parseHttpFilter_unsupportedButOptional() { .setIsOptional(true) .setTypedConfig(Any.pack(StringValue.of("unsupported"))) .build(); - assertThat(XdsListenerResource.parseHttpFilter(httpFilter, filterRegistry, true)).isNull(); + assertThat(XdsListenerResource.parseHttpFilter(httpFilter, filterRegistry, true, + getXdsResourceTypeArgs(true))).isNull(); } private static class SimpleFilterConfig implements FilterConfig { @@ -1294,12 +1297,14 @@ public TestFilter newInstance(String name) { } @Override - public ConfigOrError parseFilterConfig(Message rawProtoMessage) { + public ConfigOrError parseFilterConfig(Message rawProtoMessage, + FilterContext context) { return ConfigOrError.fromConfig(new SimpleFilterConfig(rawProtoMessage)); } @Override - public ConfigOrError parseFilterConfigOverride(Message rawProtoMessage) { + public ConfigOrError parseFilterConfigOverride(Message rawProtoMessage, + FilterContext context) { return ConfigOrError.fromConfig(new SimpleFilterConfig(rawProtoMessage)); } } @@ -1319,7 +1324,7 @@ public void parseHttpFilter_typedStructMigration() { .setValue(rawStruct) .build())).build(); FilterConfig config = XdsListenerResource.parseHttpFilter(httpFilter, filterRegistry, - true).getStruct(); + true, getXdsResourceTypeArgs(true)).getStruct(); assertThat(((SimpleFilterConfig)config).getConfig()).isEqualTo(rawStruct); HttpFilter httpFilterNewTypeStruct = HttpFilter.newBuilder() @@ -1330,7 +1335,7 @@ public void parseHttpFilter_typedStructMigration() { .setValue(rawStruct) .build())).build(); config = XdsListenerResource.parseHttpFilter(httpFilterNewTypeStruct, filterRegistry, - true).getStruct(); + true, getXdsResourceTypeArgs(true)).getStruct(); assertThat(((SimpleFilterConfig)config).getConfig()).isEqualTo(rawStruct); } @@ -1356,7 +1361,7 @@ public void parseOverrideHttpFilter_typedStructMigration() { .build()) ); Map map = XdsRouteConfigureResource.parseOverrideFilterConfigs( - rawFilterMap, filterRegistry).getStruct(); + rawFilterMap, filterRegistry, getXdsResourceTypeArgs(true)).getStruct(); assertThat(((SimpleFilterConfig)map.get("struct-0")).getConfig()).isEqualTo(rawStruct0); assertThat(((SimpleFilterConfig)map.get("struct-1")).getConfig()).isEqualTo(rawStruct1); } @@ -1368,7 +1373,8 @@ public void parseHttpFilter_unsupportedAndRequired() { .setName("unsupported.filter") .setTypedConfig(Any.pack(StringValue.of("string value"))) .build(); - assertThat(XdsListenerResource.parseHttpFilter(httpFilter, filterRegistry, true) + assertThat(XdsListenerResource + .parseHttpFilter(httpFilter, filterRegistry, true, getXdsResourceTypeArgs(true)) .getErrorDetail()).isEqualTo( "HttpFilter [unsupported.filter]" + "(type.googleapis.com/google.protobuf.StringValue) is required but unsupported " @@ -1385,7 +1391,8 @@ public void parseHttpFilter_routerFilterForClient() { .setTypedConfig(Any.pack(Router.getDefaultInstance())) .build(); FilterConfig config = XdsListenerResource.parseHttpFilter( - httpFilter, filterRegistry, true /* isForClient */).getStruct(); + httpFilter, filterRegistry, true /* isForClient */, getXdsResourceTypeArgs(true)) + .getStruct(); assertThat(config.typeUrl()).isEqualTo(RouterFilter.TYPE_URL); } @@ -1399,7 +1406,8 @@ public void parseHttpFilter_routerFilterForServer() { .setTypedConfig(Any.pack(Router.getDefaultInstance())) .build(); FilterConfig config = XdsListenerResource.parseHttpFilter( - httpFilter, filterRegistry, false /* isForClient */).getStruct(); + httpFilter, filterRegistry, false /* isForClient */, getXdsResourceTypeArgs(false)) + .getStruct(); assertThat(config.typeUrl()).isEqualTo(RouterFilter.TYPE_URL); } @@ -1426,7 +1434,8 @@ public void parseHttpFilter_faultConfigForClient() { .build())) .build(); FilterConfig config = XdsListenerResource.parseHttpFilter( - httpFilter, filterRegistry, true /* isForClient */).getStruct(); + httpFilter, filterRegistry, true /* isForClient */, getXdsResourceTypeArgs(true)) + .getStruct(); assertThat(config).isInstanceOf(FaultConfig.class); } @@ -1453,7 +1462,8 @@ public void parseHttpFilter_faultConfigUnsupportedForServer() { .build())) .build(); StructOrError config = - XdsListenerResource.parseHttpFilter(httpFilter, filterRegistry, false /* isForClient */); + XdsListenerResource.parseHttpFilter(httpFilter, filterRegistry, false /* isForClient */, + getXdsResourceTypeArgs(false)); assertThat(config.getErrorDetail()).isEqualTo( "HttpFilter [envoy.fault](" + FaultFilter.TYPE_URL + ") is required but " + "unsupported for server"); @@ -1482,7 +1492,8 @@ public void parseHttpFilter_rbacConfigForServer() { .build())) .build(); FilterConfig config = XdsListenerResource.parseHttpFilter( - httpFilter, filterRegistry, false /* isForClient */).getStruct(); + httpFilter, filterRegistry, false /* isForClient */, getXdsResourceTypeArgs(false)) + .getStruct(); assertThat(config).isInstanceOf(RbacConfig.class); } @@ -1509,7 +1520,8 @@ public void parseHttpFilter_rbacConfigUnsupportedForClient() { .build())) .build(); StructOrError config = - XdsListenerResource.parseHttpFilter(httpFilter, filterRegistry, true /* isForClient */); + XdsListenerResource.parseHttpFilter(httpFilter, filterRegistry, true /* isForClient */, + getXdsResourceTypeArgs(true)); assertThat(config.getErrorDetail()).isEqualTo( "HttpFilter [envoy.auth](" + RbacFilter.TYPE_URL + ") is required but " + "unsupported for client"); @@ -1534,7 +1546,8 @@ public void parseOverrideRbacFilterConfig() { .build(); Map configOverrides = ImmutableMap.of("envoy.auth", Any.pack(rbacPerRoute)); Map parsedConfigs = - XdsRouteConfigureResource.parseOverrideFilterConfigs(configOverrides, filterRegistry) + XdsRouteConfigureResource.parseOverrideFilterConfigs(configOverrides, filterRegistry, + getXdsResourceTypeArgs(true)) .getStruct(); assertThat(parsedConfigs).hasSize(1); assertThat(parsedConfigs).containsKey("envoy.auth"); @@ -1555,7 +1568,8 @@ public void parseOverrideFilterConfigs_unsupportedButOptional() { .setIsOptional(true).setConfig(Any.pack(StringValue.of("string value"))) .build())); Map parsedConfigs = - XdsRouteConfigureResource.parseOverrideFilterConfigs(configOverrides, filterRegistry) + XdsRouteConfigureResource.parseOverrideFilterConfigs(configOverrides, filterRegistry, + getXdsResourceTypeArgs(true)) .getStruct(); assertThat(parsedConfigs).hasSize(1); assertThat(parsedConfigs).containsKey("envoy.fault"); @@ -1574,7 +1588,9 @@ public void parseOverrideFilterConfigs_unsupportedAndRequired() { Any.pack(io.envoyproxy.envoy.config.route.v3.FilterConfig.newBuilder() .setIsOptional(false).setConfig(Any.pack(StringValue.of("string value"))) .build())); - assertThat(XdsRouteConfigureResource.parseOverrideFilterConfigs(configOverrides, filterRegistry) + assertThat(XdsRouteConfigureResource + .parseOverrideFilterConfigs(configOverrides, filterRegistry, + getXdsResourceTypeArgs(true)) .getErrorDetail()).isEqualTo( "HttpFilter [unsupported.filter]" + "(type.googleapis.com/google.protobuf.StringValue) is required but unsupported"); @@ -1584,7 +1600,9 @@ public void parseOverrideFilterConfigs_unsupportedAndRequired() { Any.pack(httpFault), "unsupported.filter", Any.pack(StringValue.of("string value"))); - assertThat(XdsRouteConfigureResource.parseOverrideFilterConfigs(configOverrides, filterRegistry) + assertThat(XdsRouteConfigureResource + .parseOverrideFilterConfigs(configOverrides, filterRegistry, + getXdsResourceTypeArgs(true)) .getErrorDetail()).isEqualTo( "HttpFilter [unsupported.filter]" + "(type.googleapis.com/google.protobuf.StringValue) is required but unsupported"); @@ -3614,7 +3632,7 @@ private static Filter buildHttpConnectionManagerFilter(HttpFilter... httpFilters private XdsResourceType.Args getXdsResourceTypeArgs(boolean isTrustedServer) { return new XdsResourceType.Args( - ServerInfo.create("http://td", "", false, isTrustedServer, false, false), "1.0", null, null, null, null + ServerInfo.create("http://td", "", false, isTrustedServer, false, false), "1.0", null, XdsTestUtils.EMPTY_BOOTSTRAP, null, null ); } } diff --git a/xds/src/test/java/io/grpc/xds/RbacFilterTest.java b/xds/src/test/java/io/grpc/xds/RbacFilterTest.java index 334e159dd1d..ca59ab4e524 100644 --- a/xds/src/test/java/io/grpc/xds/RbacFilterTest.java +++ b/xds/src/test/java/io/grpc/xds/RbacFilterTest.java @@ -299,7 +299,7 @@ public void handleException() { .putPolicies("policy-name", Policy.newBuilder().setCondition(Expr.newBuilder().build()).build()) .build()).build(); - result = FILTER_PROVIDER.parseFilterConfig(Any.pack(rawProto)); + result = FILTER_PROVIDER.parseFilterConfig(Any.pack(rawProto), getFilterContext()); assertThat(result.errorDetail).isNotNull(); } @@ -321,7 +321,8 @@ public void overrideConfig() { RbacConfig original = RbacConfig.create(authconfig); RBACPerRoute rbacPerRoute = RBACPerRoute.newBuilder().build(); - RbacConfig override = FILTER_PROVIDER.parseFilterConfigOverride(Any.pack(rbacPerRoute)).config; + RbacConfig override = FILTER_PROVIDER.parseFilterConfigOverride(Any.pack(rbacPerRoute), + getFilterContext()).config; assertThat(override).isEqualTo(RbacConfig.create(null)); ServerInterceptor interceptor = FILTER_PROVIDER.newInstance(name).buildServerInterceptor(original, override); @@ -346,22 +347,26 @@ public void ignoredConfig() { Message rawProto = io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBAC.newBuilder() .setRules(RBAC.newBuilder().setAction(Action.LOG) .putPolicies("policy-name", Policy.newBuilder().build()).build()).build(); - ConfigOrError result = FILTER_PROVIDER.parseFilterConfig(Any.pack(rawProto)); + ConfigOrError result = + FILTER_PROVIDER.parseFilterConfig(Any.pack(rawProto), getFilterContext()); assertThat(result.config).isEqualTo(RbacConfig.create(null)); } @Test public void testOrderIndependenceOfPolicies() { Message rawProto = buildComplexRbac(ImmutableList.of(1, 2, 3, 4, 5, 6), true); - ConfigOrError ascFirst = FILTER_PROVIDER.parseFilterConfig(Any.pack(rawProto)); + ConfigOrError ascFirst = + FILTER_PROVIDER.parseFilterConfig(Any.pack(rawProto), getFilterContext()); rawProto = buildComplexRbac(ImmutableList.of(1, 2, 3, 4, 5, 6), false); - ConfigOrError ascLast = FILTER_PROVIDER.parseFilterConfig(Any.pack(rawProto)); + ConfigOrError ascLast = + FILTER_PROVIDER.parseFilterConfig(Any.pack(rawProto), getFilterContext()); assertThat(ascFirst.config).isEqualTo(ascLast.config); rawProto = buildComplexRbac(ImmutableList.of(6, 5, 4, 3, 2, 1), true); - ConfigOrError decFirst = FILTER_PROVIDER.parseFilterConfig(Any.pack(rawProto)); + ConfigOrError decFirst = + FILTER_PROVIDER.parseFilterConfig(Any.pack(rawProto), getFilterContext()); assertThat(ascFirst.config).isEqualTo(decFirst.config); } @@ -390,7 +395,7 @@ private ConfigOrError parseRaw(List permissionList, List principalList) { Message rawProto = buildRbac(permissionList, principalList); Any proto = Any.pack(rawProto); - return FILTER_PROVIDER.parseFilterConfig(proto); + return FILTER_PROVIDER.parseFilterConfig(proto, getFilterContext()); } private io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBAC buildRbac( @@ -458,6 +463,12 @@ private ConfigOrError parseOverride(List permissionList, RBACPerRoute rbacPerRoute = RBACPerRoute.newBuilder().setRbac( buildRbac(permissionList, principalList)).build(); Any proto = Any.pack(rbacPerRoute); - return FILTER_PROVIDER.parseFilterConfigOverride(proto); + return FILTER_PROVIDER.parseFilterConfigOverride(proto, getFilterContext()); + } + + private Filter.FilterContext getFilterContext() { + return Filter.FilterContext.builder().grpcServiceContextProvider(mock( + io.grpc.xds.internal.grpcservice.GrpcServiceXdsContextProvider.class)) + .build(); } } diff --git a/xds/src/test/java/io/grpc/xds/StatefulFilter.java b/xds/src/test/java/io/grpc/xds/StatefulFilter.java index 4ef662c7ccd..7626222dc04 100644 --- a/xds/src/test/java/io/grpc/xds/StatefulFilter.java +++ b/xds/src/test/java/io/grpc/xds/StatefulFilter.java @@ -128,12 +128,13 @@ public synchronized int getCount() { } @Override - public ConfigOrError parseFilterConfig(Message rawProtoMessage) { + public ConfigOrError parseFilterConfig(Message rawProtoMessage, FilterContext context) { return ConfigOrError.fromConfig(Config.fromProto(rawProtoMessage, typeUrl)); } @Override - public ConfigOrError parseFilterConfigOverride(Message rawProtoMessage) { + public ConfigOrError parseFilterConfigOverride( + Message rawProtoMessage, FilterContext context) { return ConfigOrError.fromConfig(Config.fromProto(rawProtoMessage, typeUrl)); } } diff --git a/xds/src/test/java/io/grpc/xds/XdsTestUtils.java b/xds/src/test/java/io/grpc/xds/XdsTestUtils.java index f81957ee311..93113411b5e 100644 --- a/xds/src/test/java/io/grpc/xds/XdsTestUtils.java +++ b/xds/src/test/java/io/grpc/xds/XdsTestUtils.java @@ -88,6 +88,11 @@ public class XdsTestUtils { static final Bootstrapper.ServerInfo EMPTY_BOOTSTRAPPER_SERVER_INFO = Bootstrapper.ServerInfo.create( "td.googleapis.com", InsecureChannelCredentials.create(), false, true, false, false); + static final Bootstrapper.BootstrapInfo EMPTY_BOOTSTRAP = + Bootstrapper.BootstrapInfo.builder() + .servers(com.google.common.collect.ImmutableList.of(EMPTY_BOOTSTRAPPER_SERVER_INFO)) + .node(io.grpc.xds.client.EnvoyProtoData.Node.newBuilder().setId("node-id").build()) + .build(); public static final String ENDPOINT_HOSTNAME = "data-host"; public static final int ENDPOINT_PORT = 1234; @@ -252,7 +257,7 @@ static XdsConfig getDefaultXdsConfig(String serverHostName) RouteConfiguration routeConfiguration = buildRouteConfiguration(serverHostName, RDS_NAME, CLUSTER_NAME); XdsResourceType.Args args = new XdsResourceType.Args( - EMPTY_BOOTSTRAPPER_SERVER_INFO, "0", "0", null, null, null); + EMPTY_BOOTSTRAPPER_SERVER_INFO, "0", "0", EMPTY_BOOTSTRAP, null, null); XdsRouteConfigureResource.RdsUpdate rdsUpdate = XdsRouteConfigureResource.getInstance().doParse(args, routeConfiguration);