Static authorization server interceptor implementation (#8934)

* Static authorization server interceptor implementation

* Resolving comments

* Remove RbacParser file

* update error logs

* checkstyle fixes

* Add InternalRbacFilter

* formatting

* javadoc

* format test file

* resolving comments

* minor formatting

* Update comment
This commit is contained in:
Ashitha Santhosh 2022-12-21 15:30:42 -08:00 committed by GitHub
parent 530cf905b1
commit 0194ae9a41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 629 additions and 15 deletions

View File

@ -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) {

View File

@ -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
* <a href="https://github.com/grpc/proposal/blob/master/A43-grpc-authorization-api.md#user-facing-authorization-policy">
* gRPC Authorization policy</a> 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<ServerInterceptor> interceptors = new ArrayList<>();
private AuthorizationServerInterceptor(String authorizationPolicy)
throws IOException {
List<RBAC> 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 <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call, Metadata headers,
ServerCallHandler<ReqT, RespT> 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);
}
}

View File

@ -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<SimpleResponse> respOb) {
respOb.onNext(SimpleResponse.getDefaultInstance());
respOb.onCompleted();
}
}
}

View File

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

View File

@ -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<RbacConfig> 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);
}
}

View File

@ -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(

View File

@ -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();
}

View File

@ -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();
}
}