xds: add rbac http filter (#8251)

This commit is contained in:
yifeizhuang 2021-06-14 12:54:07 -07:00 committed by GitHub
parent 2cbc7fc3a5
commit c8ba601529
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 820 additions and 11 deletions

View File

@ -65,8 +65,9 @@ interface Filter {
ScheduledExecutorService scheduler);
}
// Server side filters are not currently supported, but this interface is defined for clarity.
/** Uses the FilterConfigs produced above to produce an HTTP filter interceptor for the server. */
interface ServerInterceptorBuilder {
@Nullable
ServerInterceptor buildServerInterceptor(
FilterConfig config, @Nullable FilterConfig overrideConfig);
}

View File

@ -34,7 +34,10 @@ final class FilterRegistry {
static synchronized FilterRegistry getDefaultRegistry() {
if (instance == null) {
instance = newRegistry().register(FaultFilter.INSTANCE, RouterFilter.INSTANCE);
instance = newRegistry().register(
FaultFilter.INSTANCE,
RouterFilter.INSTANCE,
RbacFilter.INSTANCE);
}
return instance;
}

View File

@ -0,0 +1,38 @@
/*
* 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 com.google.auto.value.AutoValue;
import io.grpc.xds.Filter.FilterConfig;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AuthConfig;
import javax.annotation.Nullable;
/** Rbac configuration for Rbac filter. */
@AutoValue
abstract class RbacConfig implements FilterConfig {
@Override
public final String typeUrl() {
return RbacFilter.TYPE_URL;
}
@Nullable
abstract AuthConfig authConfig();
static RbacConfig create(@Nullable AuthConfig authConfig) {
return new AutoValue_RbacConfig(authConfig);
}
}

View File

@ -0,0 +1,322 @@
/*
* 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 static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.annotations.VisibleForTesting;
import com.google.protobuf.Any;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Message;
import io.envoyproxy.envoy.config.core.v3.CidrRange;
import io.envoyproxy.envoy.config.rbac.v3.Permission;
import io.envoyproxy.envoy.config.rbac.v3.Policy;
import io.envoyproxy.envoy.config.rbac.v3.Principal;
import io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBAC;
import io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBACPerRoute;
import io.grpc.Metadata;
import io.grpc.ServerCall;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
import io.grpc.Status;
import io.grpc.xds.Filter.ServerInterceptorBuilder;
import io.grpc.xds.internal.MatcherParser;
import io.grpc.xds.internal.Matchers;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AlwaysTrueMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AndMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AuthConfig;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AuthDecision;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AuthHeaderMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AuthenticatedMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.DestinationIpMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.DestinationPortMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.InvertMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.Matcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.OrMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.PathMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.PolicyMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.RequestedServerNameMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.SourceIpMatcher;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
/** RBAC Http filter implementation. */
final class RbacFilter implements Filter, ServerInterceptorBuilder {
private static final Logger logger = Logger.getLogger(RbacFilter.class.getName());
static final RbacFilter INSTANCE = new RbacFilter();
static final String TYPE_URL =
"type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC";
RbacFilter() {}
@Override
public String[] typeUrls() {
return new String[] { TYPE_URL };
}
@Override
public ConfigOrError<RbacConfig> parseFilterConfig(Message rawProtoMessage) {
RBAC rbacProto;
if (!(rawProtoMessage instanceof Any)) {
return ConfigOrError.fromError("Invalid config type: " + rawProtoMessage.getClass());
}
Any anyMessage = (Any) rawProtoMessage;
try {
rbacProto = anyMessage.unpack(RBAC.class);
} catch (InvalidProtocolBufferException e) {
return ConfigOrError.fromError("Invalid proto: " + e);
}
return parseRbacConfig(rbacProto);
}
@VisibleForTesting
static ConfigOrError<RbacConfig> parseRbacConfig(RBAC rbac) {
if (!rbac.hasRules()) {
return ConfigOrError.fromConfig(RbacConfig.create(null));
}
io.envoyproxy.envoy.config.rbac.v3.RBAC rbacConfig = rbac.getRules();
GrpcAuthorizationEngine.Action authAction;
switch (rbacConfig.getAction()) {
case ALLOW:
authAction = GrpcAuthorizationEngine.Action.ALLOW;
break;
case DENY:
authAction = GrpcAuthorizationEngine.Action.DENY;
break;
case LOG:
return ConfigOrError.fromConfig(RbacConfig.create(null));
case UNRECOGNIZED:
default:
return ConfigOrError.fromError("Unknown rbacConfig action type: " + rbacConfig.getAction());
}
Map<String, Policy> policyMap = rbacConfig.getPoliciesMap();
List<GrpcAuthorizationEngine.PolicyMatcher> policyMatchers = new ArrayList<>();
for (Map.Entry<String, Policy> entry: policyMap.entrySet()) {
try {
Policy policy = entry.getValue();
if (policy.hasCondition() || policy.hasCheckedCondition()) {
return ConfigOrError.fromError(
"Policy.condition and Policy.checked_condition must not set: " + entry.getKey());
}
policyMatchers.add(new PolicyMatcher(entry.getKey(),
parsePermissionList(policy.getPermissionsList()),
parsePrincipalList(policy.getPrincipalsList())));
} catch (Exception e) {
return ConfigOrError.fromError("Encountered error parsing policy: " + e);
}
}
return ConfigOrError.fromConfig(RbacConfig.create(new AuthConfig(policyMatchers, authAction)));
}
@Override
public ConfigOrError<RbacConfig> parseFilterConfigOverride(Message rawProtoMessage) {
RBACPerRoute rbacPerRoute;
if (!(rawProtoMessage instanceof Any)) {
return ConfigOrError.fromError("Invalid config type: " + rawProtoMessage.getClass());
}
Any anyMessage = (Any) rawProtoMessage;
try {
rbacPerRoute = anyMessage.unpack(RBACPerRoute.class);
} catch (InvalidProtocolBufferException e) {
return ConfigOrError.fromError("Invalid proto: " + e);
}
if (rbacPerRoute.hasRbac()) {
return parseRbacConfig(rbacPerRoute.getRbac());
} else {
return ConfigOrError.fromConfig(RbacConfig.create(null));
}
}
@Nullable
@Override
public ServerInterceptor buildServerInterceptor(FilterConfig config,
@Nullable FilterConfig overrideConfig) {
checkNotNull(config, "config");
if (overrideConfig != null) {
config = overrideConfig;
}
AuthConfig authConfig = ((RbacConfig) config).authConfig();
return authConfig == null ? null : generateAuthorizationInterceptor(authConfig);
}
private ServerInterceptor generateAuthorizationInterceptor(AuthConfig config) {
checkNotNull(config, "config");
final GrpcAuthorizationEngine authEngine = new GrpcAuthorizationEngine(config);
return new ServerInterceptor() {
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
final ServerCall<ReqT, RespT> call,
final Metadata headers, ServerCallHandler<ReqT, RespT> next) {
AuthDecision authResult = authEngine.evaluate(headers, call);
logger.log(Level.FINE,
"Authorization result for serverCall {0}: {1}, matching policy: {2}.",
new Object[]{call, authResult.decision(), authResult.matchingPolicyName()});
if (GrpcAuthorizationEngine.Action.DENY.equals(authResult.decision())) {
Status status = Status.UNAUTHENTICATED.withDescription(
"Access Denied, matching policy: " + authResult.matchingPolicyName());
call.close(status, new Metadata());
return new ServerCall.Listener<ReqT>(){};
}
return next.startCall(call, headers);
}
};
}
private static OrMatcher parsePermissionList(List<Permission> permissions) {
List<Matcher> anyMatch = new ArrayList<>();
for (Permission permission : permissions) {
anyMatch.add(parsePermission(permission));
}
return new OrMatcher(anyMatch);
}
private static Matcher parsePermission(Permission permission) {
switch (permission.getRuleCase()) {
case AND_RULES:
List<Matcher> andMatch = new ArrayList<>();
for (Permission p : permission.getAndRules().getRulesList()) {
andMatch.add(parsePermission(p));
}
return new AndMatcher(andMatch);
case OR_RULES:
return parsePermissionList(permission.getOrRules().getRulesList());
case ANY:
return AlwaysTrueMatcher.INSTANCE;
case HEADER:
return parseHeaderMatcher(permission.getHeader());
case URL_PATH:
return parsePathMatcher(permission.getUrlPath());
case DESTINATION_IP:
return createDestinationIpMatcher(permission.getDestinationIp());
case DESTINATION_PORT:
return createDestinationPortMatcher(permission.getDestinationPort());
case NOT_RULE:
return new InvertMatcher(parsePermission(permission.getNotRule()));
case METADATA: // hard coded, never match.
return new InvertMatcher(AlwaysTrueMatcher.INSTANCE);
case REQUESTED_SERVER_NAME:
return parseRequestedServerNameMatcher(permission.getRequestedServerName());
case RULE_NOT_SET:
default:
throw new IllegalArgumentException(
"Unknown permission rule case: " + permission.getRuleCase());
}
}
private static OrMatcher parsePrincipalList(List<Principal> principals) {
List<Matcher> anyMatch = new ArrayList<>();
for (Principal principal: principals) {
anyMatch.add(parsePrincipal(principal));
}
return new OrMatcher(anyMatch);
}
private static Matcher parsePrincipal(Principal principal) {
switch (principal.getIdentifierCase()) {
case OR_IDS:
return parsePrincipalList(principal.getOrIds().getIdsList());
case AND_IDS:
List<Matcher> nextMatchers = new ArrayList<>();
for (Principal next : principal.getAndIds().getIdsList()) {
nextMatchers.add(parsePrincipal(next));
}
return new AndMatcher(nextMatchers);
case ANY:
return AlwaysTrueMatcher.INSTANCE;
case AUTHENTICATED:
return parseAuthenticatedMatcher(principal.getAuthenticated());
case DIRECT_REMOTE_IP:
return createSourceIpMatcher(principal.getDirectRemoteIp());
case REMOTE_IP:
return createSourceIpMatcher(principal.getRemoteIp());
case SOURCE_IP:
return createSourceIpMatcher(principal.getSourceIp());
case HEADER:
return parseHeaderMatcher(principal.getHeader());
case NOT_ID:
return new InvertMatcher(parsePrincipal(principal.getNotId()));
case URL_PATH:
return parsePathMatcher(principal.getUrlPath());
case METADATA: // hard coded, never match.
return new InvertMatcher(AlwaysTrueMatcher.INSTANCE);
case IDENTIFIER_NOT_SET:
default:
throw new IllegalArgumentException(
"Unknown principal identifier case: " + principal.getIdentifierCase());
}
}
private static PathMatcher parsePathMatcher(
io.envoyproxy.envoy.type.matcher.v3.PathMatcher proto) {
switch (proto.getRuleCase()) {
case PATH:
return new PathMatcher(MatcherParser.parseStringMatcher(proto.getPath()));
case RULE_NOT_SET:
default:
throw new IllegalArgumentException(
"Unknown path matcher rule type: " + proto.getRuleCase());
}
}
private static RequestedServerNameMatcher parseRequestedServerNameMatcher(
io.envoyproxy.envoy.type.matcher.v3.StringMatcher proto) {
return new RequestedServerNameMatcher(MatcherParser.parseStringMatcher(proto));
}
private static AuthHeaderMatcher parseHeaderMatcher(
io.envoyproxy.envoy.config.route.v3.HeaderMatcher proto) {
return new AuthHeaderMatcher(MatcherParser.parseHeaderMatcher(proto));
}
private static AuthenticatedMatcher parseAuthenticatedMatcher(
Principal.Authenticated proto) {
Matchers.StringMatcher matcher = MatcherParser.parseStringMatcher(proto.getPrincipalName());
return new AuthenticatedMatcher(matcher);
}
private static DestinationPortMatcher createDestinationPortMatcher(int port) {
return new DestinationPortMatcher(port);
}
private static DestinationIpMatcher createDestinationIpMatcher(CidrRange cidrRange) {
return new DestinationIpMatcher(Matchers.CidrMatcher.create(
resolve(cidrRange), cidrRange.getPrefixLen().getValue()));
}
private static SourceIpMatcher createSourceIpMatcher(CidrRange cidrRange) {
return new SourceIpMatcher(Matchers.CidrMatcher.create(
resolve(cidrRange), cidrRange.getPrefixLen().getValue()));
}
private static InetAddress resolve(CidrRange cidrRange) {
try {
return InetAddress.getByName(cidrRange.getAddressPrefix());
} catch (UnknownHostException ex) {
throw new IllegalArgumentException("IP address can not be found: " + ex);
}
}
}

View File

@ -0,0 +1,85 @@
/*
* 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.internal;
import com.google.re2j.Pattern;
import com.google.re2j.PatternSyntaxException;
// TODO(zivy@): may reuse common matchers parsers.
public final class MatcherParser {
/** Translates envoy proto HeaderMatcher to internal HeaderMatcher.*/
public static Matchers.HeaderMatcher parseHeaderMatcher(
io.envoyproxy.envoy.config.route.v3.HeaderMatcher proto) {
switch (proto.getHeaderMatchSpecifierCase()) {
case EXACT_MATCH:
return Matchers.HeaderMatcher.forExactValue(
proto.getName(), proto.getExactMatch(), proto.getInvertMatch());
case SAFE_REGEX_MATCH:
String rawPattern = proto.getSafeRegexMatch().getRegex();
Pattern safeRegExMatch;
try {
safeRegExMatch = Pattern.compile(rawPattern);
} catch (PatternSyntaxException e) {
throw new IllegalArgumentException(
"HeaderMatcher [" + proto.getName() + "] contains malformed safe regex pattern: "
+ e.getMessage());
}
return Matchers.HeaderMatcher.forSafeRegEx(
proto.getName(), safeRegExMatch, proto.getInvertMatch());
case RANGE_MATCH:
Matchers.HeaderMatcher.Range rangeMatch = Matchers.HeaderMatcher.Range.create(
proto.getRangeMatch().getStart(), proto.getRangeMatch().getEnd());
return Matchers.HeaderMatcher.forRange(
proto.getName(), rangeMatch, proto.getInvertMatch());
case PRESENT_MATCH:
return Matchers.HeaderMatcher.forPresent(
proto.getName(), proto.getPresentMatch(), proto.getInvertMatch());
case PREFIX_MATCH:
return Matchers.HeaderMatcher.forPrefix(
proto.getName(), proto.getPrefixMatch(), proto.getInvertMatch());
case SUFFIX_MATCH:
return Matchers.HeaderMatcher.forSuffix(
proto.getName(), proto.getSuffixMatch(), proto.getInvertMatch());
case HEADERMATCHSPECIFIER_NOT_SET:
default:
throw new IllegalArgumentException(
"Unknown header matcher type: " + proto.getHeaderMatchSpecifierCase());
}
}
/** Translate StringMatcher envoy proto to internal StringMatcher. */
public static Matchers.StringMatcher parseStringMatcher(
io.envoyproxy.envoy.type.matcher.v3.StringMatcher proto) {
switch (proto.getMatchPatternCase()) {
case EXACT:
return Matchers.StringMatcher.forExact(proto.getExact(), proto.getIgnoreCase());
case PREFIX:
return Matchers.StringMatcher.forPrefix(proto.getPrefix(), proto.getIgnoreCase());
case SUFFIX:
return Matchers.StringMatcher.forSuffix(proto.getSuffix(), proto.getIgnoreCase());
case SAFE_REGEX:
return Matchers.StringMatcher.forSafeRegEx(
Pattern.compile(proto.getSafeRegex().getRegex()));
case CONTAINS:
return Matchers.StringMatcher.forContains(proto.getContains());
case MATCHPATTERN_NOT_SET:
default:
throw new IllegalArgumentException(
"Unknown StringMatcher match pattern: " + proto.getMatchPatternCase());
}
}
}

View File

@ -208,10 +208,10 @@ public final class GrpcAuthorizationEngine {
}
}
public static final class HeaderMatcher implements Matcher {
public static final class AuthHeaderMatcher implements Matcher {
private final Matchers.HeaderMatcher delegate;
public HeaderMatcher(Matchers.HeaderMatcher delegate) {
public AuthHeaderMatcher(Matchers.HeaderMatcher delegate) {
this.delegate = checkNotNull(delegate, "delegate");
}
@ -234,6 +234,19 @@ public final class GrpcAuthorizationEngine {
}
}
public static final class RequestedServerNameMatcher implements Matcher {
private final Matchers.StringMatcher delegate;
public RequestedServerNameMatcher(Matchers.StringMatcher delegate) {
this.delegate = checkNotNull(delegate, "delegate");
}
@Override
public boolean matches(EvaluateArgs args) {
return delegate.matches(args.getRequestedServerName());
}
}
private static final class EvaluateArgs {
private final Metadata metadata;
private final ServerCall<?,?> serverCall;
@ -330,9 +343,13 @@ public final class GrpcAuthorizationEngine {
SocketAddress addr = serverCall.getAttributes().get(Grpc.TRANSPORT_ATTR_LOCAL_ADDR);
return addr == null ? -1 : ((InetSocketAddress) addr).getPort();
}
private String getRequestedServerName() {
return "";
}
}
interface Matcher {
public interface Matcher {
boolean matches(EvaluateArgs args);
}
@ -392,7 +409,7 @@ public final class GrpcAuthorizationEngine {
/** Always true matcher.*/
public static final class AlwaysTrueMatcher implements Matcher {
static final AlwaysTrueMatcher INSTANCE = new AlwaysTrueMatcher();
public static AlwaysTrueMatcher INSTANCE = new AlwaysTrueMatcher();
@Override
public boolean matches(EvaluateArgs args) {

View File

@ -0,0 +1,343 @@
/*
* 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 static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import com.google.api.expr.v1alpha1.Expr;
import com.google.protobuf.Any;
import com.google.protobuf.Message;
import com.google.protobuf.UInt32Value;
import io.envoyproxy.envoy.config.core.v3.CidrRange;
import io.envoyproxy.envoy.config.rbac.v3.Permission;
import io.envoyproxy.envoy.config.rbac.v3.Policy;
import io.envoyproxy.envoy.config.rbac.v3.Principal;
import io.envoyproxy.envoy.config.rbac.v3.Principal.Authenticated;
import io.envoyproxy.envoy.config.rbac.v3.RBAC;
import io.envoyproxy.envoy.config.rbac.v3.RBAC.Action;
import io.envoyproxy.envoy.config.route.v3.HeaderMatcher;
import io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBACPerRoute;
import io.envoyproxy.envoy.type.matcher.v3.MetadataMatcher;
import io.envoyproxy.envoy.type.matcher.v3.PathMatcher;
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.MethodDescriptor.MethodType;
import io.grpc.ServerCall;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
import io.grpc.Status;
import io.grpc.testing.TestMethodDescriptors;
import io.grpc.xds.Filter.ConfigOrError;
import io.grpc.xds.Filter.FilterConfig;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AlwaysTrueMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AuthConfig;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AuthDecision;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.DestinationPortMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.OrMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.PolicyMatcher;
import java.net.InetSocketAddress;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.net.ssl.SSLSession;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor;
/** Tests for {@link RbacFilter}. */
@RunWith(JUnit4.class)
public class RbacFilterTest {
private static final String PATH = "auth";
private static final StringMatcher STRING_MATCHER =
StringMatcher.newBuilder().setExact("/" + PATH).setIgnoreCase(true).build();
@Test
@SuppressWarnings({"unchecked", "deprecation"})
public void ipPortParser() {
CidrRange cidrRange = CidrRange.newBuilder().setAddressPrefix("10.10.10.0")
.setPrefixLen(UInt32Value.of(24)).build();
List<Permission> permissionList = Arrays.asList(
Permission.newBuilder().setAndRules(Permission.Set.newBuilder()
.addRules(Permission.newBuilder().setDestinationIp(cidrRange).build())
.addRules(Permission.newBuilder().setDestinationPort(9090).build()).build()
).build());
List<Principal> principalList = Arrays.asList(
Principal.newBuilder().setAndIds(Principal.Set.newBuilder()
.addIds(Principal.newBuilder().setDirectRemoteIp(cidrRange).build())
.addIds(Principal.newBuilder().setRemoteIp(cidrRange).build())
.addIds(Principal.newBuilder().setSourceIp(cidrRange).build())
.build()).build());
ConfigOrError<?> result = parseRaw(permissionList, principalList);
assertThat(result.errorDetail).isNull();
ServerCall<Void,Void> serverCall = mock(ServerCall.class);
Attributes attributes = Attributes.newBuilder()
.set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, new InetSocketAddress("10.10.10.0", 1))
.set(Grpc.TRANSPORT_ATTR_LOCAL_ADDR, new InetSocketAddress("10.10.10.0",9090))
.build();
when(serverCall.getAttributes()).thenReturn(attributes);
when(serverCall.getMethodDescriptor()).thenReturn(method().build());
GrpcAuthorizationEngine engine =
new GrpcAuthorizationEngine(((RbacConfig)result.config).authConfig());
AuthDecision decision = engine.evaluate(new Metadata(), serverCall);
assertThat(decision.decision()).isEqualTo(GrpcAuthorizationEngine.Action.DENY);
}
@Test
@SuppressWarnings("unchecked")
public void pathParser() {
PathMatcher pathMatcher = PathMatcher.newBuilder().setPath(STRING_MATCHER).build();
List<Permission> permissionList = Arrays.asList(
Permission.newBuilder().setUrlPath(pathMatcher).build());
List<Principal> principalList = Arrays.asList(
Principal.newBuilder().setUrlPath(pathMatcher).build());
ConfigOrError<RbacConfig> result = parse(permissionList, principalList);
assertThat(result.errorDetail).isNull();
ServerCall<Void,Void> serverCall = mock(ServerCall.class);
when(serverCall.getMethodDescriptor()).thenReturn(method().build());
GrpcAuthorizationEngine engine =
new GrpcAuthorizationEngine(result.config.authConfig());
AuthDecision decision = engine.evaluate(new Metadata(), serverCall);
assertThat(decision.decision()).isEqualTo(GrpcAuthorizationEngine.Action.DENY);
}
@Test
@SuppressWarnings("unchecked")
public void authenticatedParser() throws Exception {
List<Permission> permissionList = Arrays.asList(
Permission.newBuilder().setNotRule(
Permission.newBuilder().setRequestedServerName(STRING_MATCHER).build()).build());
List<Principal> principalList = Arrays.asList(
Principal.newBuilder().setAuthenticated(Authenticated.newBuilder()
.setPrincipalName(STRING_MATCHER).build()).build());
ConfigOrError<?> result = parse(permissionList, principalList);
assertThat(result.errorDetail).isNull();
SSLSession sslSession = mock(SSLSession.class);
X509Certificate mockCert = mock(X509Certificate.class);
when(sslSession.getPeerCertificates()).thenReturn(new X509Certificate[]{mockCert});
when(mockCert.getSubjectAlternativeNames()).thenReturn(
Arrays.<List<?>>asList(Arrays.asList(2, "/" + PATH)));
Attributes attributes = Attributes.newBuilder()
.set(Grpc.TRANSPORT_ATTR_SSL_SESSION, sslSession)
.build();
ServerCall<Void,Void> serverCall = mock(ServerCall.class);
when(serverCall.getAttributes()).thenReturn(attributes);
GrpcAuthorizationEngine engine =
new GrpcAuthorizationEngine(((RbacConfig)result.config).authConfig());
AuthDecision decision = engine.evaluate(new Metadata(), serverCall);
assertThat(decision.decision()).isEqualTo(GrpcAuthorizationEngine.Action.DENY);
}
@Test
@SuppressWarnings("unchecked")
public void headerParser() {
HeaderMatcher headerMatcher = HeaderMatcher.newBuilder()
.setName("party").setExactMatch("win").build();
List<Permission> permissionList = Arrays.asList(
Permission.newBuilder().setHeader(headerMatcher).build());
List<Principal> principalList = Arrays.asList(
Principal.newBuilder().setHeader(headerMatcher).build());
ConfigOrError<RbacConfig> result = parseOverride(permissionList, principalList);
assertThat(result.errorDetail).isNull();
ServerCall<Void,Void> serverCall = mock(ServerCall.class);
GrpcAuthorizationEngine engine =
new GrpcAuthorizationEngine(result.config.authConfig());
AuthDecision decision = engine.evaluate(metadata("party", "win"), serverCall);
assertThat(decision.decision()).isEqualTo(GrpcAuthorizationEngine.Action.DENY);
}
@Test
@SuppressWarnings("unchecked")
public void compositeRules() {
MetadataMatcher metadataMatcher = MetadataMatcher.newBuilder().build();
List<Permission> permissionList = Arrays.asList(
Permission.newBuilder().setOrRules(Permission.Set.newBuilder().addRules(
Permission.newBuilder().setMetadata(metadataMatcher).build()
).build()).build());
List<Principal> principalList = Arrays.asList(
Principal.newBuilder().setNotId(
Principal.newBuilder().setMetadata(metadataMatcher).build()
).build());
ConfigOrError<? extends FilterConfig> result = parse(permissionList, principalList);
assertThat(result.errorDetail).isNull();
assertThat(result.config).isInstanceOf(RbacConfig.class);
ServerCall<Void,Void> serverCall = mock(ServerCall.class);
GrpcAuthorizationEngine engine =
new GrpcAuthorizationEngine(((RbacConfig)result.config).authConfig());
AuthDecision decision = engine.evaluate(new Metadata(), serverCall);
assertThat(decision.decision()).isEqualTo(GrpcAuthorizationEngine.Action.ALLOW);
}
@SuppressWarnings("unchecked")
@Test
public void testAuthorizationInterceptor() {
ServerCallHandler<Void, Void> mockHandler = mock(ServerCallHandler.class);
ServerCall<Void, Void> mockServerCall = mock(ServerCall.class);
Attributes attr = Attributes.newBuilder()
.set(Grpc.TRANSPORT_ATTR_LOCAL_ADDR, new InetSocketAddress("1::", 20))
.build();
when(mockServerCall.getAttributes()).thenReturn(attr);
PolicyMatcher policyMatcher = new PolicyMatcher("policy-matcher",
OrMatcher.create(new DestinationPortMatcher(99999)),
OrMatcher.create(AlwaysTrueMatcher.INSTANCE));
AuthConfig authconfig = new AuthConfig(Collections.singletonList(policyMatcher),
GrpcAuthorizationEngine.Action.ALLOW);
new RbacFilter().buildServerInterceptor(RbacConfig.create(authconfig), null)
.interceptCall(mockServerCall, new Metadata(), mockHandler);
verify(mockHandler, never()).startCall(eq(mockServerCall), any(Metadata.class));
ArgumentCaptor<Status> captor = ArgumentCaptor.forClass(Status.class);
verify(mockServerCall).close(captor.capture(), any(Metadata.class));
assertThat(captor.getValue().getCode()).isEqualTo(Status.UNAUTHENTICATED.getCode());
verify(mockServerCall).getAttributes();
verifyNoMoreInteractions(mockServerCall);
authconfig = new AuthConfig(Collections.singletonList(policyMatcher),
GrpcAuthorizationEngine.Action.DENY);
new RbacFilter().buildServerInterceptor(RbacConfig.create(authconfig), null)
.interceptCall(mockServerCall, new Metadata(), mockHandler);
verify(mockHandler).startCall(eq(mockServerCall), any(Metadata.class));
}
@Test
public void handleException() {
PathMatcher pathMatcher = PathMatcher.newBuilder()
.setPath(StringMatcher.newBuilder().build()).build();
List<Permission> permissionList = Arrays.asList(
Permission.newBuilder().setUrlPath(pathMatcher).build());
List<Principal> principalList = Arrays.asList(
Principal.newBuilder().setUrlPath(pathMatcher).build());
ConfigOrError<?> result = parse(permissionList, principalList);
assertThat(result.errorDetail).isNotNull();
permissionList = Arrays.asList(Permission.newBuilder().build());
principalList = Arrays.asList(Principal.newBuilder().build());
result = parse(permissionList, principalList);
assertThat(result.errorDetail).isNotNull();
Message rawProto = io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBAC.newBuilder()
.setRules(RBAC.newBuilder().setAction(Action.DENY)
.putPolicies("policy-name",
Policy.newBuilder().setCondition(Expr.newBuilder().build()).build())
.build()).build();
result = new RbacFilter().parseFilterConfig(Any.pack(rawProto));
assertThat(result.errorDetail).isNotNull();
}
@Test
@SuppressWarnings("unchecked")
public void overrideConfig() {
ServerCallHandler<Void, Void> mockHandler = mock(ServerCallHandler.class);
ServerCall<Void, Void> mockServerCall = mock(ServerCall.class);
Attributes attr = Attributes.newBuilder()
.set(Grpc.TRANSPORT_ATTR_LOCAL_ADDR, new InetSocketAddress("1::", 20))
.build();
when(mockServerCall.getAttributes()).thenReturn(attr);
PolicyMatcher policyMatcher = new PolicyMatcher("policy-matcher",
OrMatcher.create(new DestinationPortMatcher(99999)),
OrMatcher.create(AlwaysTrueMatcher.INSTANCE));
AuthConfig authconfig = new AuthConfig(Collections.singletonList(policyMatcher),
GrpcAuthorizationEngine.Action.ALLOW);
RbacConfig original = RbacConfig.create(authconfig);
RBACPerRoute rbacPerRoute = RBACPerRoute.newBuilder().build();
RbacConfig override =
new RbacFilter().parseFilterConfigOverride(Any.pack(rbacPerRoute)).config;
assertThat(override).isEqualTo(RbacConfig.create(null));
ServerInterceptor interceptor = new RbacFilter().buildServerInterceptor(original, override);
assertThat(interceptor).isNull();
policyMatcher = new PolicyMatcher("policy-matcher-override",
OrMatcher.create(new DestinationPortMatcher(20)),
OrMatcher.create(AlwaysTrueMatcher.INSTANCE));
authconfig = new AuthConfig(Collections.singletonList(policyMatcher),
GrpcAuthorizationEngine.Action.ALLOW);
override = RbacConfig.create(authconfig);
new RbacFilter().buildServerInterceptor(original, override)
.interceptCall(mockServerCall, new Metadata(), mockHandler);
verify(mockHandler).startCall(eq(mockServerCall), any(Metadata.class));
verify(mockServerCall).getAttributes();
verifyNoMoreInteractions(mockServerCall);
}
@Test
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<RbacConfig> result = new RbacFilter().parseFilterConfig(Any.pack(rawProto));
assertThat(result.config).isEqualTo(RbacConfig.create(null));
}
private static Metadata metadata(String key, String value) {
Metadata metadata = new Metadata();
metadata.put(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER), value);
return metadata;
}
private MethodDescriptor.Builder<Void, Void> method() {
return MethodDescriptor.<Void,Void>newBuilder()
.setType(MethodType.BIDI_STREAMING)
.setFullMethodName(PATH)
.setRequestMarshaller(TestMethodDescriptors.voidMarshaller())
.setResponseMarshaller(TestMethodDescriptors.voidMarshaller());
}
private ConfigOrError<RbacConfig> parse(List<Permission> permissionList,
List<Principal> principalList) {
return RbacFilter.parseRbacConfig(buildRbac(permissionList, principalList));
}
private ConfigOrError<RbacConfig> parseRaw(List<Permission> permissionList,
List<Principal> principalList) {
Message rawProto = buildRbac(permissionList, principalList);
Any proto = Any.pack(rawProto);
return new RbacFilter().parseFilterConfig(proto);
}
private io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBAC buildRbac(
List<Permission> permissionList, List<Principal> principalList) {
return io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBAC.newBuilder()
.setRules(RBAC.newBuilder().setAction(Action.DENY)
.putPolicies("policy-name", Policy.newBuilder()
.addAllPermissions(permissionList)
.addAllPrincipals(principalList).build()).build()).build();
}
private ConfigOrError<RbacConfig> parseOverride(List<Permission> permissionList,
List<Principal> principalList) {
RBACPerRoute rbacPerRoute = RBACPerRoute.newBuilder().setRbac(
buildRbac(permissionList, principalList)).build();
Any proto = Any.pack(rbacPerRoute);
return new RbacFilter().parseFilterConfigOverride(proto);
}
}

View File

@ -38,10 +38,10 @@ import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AlwaysTrueMatche
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AndMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AuthConfig;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AuthDecision;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AuthHeaderMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AuthenticatedMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.DestinationIpMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.DestinationPortMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.HeaderMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.InvertMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.OrMatcher;
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.PathMatcher;
@ -143,7 +143,7 @@ public class GrpcAuthorizationEngineTest {
@Test
public void headerMatcher() {
HeaderMatcher headerMatcher = new HeaderMatcher(Matchers.HeaderMatcher
AuthHeaderMatcher headerMatcher = new AuthHeaderMatcher(Matchers.HeaderMatcher
.forExactValue(HEADER_KEY, HEADER_VALUE, false));
OrMatcher principal = OrMatcher.create(headerMatcher);
OrMatcher permission = OrMatcher.create(
@ -156,7 +156,7 @@ public class GrpcAuthorizationEngineTest {
assertThat(decision.matchingPolicyName()).isEqualTo(POLICY_NAME);
HEADER.put(Metadata.Key.of(HEADER_KEY, Metadata.ASCII_STRING_MARSHALLER), HEADER_VALUE);
headerMatcher = new HeaderMatcher(Matchers.HeaderMatcher
headerMatcher = new AuthHeaderMatcher(Matchers.HeaderMatcher
.forExactValue(HEADER_KEY, HEADER_VALUE + "," + HEADER_VALUE, false));
principal = OrMatcher.create(headerMatcher);
policyMatcher = new PolicyMatcher(POLICY_NAME,
@ -166,7 +166,7 @@ public class GrpcAuthorizationEngineTest {
decision = engine.evaluate(HEADER, serverCall);
assertThat(decision.decision()).isEqualTo(Action.ALLOW);
headerMatcher = new HeaderMatcher(Matchers.HeaderMatcher
headerMatcher = new AuthHeaderMatcher(Matchers.HeaderMatcher
.forExactValue(HEADER_KEY + Metadata.BINARY_HEADER_SUFFIX, HEADER_VALUE, false));
principal = OrMatcher.create(headerMatcher);
policyMatcher = new PolicyMatcher(POLICY_NAME,
@ -271,7 +271,7 @@ public class GrpcAuthorizationEngineTest {
new InvertMatcher(new DestinationPortMatcher(PORT + 1))));
PolicyMatcher policyMatcher1 = new PolicyMatcher(POLICY_NAME, permission, principal);
HeaderMatcher headerMatcher = new HeaderMatcher(Matchers.HeaderMatcher
AuthHeaderMatcher headerMatcher = new AuthHeaderMatcher(Matchers.HeaderMatcher
.forExactValue(HEADER_KEY, HEADER_VALUE + 1, false));
authMatcher = new AuthenticatedMatcher(
StringMatcher.forContains("TEST.google.fr"));