diff --git a/authz/build.gradle b/authz/build.gradle
index ce327f14e1..50084752d0 100644
--- a/authz/build.gradle
+++ b/authz/build.gradle
@@ -33,12 +33,6 @@ tasks.named("jar").configure {
classifier = 'original'
}
-// TODO(ashithasantosh): Remove javadoc exclusion on adding authorization
-// interceptor implementations.
-tasks.named("javadoc").configure {
- exclude "io/grpc/authz/*"
-}
-
tasks.named("shadowJar").configure {
classifier = null
dependencies {
@@ -52,6 +46,12 @@ tasks.named("shadowJar").configure {
relocate 'com.google.api.expr', 'io.grpc.xds.shaded.com.google.api.expr'
}
+tasks.named("compileJava").configure {
+ it.options.compilerArgs += [
+ "-Xlint:-processing",
+ ]
+}
+
publishing {
publications {
maven(MavenPublication) {
diff --git a/authz/src/main/java/io/grpc/authz/AuthorizationServerInterceptor.java b/authz/src/main/java/io/grpc/authz/AuthorizationServerInterceptor.java
new file mode 100644
index 0000000000..2e39866609
--- /dev/null
+++ b/authz/src/main/java/io/grpc/authz/AuthorizationServerInterceptor.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2022 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.authz;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import io.envoyproxy.envoy.config.rbac.v3.RBAC;
+import io.grpc.ExperimentalApi;
+import io.grpc.InternalServerInterceptors;
+import io.grpc.Metadata;
+import io.grpc.ServerCall;
+import io.grpc.ServerCallHandler;
+import io.grpc.ServerInterceptor;
+import io.grpc.xds.InternalRbacFilter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Authorization server interceptor for static policy. The class will get
+ *
+ * gRPC Authorization policy as a JSON string during initialization.
+ * This policy will be translated to Envoy RBAC policies to make
+ * authorization decisions. The policy cannot be changed once created.
+ */
+@ExperimentalApi("https://github.com/grpc/grpc-java/issues/9746")
+public final class AuthorizationServerInterceptor implements ServerInterceptor {
+ private final List interceptors = new ArrayList<>();
+
+ private AuthorizationServerInterceptor(String authorizationPolicy)
+ throws IOException {
+ List rbacs = AuthorizationPolicyTranslator.translate(authorizationPolicy);
+ if (rbacs == null || rbacs.isEmpty() || rbacs.size() > 2) {
+ throw new IllegalArgumentException("Failed to translate authorization policy");
+ }
+ for (RBAC rbac: rbacs) {
+ interceptors.add(
+ InternalRbacFilter.createInterceptor(
+ io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBAC.newBuilder()
+ .setRules(rbac).build()));
+ }
+ }
+
+ @Override
+ public ServerCall.Listener interceptCall(
+ ServerCall call, Metadata headers,
+ ServerCallHandler next) {
+ for (ServerInterceptor interceptor: interceptors) {
+ next = InternalServerInterceptors.interceptCallHandlerCreate(interceptor, next);
+ }
+ return next.startCall(call, headers);
+ }
+
+ // Static method that creates an AuthorizationServerInterceptor.
+ public static AuthorizationServerInterceptor create(String authorizationPolicy)
+ throws IOException {
+ checkNotNull(authorizationPolicy, "authorizationPolicy");
+ return new AuthorizationServerInterceptor(authorizationPolicy);
+ }
+}
diff --git a/authz/src/test/java/io/grpc/authz/AuthorizationEnd2EndTest.java b/authz/src/test/java/io/grpc/authz/AuthorizationEnd2EndTest.java
new file mode 100644
index 0000000000..423c27bee5
--- /dev/null
+++ b/authz/src/test/java/io/grpc/authz/AuthorizationEnd2EndTest.java
@@ -0,0 +1,375 @@
+/*
+ * Copyright 2022 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.authz;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import io.grpc.ChannelCredentials;
+import io.grpc.Grpc;
+import io.grpc.InsecureChannelCredentials;
+import io.grpc.InsecureServerCredentials;
+import io.grpc.ManagedChannel;
+import io.grpc.Server;
+import io.grpc.ServerCredentials;
+import io.grpc.StatusRuntimeException;
+import io.grpc.TlsChannelCredentials;
+import io.grpc.TlsServerCredentials;
+import io.grpc.TlsServerCredentials.ClientAuth;
+import io.grpc.internal.testing.TestUtils;
+import io.grpc.stub.StreamObserver;
+import io.grpc.testing.protobuf.SimpleRequest;
+import io.grpc.testing.protobuf.SimpleResponse;
+import io.grpc.testing.protobuf.SimpleServiceGrpc;
+
+import java.io.File;
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class AuthorizationEnd2EndTest {
+ public static final String SERVER_0_KEY_FILE = "server0.key";
+ public static final String SERVER_0_PEM_FILE = "server0.pem";
+ public static final String CLIENT_0_KEY_FILE = "client.key";
+ public static final String CLIENT_0_PEM_FILE = "client.pem";
+ public static final String CA_PEM_FILE = "ca.pem";
+
+ private Server server;
+ private ManagedChannel channel;
+
+ private void initServerWithStaticAuthz(
+ String authorizationPolicy, ServerCredentials serverCredentials) throws Exception {
+ AuthorizationServerInterceptor authzInterceptor =
+ AuthorizationServerInterceptor.create(authorizationPolicy);
+ server = Grpc.newServerBuilderForPort(0, serverCredentials)
+ .addService(new SimpleServiceImpl())
+ .intercept(authzInterceptor)
+ .build()
+ .start();
+ }
+
+ private SimpleServiceGrpc.SimpleServiceBlockingStub getStub() {
+ channel =
+ Grpc.newChannelBuilderForAddress(
+ "localhost", server.getPort(), InsecureChannelCredentials.create())
+ .build();
+ return SimpleServiceGrpc.newBlockingStub(channel);
+ }
+
+ private SimpleServiceGrpc.SimpleServiceBlockingStub getStub(
+ ChannelCredentials channelCredentials) {
+ channel = Grpc.newChannelBuilderForAddress(
+ "localhost", server.getPort(), channelCredentials)
+ .overrideAuthority("foo.test.google.com.au")
+ .build();
+ return SimpleServiceGrpc.newBlockingStub(channel);
+ }
+
+ @After
+ public void tearDown() {
+ if (server != null) {
+ server.shutdown();
+ }
+ if (channel != null) {
+ channel.shutdown();
+ }
+ }
+
+ @Test
+ public void staticAuthzAllowsRpcNoMatchInDenyMatchInAllowTest() throws Exception {
+ String policy = "{"
+ + " \"name\" : \"authz\" ,"
+ + " \"deny_rules\": ["
+ + " {"
+ + " \"name\": \"deny_UnaryRpc\","
+ + " \"request\": {"
+ + " \"paths\": ["
+ + " \"*/UnaryRpc\""
+ + " ],"
+ + " \"headers\": ["
+ + " {"
+ + " \"key\": \"dev-path\","
+ + " \"values\": [\"/dev/path/*\"]"
+ + " }"
+ + " ]"
+ + " }"
+ + " }"
+ + " ],"
+ + " \"allow_rules\": ["
+ + " {"
+ + " \"name\": \"allow_all\""
+ + " }"
+ + " ]"
+ + "}";
+ initServerWithStaticAuthz(policy, InsecureServerCredentials.create());
+ getStub().unaryRpc(SimpleRequest.getDefaultInstance());
+ }
+
+ @Test
+ public void staticAuthzDeniesRpcNoMatchInDenyAndAllowTest() throws Exception {
+ String policy = "{"
+ + " \"name\" : \"authz\" ,"
+ + " \"deny_rules\": ["
+ + " {"
+ + " \"name\": \"deny_foo\","
+ + " \"source\": {"
+ + " \"principals\": ["
+ + " \"foo\""
+ + " ]"
+ + " }"
+ + " }"
+ + " ],"
+ + " \"allow_rules\": ["
+ + " {"
+ + " \"name\": \"allow_ClientStreamingRpc\","
+ + " \"request\": {"
+ + " \"paths\": ["
+ + " \"*/ClientStreamingRpc\""
+ + " ]"
+ + " }"
+ + " }"
+ + " ]"
+ + "}";
+ initServerWithStaticAuthz(policy, InsecureServerCredentials.create());
+ try {
+ getStub().unaryRpc(SimpleRequest.getDefaultInstance());
+ fail("exception expected");
+ } catch (StatusRuntimeException sre) {
+ assertThat(sre).hasMessageThat().isEqualTo(
+ "PERMISSION_DENIED: Access Denied");
+ } catch (Exception e) {
+ throw new AssertionError("the test failed ", e);
+ }
+ }
+
+ @Test
+ public void staticAuthzDeniesRpcMatchInDenyAndAllowTest() throws Exception {
+ String policy = "{"
+ + " \"name\" : \"authz\" ,"
+ + " \"deny_rules\": ["
+ + " {"
+ + " \"name\": \"deny_UnaryRpc\","
+ + " \"request\": {"
+ + " \"paths\": ["
+ + " \"*/UnaryRpc\""
+ + " ]"
+ + " }"
+ + " }"
+ + " ],"
+ + " \"allow_rules\": ["
+ + " {"
+ + " \"name\": \"allow_UnaryRpc\","
+ + " \"request\": {"
+ + " \"paths\": ["
+ + " \"*/UnaryRpc\""
+ + " ]"
+ + " }"
+ + " }"
+ + " ]"
+ + "}";
+ initServerWithStaticAuthz(policy, InsecureServerCredentials.create());
+ try {
+ getStub().unaryRpc(SimpleRequest.getDefaultInstance());
+ fail("exception expected");
+ } catch (StatusRuntimeException sre) {
+ assertThat(sre).hasMessageThat().isEqualTo(
+ "PERMISSION_DENIED: Access Denied");
+ } catch (Exception e) {
+ throw new AssertionError("the test failed ", e);
+ }
+ }
+
+ @Test
+ public void staticAuthzDeniesRpcMatchInDenyNoMatchInAllowTest() throws Exception {
+ String policy = "{"
+ + " \"name\" : \"authz\" ,"
+ + " \"deny_rules\": ["
+ + " {"
+ + " \"name\": \"deny_UnaryRpc\","
+ + " \"request\": {"
+ + " \"paths\": ["
+ + " \"*/UnaryRpc\""
+ + " ]"
+ + " }"
+ + " }"
+ + " ],"
+ + " \"allow_rules\": ["
+ + " {"
+ + " \"name\": \"allow_ClientStreamingRpc\","
+ + " \"request\": {"
+ + " \"paths\": ["
+ + " \"*/ClientStreamingRpc\""
+ + " ]"
+ + " }"
+ + " }"
+ + " ]"
+ + "}";
+ initServerWithStaticAuthz(policy, InsecureServerCredentials.create());
+ try {
+ getStub().unaryRpc(SimpleRequest.getDefaultInstance());
+ fail("exception expected");
+ } catch (StatusRuntimeException sre) {
+ assertThat(sre).hasMessageThat().isEqualTo(
+ "PERMISSION_DENIED: Access Denied");
+ } catch (Exception e) {
+ throw new AssertionError("the test failed ", e);
+ }
+ }
+
+ @Test
+ public void staticAuthzAllowsRpcEmptyDenyMatchInAllowTest() throws Exception {
+ String policy = "{"
+ + " \"name\" : \"authz\" ,"
+ + " \"allow_rules\": ["
+ + " {"
+ + " \"name\": \"allow_UnaryRpc\","
+ + " \"request\": {"
+ + " \"paths\": ["
+ + " \"*/UnaryRpc\""
+ + " ]"
+ + " }"
+ + " }"
+ + " ]"
+ + "}";
+ initServerWithStaticAuthz(policy, InsecureServerCredentials.create());
+ getStub().unaryRpc(SimpleRequest.getDefaultInstance());
+ }
+
+ @Test
+ public void staticAuthzDeniesRpcEmptyDenyNoMatchInAllowTest() throws Exception {
+ String policy = "{"
+ + " \"name\" : \"authz\" ,"
+ + " \"allow_rules\": ["
+ + " {"
+ + " \"name\": \"allow_ClientStreamingRpc\","
+ + " \"request\": {"
+ + " \"paths\": ["
+ + " \"*/ClientStreamingRpc\""
+ + " ]"
+ + " }"
+ + " }"
+ + " ]"
+ + "}";
+ initServerWithStaticAuthz(policy, InsecureServerCredentials.create());
+ try {
+ getStub().unaryRpc(SimpleRequest.getDefaultInstance());
+ fail("exception expected");
+ } catch (StatusRuntimeException sre) {
+ assertThat(sre).hasMessageThat().isEqualTo(
+ "PERMISSION_DENIED: Access Denied");
+ } catch (Exception e) {
+ throw new AssertionError("the test failed ", e);
+ }
+ }
+
+ @Test
+ public void staticAuthzDeniesRpcWithPrincipalsFieldOnUnauthenticatedConnectionTest()
+ throws Exception {
+ String policy = "{"
+ + " \"name\" : \"authz\" ,"
+ + " \"allow_rules\": ["
+ + " {"
+ + " \"name\": \"allow_authenticated\","
+ + " \"source\": {"
+ + " \"principals\": [\"*\", \"\"]"
+ + " }"
+ + " }"
+ + " ]"
+ + "}";
+ initServerWithStaticAuthz(policy, InsecureServerCredentials.create());
+ try {
+ getStub().unaryRpc(SimpleRequest.getDefaultInstance());
+ fail("exception expected");
+ } catch (StatusRuntimeException sre) {
+ assertThat(sre).hasMessageThat().isEqualTo(
+ "PERMISSION_DENIED: Access Denied");
+ } catch (Exception e) {
+ throw new AssertionError("the test failed ", e);
+ }
+ }
+
+ @Test
+ public void staticAuthzAllowsRpcWithPrincipalsFieldOnMtlsAuthenticatedConnectionTest()
+ throws Exception {
+ File caCertFile = TestUtils.loadCert(CA_PEM_FILE);
+ File serverKey0File = TestUtils.loadCert(SERVER_0_KEY_FILE);
+ File serverCert0File = TestUtils.loadCert(SERVER_0_PEM_FILE);
+ File clientKey0File = TestUtils.loadCert(CLIENT_0_KEY_FILE);
+ File clientCert0File = TestUtils.loadCert(CLIENT_0_PEM_FILE);
+ String policy = "{"
+ + " \"name\" : \"authz\" ,"
+ + " \"allow_rules\": ["
+ + " {"
+ + " \"name\": \"allow_mtls\","
+ + " \"source\": {"
+ + " \"principals\": [\"*\"]"
+ + " }"
+ + " }"
+ + " ]"
+ + "}";
+ ServerCredentials serverCredentials = TlsServerCredentials.newBuilder()
+ .keyManager(serverCert0File, serverKey0File)
+ .trustManager(caCertFile)
+ .clientAuth(ClientAuth.REQUIRE)
+ .build();
+ initServerWithStaticAuthz(policy, serverCredentials);
+ ChannelCredentials channelCredentials = TlsChannelCredentials.newBuilder()
+ .keyManager(clientCert0File, clientKey0File)
+ .trustManager(caCertFile)
+ .build();
+ getStub(channelCredentials).unaryRpc(SimpleRequest.getDefaultInstance());
+ }
+
+ @Test
+ public void staticAuthzAllowsRpcWithPrincipalsFieldOnTlsAuthenticatedConnectionTest()
+ throws Exception {
+ File caCertFile = TestUtils.loadCert(CA_PEM_FILE);
+ File serverKey0File = TestUtils.loadCert(SERVER_0_KEY_FILE);
+ File serverCert0File = TestUtils.loadCert(SERVER_0_PEM_FILE);
+ String policy = "{"
+ + " \"name\" : \"authz\" ,"
+ + " \"allow_rules\": ["
+ + " {"
+ + " \"name\": \"allow_tls\","
+ + " \"source\": {"
+ + " \"principals\": [\"\"]"
+ + " }"
+ + " }"
+ + " ]"
+ + "}";
+ ServerCredentials serverCredentials = TlsServerCredentials.newBuilder()
+ .keyManager(serverCert0File, serverKey0File)
+ .trustManager(caCertFile)
+ .clientAuth(ClientAuth.OPTIONAL)
+ .build();
+ initServerWithStaticAuthz(policy, serverCredentials);
+ ChannelCredentials channelCredentials = TlsChannelCredentials.newBuilder()
+ .trustManager(caCertFile)
+ .build();
+ getStub(channelCredentials).unaryRpc(SimpleRequest.getDefaultInstance());
+ }
+
+ private static class SimpleServiceImpl extends SimpleServiceGrpc.SimpleServiceImplBase {
+ @Override
+ public void unaryRpc(SimpleRequest req, StreamObserver respOb) {
+ respOb.onNext(SimpleResponse.getDefaultInstance());
+ respOb.onCompleted();
+ }
+ }
+}
diff --git a/authz/src/test/java/io/grpc/authz/AuthorizationServerInterceptorTest.java b/authz/src/test/java/io/grpc/authz/AuthorizationServerInterceptorTest.java
new file mode 100644
index 0000000000..990228e2c9
--- /dev/null
+++ b/authz/src/test/java/io/grpc/authz/AuthorizationServerInterceptorTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2022 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.authz;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+
+@RunWith(JUnit4.class)
+public class AuthorizationServerInterceptorTest {
+ @Test
+ public void invalidPolicyFailsStaticAuthzInterceptorCreation() throws Exception {
+ String policy = "{ \"name\": \"abc\",, }";
+ try {
+ AuthorizationServerInterceptor.create(policy);
+ fail("exception expected");
+ } catch (IOException ioe) {
+ assertThat(ioe).hasMessageThat().isEqualTo(
+ "Use JsonReader.setLenient(true) to accept malformed JSON"
+ + " at line 1 column 18 path $.name");
+ } catch (Exception e) {
+ throw new AssertionError("the test failed ", e);
+ }
+ }
+
+ @Test
+ public void validPolicyCreatesStaticAuthzInterceptor() throws Exception {
+ String policy = "{"
+ + " \"name\" : \"authz\","
+ + " \"deny_rules\": ["
+ + " {"
+ + " \"name\": \"deny_foo\","
+ + " \"source\": {"
+ + " \"principals\": ["
+ + " \"spiffe://foo.com\""
+ + " ]"
+ + " }"
+ + " }"
+ + " ],"
+ + " \"allow_rules\": ["
+ + " {"
+ + " \"name\": \"allow_all\""
+ + " }"
+ + " ]"
+ + "}";
+ assertNotNull(AuthorizationServerInterceptor.create(policy));
+ }
+}
diff --git a/xds/src/main/java/io/grpc/xds/InternalRbacFilter.java b/xds/src/main/java/io/grpc/xds/InternalRbacFilter.java
new file mode 100644
index 0000000000..54e6c748cd
--- /dev/null
+++ b/xds/src/main/java/io/grpc/xds/InternalRbacFilter.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2021 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.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBAC;
+import io.grpc.Internal;
+import io.grpc.ServerInterceptor;
+import io.grpc.xds.RbacConfig;
+import io.grpc.xds.RbacFilter;
+
+/** This class exposes some functionality in RbacFilter to other packages. */
+@Internal
+public final class InternalRbacFilter {
+
+ private InternalRbacFilter() {}
+
+ /** Parses RBAC filter config and creates AuthorizationServerInterceptor. */
+ public static ServerInterceptor createInterceptor(RBAC rbac) {
+ ConfigOrError filterConfig = RbacFilter.parseRbacConfig(rbac);
+ if (filterConfig.errorDetail != null) {
+ throw new IllegalArgumentException(
+ String.format("Failed to parse Rbac policy: %s", filterConfig.errorDetail));
+ }
+ return new RbacFilter().buildServerInterceptor(filterConfig.config, null);
+ }
+}
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 0a971655df..39b80bbcc0 100644
--- a/xds/src/main/java/io/grpc/xds/internal/MatcherParser.java
+++ b/xds/src/main/java/io/grpc/xds/internal/MatcherParser.java
@@ -54,6 +54,12 @@ public final class MatcherParser {
case SUFFIX_MATCH:
return Matchers.HeaderMatcher.forSuffix(
proto.getName(), proto.getSuffixMatch(), proto.getInvertMatch());
+ case CONTAINS_MATCH:
+ return Matchers.HeaderMatcher.forContains(
+ proto.getName(), proto.getContainsMatch(), proto.getInvertMatch());
+ case STRING_MATCH:
+ return Matchers.HeaderMatcher.forString(
+ proto.getName(), parseStringMatcher(proto.getStringMatch()), proto.getInvertMatch());
case HEADERMATCHSPECIFIER_NOT_SET:
default:
throw new IllegalArgumentException(
diff --git a/xds/src/main/java/io/grpc/xds/internal/Matchers.java b/xds/src/main/java/io/grpc/xds/internal/Matchers.java
index 3bf7b7723e..f833fd2e48 100644
--- a/xds/src/main/java/io/grpc/xds/internal/Matchers.java
+++ b/xds/src/main/java/io/grpc/xds/internal/Matchers.java
@@ -62,6 +62,14 @@ public final class Matchers {
@Nullable
public abstract String suffix();
+ // Matches header value with the substring.
+ @Nullable
+ public abstract String contains();
+
+ // Matches header value with the string matcher.
+ @Nullable
+ public abstract StringMatcher stringMatcher();
+
// Whether the matching semantics is inverted. E.g., present && !inverted -> !present
public abstract boolean inverted();
@@ -69,50 +77,71 @@ public final class Matchers {
public static HeaderMatcher forExactValue(String name, String exactValue, boolean inverted) {
checkNotNull(name, "name");
checkNotNull(exactValue, "exactValue");
- return HeaderMatcher.create(name, exactValue, null, null, null, null, null, inverted);
+ return HeaderMatcher.create(
+ name, exactValue, null, null, null, null, null, null, null, inverted);
}
/** The request header value should match the regular expression pattern. */
public static HeaderMatcher forSafeRegEx(String name, Pattern safeRegEx, boolean inverted) {
checkNotNull(name, "name");
checkNotNull(safeRegEx, "safeRegEx");
- return HeaderMatcher.create(name, null, safeRegEx, null, null, null, null, inverted);
+ return HeaderMatcher.create(
+ name, null, safeRegEx, null, null, null, null, null, null, inverted);
}
/** The request header value should be within the range. */
public static HeaderMatcher forRange(String name, Range range, boolean inverted) {
checkNotNull(name, "name");
checkNotNull(range, "range");
- return HeaderMatcher.create(name, null, null, range, null, null, null, inverted);
+ return HeaderMatcher.create(name, null, null, range, null, null, null, null, null, inverted);
}
/** The request header value should exist. */
public static HeaderMatcher forPresent(String name, boolean present, boolean inverted) {
checkNotNull(name, "name");
- return HeaderMatcher.create(name, null, null, null, present, null, null, inverted);
+ return HeaderMatcher.create(
+ name, null, null, null, present, null, null, null, null, inverted);
}
/** The request header value should have this prefix. */
public static HeaderMatcher forPrefix(String name, String prefix, boolean inverted) {
checkNotNull(name, "name");
checkNotNull(prefix, "prefix");
- return HeaderMatcher.create(name, null, null, null, null, prefix, null, inverted);
+ return HeaderMatcher.create(name, null, null, null, null, prefix, null, null, null, inverted);
}
/** The request header value should have this suffix. */
public static HeaderMatcher forSuffix(String name, String suffix, boolean inverted) {
checkNotNull(name, "name");
checkNotNull(suffix, "suffix");
- return HeaderMatcher.create(name, null, null, null, null, null, suffix, inverted);
+ return HeaderMatcher.create(name, null, null, null, null, null, suffix, null, null, inverted);
+ }
+
+ /** The request header value should have this substring. */
+ public static HeaderMatcher forContains(String name, String contains, boolean inverted) {
+ checkNotNull(name, "name");
+ checkNotNull(contains, "contains");
+ return HeaderMatcher.create(
+ name, null, null, null, null, null, null, contains, null, inverted);
+ }
+
+ /** The request header value should match this stringMatcher. */
+ public static HeaderMatcher forString(
+ String name, StringMatcher stringMatcher, boolean inverted) {
+ checkNotNull(name, "name");
+ checkNotNull(stringMatcher, "stringMatcher");
+ return HeaderMatcher.create(
+ name, null, null, null, null, null, null, null, stringMatcher, inverted);
}
private static HeaderMatcher create(String name, @Nullable String exactValue,
@Nullable Pattern safeRegEx, @Nullable Range range,
@Nullable Boolean present, @Nullable String prefix,
- @Nullable String suffix, boolean inverted) {
+ @Nullable String suffix, @Nullable String contains,
+ @Nullable StringMatcher stringMatcher, boolean inverted) {
checkNotNull(name, "name");
return new AutoValue_Matchers_HeaderMatcher(name, exactValue, safeRegEx, range, present,
- prefix, suffix, inverted);
+ prefix, suffix, contains, stringMatcher, inverted);
}
/** Returns the matching result. */
@@ -138,8 +167,12 @@ public final class Matchers {
baseMatch = value.startsWith(prefix());
} else if (present() != null) {
baseMatch = present();
- } else {
+ } else if (suffix() != null) {
baseMatch = value.endsWith(suffix());
+ } else if (contains() != null) {
+ baseMatch = value.contains(contains());
+ } else {
+ baseMatch = stringMatcher().matches(value);
}
return baseMatch != inverted();
}
diff --git a/xds/src/test/java/io/grpc/xds/internal/MatcherTest.java b/xds/src/test/java/io/grpc/xds/internal/MatcherTest.java
index 93a9b7087d..4e5d278d50 100644
--- a/xds/src/test/java/io/grpc/xds/internal/MatcherTest.java
+++ b/xds/src/test/java/io/grpc/xds/internal/MatcherTest.java
@@ -163,6 +163,15 @@ public class MatcherTest {
assertThat(matcher.matches("1v2")).isFalse();
assertThat(matcher.matches(null)).isFalse();
+ matcher = HeaderMatcher.forContains("version", "v1", false);
+ assertThat(matcher.matches("xv1")).isTrue();
+ assertThat(matcher.matches("1vx")).isFalse();
+ assertThat(matcher.matches(null)).isFalse();
+ matcher = HeaderMatcher.forContains("version", "v1", true);
+ assertThat(matcher.matches("xv1")).isFalse();
+ assertThat(matcher.matches("1vx")).isTrue();
+ assertThat(matcher.matches(null)).isFalse();
+
matcher = HeaderMatcher.forSafeRegEx("version", Pattern.compile("v2.*"), false);
assertThat(matcher.matches("v2..")).isTrue();
assertThat(matcher.matches("v1")).isFalse();
@@ -180,5 +189,14 @@ public class MatcherTest {
assertThat(matcher.matches("1")).isTrue();
assertThat(matcher.matches("8080")).isFalse();
assertThat(matcher.matches(null)).isFalse();
+
+ matcher = HeaderMatcher.forString("version", StringMatcher.forExact("v1", true), false);
+ assertThat(matcher.matches("v1")).isTrue();
+ assertThat(matcher.matches("v1x")).isFalse();
+ assertThat(matcher.matches(null)).isFalse();
+ matcher = HeaderMatcher.forString("version", StringMatcher.forExact("v1", true), true);
+ assertThat(matcher.matches("v1x")).isTrue();
+ assertThat(matcher.matches("v1")).isFalse();
+ assertThat(matcher.matches(null)).isFalse();
}
}