Fixup: Request builder review fixes
diff --git a/xds/src/main/java/io/grpc/xds/internal/extauthz/CertificateUtils.java b/xds/src/main/java/io/grpc/xds/internal/extauthz/CertificateUtils.java index c025383..9d4ea4f 100644 --- a/xds/src/main/java/io/grpc/xds/internal/extauthz/CertificateUtils.java +++ b/xds/src/main/java/io/grpc/xds/internal/extauthz/CertificateUtils.java
@@ -30,7 +30,7 @@ /** * A utility class for certificate-related information. */ -public final class CertificateUtils { +final class CertificateUtils { private static final Logger logger = Logger.getLogger(CertificateUtils.class.getName()); // From RFC 5280, section 4.2.1.6, Subject Alternative Name // dNSName (2) @@ -48,21 +48,21 @@ * @param cert The certificate. * @return The principal. */ - public static String getPrincipal(X509Certificate cert) { + static String getPrincipal(X509Certificate cert) { try { Collection<List<?>> sans = cert.getSubjectAlternativeNames(); if (sans != null) { // Look for URI SAN (Priority 1). for (List<?> san : sans) { if (san.size() == 2 && san.get(0) instanceof Integer - && (Integer) san.get(0) == SAN_TYPE_URI) { + && san.get(0).equals(SAN_TYPE_URI)) { return (String) san.get(1); } } // If no URI SAN, look for DNS SAN (Priority 2). for (List<?> san : sans) { if (san.size() == 2 && san.get(0) instanceof Integer - && (Integer) san.get(0) == SAN_TYPE_DNS_NAME) { + && san.get(0).equals(SAN_TYPE_DNS_NAME)) { return (String) san.get(1); } } @@ -82,10 +82,10 @@ * @throws CertificateEncodingException If an error occurs while encoding the certificate. * @throws UnsupportedEncodingException If an error occurs while encoding the URL. */ - public static String getUrlPemEncodedCertificate(X509Certificate cert) + static String getUrlPemEncodedCertificate(X509Certificate cert) throws CertificateEncodingException, UnsupportedEncodingException { String pemCert = CertPemConverter.toPem(cert); - return URLEncoder.encode(pemCert, StandardCharsets.UTF_8.toString()); + return URLEncoder.encode(pemCert, StandardCharsets.UTF_8.name()); } /** @@ -106,7 +106,9 @@ * @throws CertificateEncodingException If an error occurs while encoding the certificate. */ public static String toPem(X509Certificate cert) throws CertificateEncodingException { - return X509_PEM_HEADER + BaseEncoding.base64().encode(cert.getEncoded()) + X509_PEM_FOOTER; + return X509_PEM_HEADER + + BaseEncoding.base64().withSeparator("\n", 64).encode(cert.getEncoded()) + + X509_PEM_FOOTER; } } }
diff --git a/xds/src/main/java/io/grpc/xds/internal/extauthz/CheckRequestBuilder.java b/xds/src/main/java/io/grpc/xds/internal/extauthz/CheckRequestBuilder.java index c16cabd..bb0eb5c 100644 --- a/xds/src/main/java/io/grpc/xds/internal/extauthz/CheckRequestBuilder.java +++ b/xds/src/main/java/io/grpc/xds/internal/extauthz/CheckRequestBuilder.java
@@ -18,8 +18,11 @@ import com.google.common.io.BaseEncoding; +import com.google.protobuf.ByteString; import com.google.protobuf.Timestamp; import io.envoyproxy.envoy.config.core.v3.Address; +import io.envoyproxy.envoy.config.core.v3.HeaderMap; +import io.envoyproxy.envoy.config.core.v3.HeaderValue; import io.envoyproxy.envoy.config.core.v3.SocketAddress; import io.envoyproxy.envoy.service.auth.v3.AttributeContext; import io.envoyproxy.envoy.service.auth.v3.CheckRequest; @@ -33,24 +36,24 @@ import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.List; import java.util.Locale; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; +import javax.annotation.concurrent.ThreadSafe; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; /** - * Interface for building external authorization check requests. + * Builds external authorization check requests from gRPC call metadata. */ +@ThreadSafe public class CheckRequestBuilder { /** * An interface for providing certificate-related information. */ - public interface CertificateProvider { + interface CertificateProvider { /** * Gets the principal from a certificate. * @@ -72,11 +75,17 @@ } private static final Logger logger = Logger.getLogger(CheckRequestBuilder.class.getName()); + private static final BaseEncoding BASE64_NO_PAD = BaseEncoding.base64().omitPadding(); private final ExtAuthzConfig config; private final CertificateProvider certificateProvider; + /** + * Constructs a new {@link CheckRequestBuilder} with the default certificate provider. + * + * @param config The external authorization configuration. + */ public CheckRequestBuilder(ExtAuthzConfig config) { this(config, new CertificateProvider() { @Override @@ -92,18 +101,40 @@ }); } - public CheckRequestBuilder(ExtAuthzConfig config, CertificateProvider certificateProvider) { + /** + * Constructs a new {@link CheckRequestBuilder} with a custom certificate provider. + * + * @param config The external authorization configuration. + * @param certificateProvider The certificate provider. + */ + CheckRequestBuilder(ExtAuthzConfig config, CertificateProvider certificateProvider) { this.config = config; this.certificateProvider = certificateProvider; } + /** + * Builds a check request for a client-side call. + * + * @param methodDescriptor The method descriptor of the RPC. + * @param headers The initial metadata headers. + * @param requestTime The timestamp when the request was initiated. + * @return The constructed {@link CheckRequest}. + */ public CheckRequest buildRequest(MethodDescriptor<?, ?> methodDescriptor, Metadata headers, Timestamp requestTime) { return build(methodDescriptor, headers, requestTime, null, null, null); } + /** + * Builds a check request for a server-side call. + * + * @param serverCall The server call. + * @param headers The initial metadata headers. + * @param requestTime The timestamp when the request was initiated. + * @return The constructed {@link CheckRequest}. + */ public CheckRequest buildRequest(ServerCall<?, ?> serverCall, Metadata headers, Timestamp requestTime) { java.net.SocketAddress localAddress = @@ -171,10 +202,24 @@ AttributeContext.Peer.Builder peerBuilder = AttributeContext.Peer.newBuilder(); if (socketAddress instanceof InetSocketAddress) { InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress; + // Prefer the resolved IP address, but fall back to the hostname string for + // unresolved addresses. In practice, Netty transports always provide resolved + // InetSocketAddress instances for active connections, and other gRPC + // implementations (C++, Go) always produce IP addresses because they operate + // on real TCP sockets. However, Envoy's address.proto permits hostnames (the + // only constraint is a non-empty string), so we gracefully fall back to + // getHostString() for robustness. See also TcpMetrics.java for precedent: + // https://github.com/grpc/grpc-java/blob/master/netty/src/main/java/io/grpc/netty/TcpMetrics.java + String address; + if (inetSocketAddress.getAddress() != null) { + address = inetSocketAddress.getAddress().getHostAddress(); + } else { + address = inetSocketAddress.getHostString(); + } peerBuilder .setAddress(Address.newBuilder() .setSocketAddress(SocketAddress.newBuilder() - .setAddress(inetSocketAddress.getAddress().getHostAddress()) + .setAddress(address) .setPortValue(inetSocketAddress.getPort())) .build()); } @@ -190,36 +235,55 @@ httpReqBuilder.setMethod("POST"); httpReqBuilder.setProtocol("HTTP/2"); httpReqBuilder.setSize(-1); + + HeaderMap.Builder headerMapBuilder = HeaderMap.newBuilder(); for (String key : headers.keys()) { if (!isAllowed(key)) { continue; } - String value; + String lowerCaseKey = key.toLowerCase(Locale.ROOT); if (key.endsWith(Metadata.BINARY_HEADER_SUFFIX)) { - value = getBinaryHeaderValue(headers, key); + populateBinaryHeaderValues(headers, key, lowerCaseKey, headerMapBuilder); } else { - value = getAsciiHeaderValue(headers, key); + populateAsciiHeaderValues(headers, key, lowerCaseKey, headerMapBuilder); } - httpReqBuilder.putHeaders(key.toLowerCase(Locale.ROOT), value); } + httpReqBuilder.setHeaderMap(headerMapBuilder); reqBuilder.setHttp(httpReqBuilder); return reqBuilder.build(); } - private String getBinaryHeaderValue(Metadata headers, String key) { + private void populateBinaryHeaderValues(Metadata headers, String key, String lowerCaseKey, + HeaderMap.Builder headerMapBuilder) { Iterable<byte[]> binaryValues = headers.getAll(Metadata.Key.of(key, Metadata.BINARY_BYTE_MARSHALLER)); - List<String> base64Values = new ArrayList<>(); - for (byte[] value : binaryValues) { - base64Values.add(BaseEncoding.base64().encode(value)); + if (binaryValues != null) { + for (byte[] value : binaryValues) { + // Binary header values are base64-encoded before storing in rawValue, + // matching Envoy's behavior for CheckRequest header serialization. + String base64Value = BASE64_NO_PAD.encode(value); + headerMapBuilder.addHeaders( + HeaderValue.newBuilder() + .setKey(lowerCaseKey) + .setRawValue(ByteString.copyFromUtf8(base64Value)) + .build()); + } } - return String.join(",", base64Values); } - private String getAsciiHeaderValue(Metadata headers, String key) { + private void populateAsciiHeaderValues(Metadata headers, String key, String lowerCaseKey, + HeaderMap.Builder headerMapBuilder) { Iterable<String> stringValues = headers.getAll(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER)); - return String.join(",", stringValues); + if (stringValues != null) { + for (String value : stringValues) { + headerMapBuilder.addHeaders( + HeaderValue.newBuilder() + .setKey(lowerCaseKey) + .setRawValue(ByteString.copyFromUtf8(value)) + .build()); + } + } } private boolean isAllowed(String header) {
diff --git a/xds/src/test/java/io/grpc/xds/internal/extauthz/CheckRequestBuilderTest.java b/xds/src/test/java/io/grpc/xds/internal/extauthz/CheckRequestBuilderTest.java index 8747e05..c205c57 100644 --- a/xds/src/test/java/io/grpc/xds/internal/extauthz/CheckRequestBuilderTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/extauthz/CheckRequestBuilderTest.java
@@ -20,30 +20,24 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import com.google.protobuf.Any; +import com.google.common.collect.ImmutableList; import com.google.protobuf.Timestamp; import io.envoyproxy.envoy.config.core.v3.Address; -import io.envoyproxy.envoy.config.core.v3.GrpcService; -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.config.core.v3.HeaderMap; import io.envoyproxy.envoy.service.auth.v3.AttributeContext; import io.envoyproxy.envoy.service.auth.v3.CheckRequest; -import io.envoyproxy.envoy.type.matcher.v3.ListStringMatcher; -import io.envoyproxy.envoy.type.matcher.v3.StringMatcher; import io.grpc.Attributes; import io.grpc.Grpc; import io.grpc.Metadata; import io.grpc.MethodDescriptor; import io.grpc.testing.TestMethodDescriptors; -import io.grpc.xds.ExtAuthzConfigParser; -import io.grpc.xds.client.Bootstrapper.BootstrapInfo; -import io.grpc.xds.client.CommonBootstrapperTestUtils; +import io.grpc.xds.internal.Matchers; +import io.grpc.xds.internal.extauthz.ExtAuthzTestHelper.TestServerCall; +import io.grpc.xds.internal.grpcservice.GrpcServiceConfig; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.security.cert.Certificate; import java.security.cert.X509Certificate; -import java.util.Collections; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; import org.junit.Before; @@ -105,12 +99,12 @@ headers.put(Metadata.Key.of("bin-header-bin", Metadata.BINARY_BYTE_MARSHALLER), binaryValue); // Configure CheckRequestBuilder to allow specific headers - ListStringMatcher allowedHeaders = ListStringMatcher.newBuilder() - .addPatterns(StringMatcher.newBuilder().setExact("allowed-header").build()) - .addPatterns(StringMatcher.newBuilder().setExact("overridden-header").build()).build(); - ListStringMatcher disallowedHeaders = ListStringMatcher.newBuilder() - .addPatterns(StringMatcher.newBuilder().setExact("disallowed-header").build()) - .addPatterns(StringMatcher.newBuilder().setExact("overridden-header").build()).build(); + ImmutableList<Matchers.StringMatcher> allowedHeaders = ImmutableList.of( + Matchers.StringMatcher.forExact("allowed-header", false), + Matchers.StringMatcher.forExact("overridden-header", false)); + ImmutableList<Matchers.StringMatcher> disallowedHeaders = ImmutableList.of( + Matchers.StringMatcher.forExact("disallowed-header", false), + Matchers.StringMatcher.forExact("overridden-header", false)); ExtAuthzConfig config = buildExtAuthzConfig(allowedHeaders, disallowedHeaders, true); checkRequestBuilder = new CheckRequestBuilder(config, certificateProvider); @@ -135,10 +129,9 @@ assertThat(attrContext.getDestination().getPrincipal()).isEqualTo("local-principal"); AttributeContext.HttpRequest http = attrContext.getRequest().getHttp(); - assertThat(http.getHeadersMap()).containsEntry("allowed-header", "v1"); - assertThat(http.getHeadersMap()).doesNotContainKey("bin-header-bin"); - assertThat(http.getHeadersMap()).doesNotContainKey("disallowed-header"); - assertThat(http.getHeadersMap()).doesNotContainKey("overridden-header"); + assertThat(http.getHeaderMap().getHeadersList()).containsExactly( + io.envoyproxy.envoy.config.core.v3.HeaderValue.newBuilder().setKey("allowed-header") + .setRawValue(com.google.protobuf.ByteString.copyFromUtf8("v1")).build()); } @Test @@ -155,7 +148,8 @@ assertThat(request.getAttributes().getRequest().getHttp().getMethod()).isEqualTo("POST"); assertThat(request.getAttributes().getRequest().getHttp().getProtocol()).isEqualTo("HTTP/2"); assertThat(request.getAttributes().getRequest().getHttp().getSize()).isEqualTo(-1); - assertThat(request.getAttributes().getRequest().getHttp().getHeadersMap()).isEmpty(); + assertThat(request.getAttributes().getRequest().getHttp().getHeaderMap().getHeadersList()) + .isEmpty(); assertThat(request.getAttributes().hasSource()).isFalse(); assertThat(request.getAttributes().hasDestination()).isFalse(); } @@ -170,9 +164,9 @@ headers.put(Metadata.Key.of("bin-header-bin", Metadata.BINARY_BYTE_MARSHALLER), binaryValue); // Configure CheckRequestBuilder with empty allowed headers - ListStringMatcher allowedHeaders = ListStringMatcher.newBuilder().build(); // empty - ListStringMatcher disallowedHeaders = ListStringMatcher.newBuilder() - .addPatterns(StringMatcher.newBuilder().setExact("disallowed-header").build()).build(); + ImmutableList<Matchers.StringMatcher> allowedHeaders = ImmutableList.of(); + ImmutableList<Matchers.StringMatcher> disallowedHeaders = ImmutableList.of( + Matchers.StringMatcher.forExact("disallowed-header", false)); ExtAuthzConfig config = buildExtAuthzConfig(allowedHeaders, disallowedHeaders, true); checkRequestBuilder = new CheckRequestBuilder(config, certificateProvider); @@ -187,9 +181,11 @@ AttributeContext.HttpRequest http = attrContext.getRequest().getHttp(); assertThat(http.getPath()).isEqualTo("/" + methodDescriptor.getFullMethodName()); - assertThat(http.getHeadersMap()).containsEntry("some-header", "v1"); - assertThat(http.getHeadersMap()).containsEntry("bin-header-bin", "AQID"); - assertThat(http.getHeadersMap()).doesNotContainKey("disallowed-header"); + assertThat(http.getHeaderMap().getHeadersList()).containsExactly( + io.envoyproxy.envoy.config.core.v3.HeaderValue.newBuilder().setKey("some-header") + .setRawValue(com.google.protobuf.ByteString.copyFromUtf8("v1")).build(), + io.envoyproxy.envoy.config.core.v3.HeaderValue.newBuilder().setKey("bin-header-bin") + .setRawValue(com.google.protobuf.ByteString.copyFromUtf8("AQID")).build()); } @Test @@ -237,8 +233,8 @@ @Test public void buildRequest_forServer_includePeerCertFalse() throws Exception { - ExtAuthzConfig config = buildExtAuthzConfig(ListStringMatcher.newBuilder().build(), - ListStringMatcher.newBuilder().build(), false); + ExtAuthzConfig config = buildExtAuthzConfig(ImmutableList.of(), + ImmutableList.of(), false); checkRequestBuilder = new CheckRequestBuilder(config, certificateProvider); SocketAddress remoteAddress = new InetSocketAddress("192.168.1.1", 12345); @@ -329,6 +325,33 @@ } @Test + public void buildRequest_forServer_unresolvedInetSocketAddress() { + SocketAddress localAddress = + InetSocketAddress.createUnresolved("local-hostname", 443); + SocketAddress remoteAddress = + InetSocketAddress.createUnresolved("remote-hostname", 8080); + Attributes attributes = + Attributes.newBuilder().set(Grpc.TRANSPORT_ATTR_LOCAL_ADDR, localAddress) + .set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, remoteAddress).build(); + serverCall = new TestServerCall<>(attributes, methodDescriptor); + + CheckRequest request = + checkRequestBuilder.buildRequest(serverCall, new Metadata(), requestTime); + + AttributeContext attrContext = request.getAttributes(); + assertThat(attrContext.hasSource()).isTrue(); + assertThat(attrContext.getSource().getAddress().getSocketAddress().getAddress()) + .isEqualTo("remote-hostname"); + assertThat(attrContext.getSource().getAddress().getSocketAddress().getPortValue()) + .isEqualTo(8080); + assertThat(attrContext.hasDestination()).isTrue(); + assertThat(attrContext.getDestination().getAddress().getSocketAddress().getAddress()) + .isEqualTo("local-hostname"); + assertThat(attrContext.getDestination().getAddress().getSocketAddress().getPortValue()) + .isEqualTo(443); + } + + @Test public void buildRequest_forServer_handlesCertificateEncodingException() throws Exception { SocketAddress localAddress = new InetSocketAddress("10.0.0.2", 443); SocketAddress remoteAddress = new InetSocketAddress("192.168.1.1", 12345); @@ -353,28 +376,105 @@ assertThat(source.getCertificate()).isEmpty(); } - private ExtAuthzConfig buildExtAuthzConfig() throws ExtAuthzParseException { - return buildExtAuthzConfig(ListStringMatcher.newBuilder().build(), - ListStringMatcher.newBuilder().build(), true); + @Test + public void buildRequest_forClient_allowedHeadersFiltering() { + Metadata headers = new Metadata(); + headers.put(Metadata.Key.of("allowed-header", Metadata.ASCII_STRING_MARSHALLER), "v1"); + headers.put(Metadata.Key.of("not-allowed-header", Metadata.ASCII_STRING_MARSHALLER), "v2"); + + // Configure with non-empty allowed headers — only "allowed-header" should pass + ImmutableList<Matchers.StringMatcher> allowedHeaders = ImmutableList.of( + Matchers.StringMatcher.forExact("allowed-header", false)); + ExtAuthzConfig config = buildExtAuthzConfig(allowedHeaders, ImmutableList.of(), true); + CheckRequestBuilder builder = new CheckRequestBuilder(config, certificateProvider); + + CheckRequest request = builder.buildRequest(methodDescriptor, headers, requestTime); + + HeaderMap headerMap = request.getAttributes().getRequest().getHttp().getHeaderMap(); + assertThat(headerMap.getHeadersList()).hasSize(1); + assertThat(headerMap.getHeadersList().get(0).getKey()).isEqualTo("allowed-header"); + assertThat(headerMap.getHeadersList().get(0).getRawValue().toStringUtf8()).isEqualTo("v1"); } - private ExtAuthzConfig buildExtAuthzConfig(ListStringMatcher allowed, - ListStringMatcher disallowed, boolean includePeerCertificate) throws ExtAuthzParseException { - Any googleDefaultChannelCreds = Any.pack(GoogleDefaultCredentials.newBuilder().build()); - Any fakeAccessTokenCreds = - Any.pack(AccessTokenCredentials.newBuilder().setToken("fake-token").build()); - ExtAuthz.Builder builder = ExtAuthz.newBuilder() - .setGrpcService(GrpcService.newBuilder() - .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() - .setTargetUri("test-cluster").addChannelCredentialsPlugin(googleDefaultChannelCreds) - .addCallCredentialsPlugin(fakeAccessTokenCreds).build()) - .build()) - .setIncludePeerCertificate(includePeerCertificate).setAllowedHeaders(allowed) - .setDisallowedHeaders(disallowed); - BootstrapInfo bootstrapInfo = - CommonBootstrapperTestUtils.buildBootStrap( - Collections.singletonList("test-cluster")); - return ExtAuthzConfigParser.parse( - builder.build(), bootstrapInfo, bootstrapInfo.servers().get(0)); + @Test + public void buildRequest_forClient_emptyMetadata() { + CheckRequest request = + checkRequestBuilder.buildRequest(methodDescriptor, new Metadata(), requestTime); + + AttributeContext attrContext = request.getAttributes(); + assertThat(attrContext.hasSource()).isFalse(); + assertThat(attrContext.hasDestination()).isFalse(); + assertThat(attrContext.getRequest().getHttp().getPath()) + .isEqualTo("/" + methodDescriptor.getFullMethodName()); + assertThat(attrContext.getRequest().getHttp().getMethod()).isEqualTo("POST"); + assertThat(attrContext.getRequest().getHttp().getProtocol()).isEqualTo("HTTP/2"); + assertThat(attrContext.getRequest().getHttp().getHeaderMap().getHeadersList()).isEmpty(); + } + + @Test + public void buildRequest_multiValuedHeaders() throws Exception { + Metadata headers = new Metadata(); + Metadata.Key<String> asciiKey = Metadata.Key.of("x-custom", Metadata.ASCII_STRING_MARSHALLER); + headers.put(asciiKey, "value1"); + headers.put(asciiKey, "value2"); + + Metadata.Key<byte[]> binaryKey = + Metadata.Key.of("x-custom-bin", Metadata.BINARY_BYTE_MARSHALLER); + headers.put(binaryKey, new byte[]{1, 2}); + headers.put(binaryKey, new byte[]{3, 4}); + + ExtAuthzConfig configWithAllowedHeaders = buildExtAuthzConfig( + ImmutableList.of(Matchers.StringMatcher.forExact("x-custom", false), + Matchers.StringMatcher.forExact("x-custom-bin", false)), + ImmutableList.of(), true); + CheckRequestBuilder builderWithConfig = + new CheckRequestBuilder(configWithAllowedHeaders, certificateProvider); + + CheckRequest request = + builderWithConfig.buildRequest(methodDescriptor, headers, requestTime); + + HeaderMap headerMap = request.getAttributes().getRequest().getHttp().getHeaderMap(); + assertThat(headerMap.getHeadersList()).hasSize(4); + assertThat(headerMap.getHeadersList().get(0).getKey()).isEqualTo("x-custom-bin"); + assertThat(headerMap.getHeadersList().get(0).getRawValue().toStringUtf8()).isEqualTo("AQI"); + assertThat(headerMap.getHeadersList().get(1).getKey()).isEqualTo("x-custom-bin"); + assertThat(headerMap.getHeadersList().get(1).getRawValue().toStringUtf8()).isEqualTo("AwQ"); + assertThat(headerMap.getHeadersList().get(2).getKey()).isEqualTo("x-custom"); + assertThat(headerMap.getHeadersList().get(2).getRawValue().toStringUtf8()).isEqualTo("value1"); + assertThat(headerMap.getHeadersList().get(3).getKey()).isEqualTo("x-custom"); + assertThat(headerMap.getHeadersList().get(3).getRawValue().toStringUtf8()).isEqualTo("value2"); + } + + private ExtAuthzConfig buildExtAuthzConfig() { + return buildExtAuthzConfig(ImmutableList.of(), ImmutableList.of(), true); + } + + private ExtAuthzConfig buildExtAuthzConfig( + ImmutableList<Matchers.StringMatcher> allowed, + ImmutableList<Matchers.StringMatcher> disallowed, + boolean includePeerCertificate) { + GrpcServiceConfig.GoogleGrpcConfig googleGrpc = GrpcServiceConfig.GoogleGrpcConfig.builder() + .target("test-cluster") + .configuredChannelCredentials(io.grpc.xds.client.ConfiguredChannelCredentials.create( + mock(io.grpc.ChannelCredentials.class), + mock(io.grpc.xds.client.ConfiguredChannelCredentials.ChannelCredsConfig.class))) + .build(); + + GrpcServiceConfig dummyServiceConfig = GrpcServiceConfig.builder() + .googleGrpc(googleGrpc) + .initialMetadata(ImmutableList.of()) + .build(); + + return ExtAuthzConfig.builder() + .grpcService(dummyServiceConfig) + .includePeerCertificate(includePeerCertificate) + .allowedHeaders(allowed) + .disallowedHeaders(disallowed) + .failureModeAllow(true) + .failureModeAllowHeaderAdd(false) + .denyAtDisable(false) + .filterEnabled(Matchers.FractionMatcher.create(100, 100)) + .statusOnError(io.grpc.Status.INTERNAL) + .build(); } }