diff --git a/xds/src/main/java/io/grpc/xds/Filter.java b/xds/src/main/java/io/grpc/xds/Filter.java index f8372cdd1b..3da71fd6a4 100644 --- a/xds/src/main/java/io/grpc/xds/Filter.java +++ b/xds/src/main/java/io/grpc/xds/Filter.java @@ -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); } diff --git a/xds/src/main/java/io/grpc/xds/FilterRegistry.java b/xds/src/main/java/io/grpc/xds/FilterRegistry.java index db4f256bce..7f1fe82c6c 100644 --- a/xds/src/main/java/io/grpc/xds/FilterRegistry.java +++ b/xds/src/main/java/io/grpc/xds/FilterRegistry.java @@ -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; } diff --git a/xds/src/main/java/io/grpc/xds/RbacConfig.java b/xds/src/main/java/io/grpc/xds/RbacConfig.java new file mode 100644 index 0000000000..14f6ae33e1 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/RbacConfig.java @@ -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); + } +} diff --git a/xds/src/main/java/io/grpc/xds/RbacFilter.java b/xds/src/main/java/io/grpc/xds/RbacFilter.java new file mode 100644 index 0000000000..c393f1ce04 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/RbacFilter.java @@ -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 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 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 policyMap = rbacConfig.getPoliciesMap(); + List policyMatchers = new ArrayList<>(); + for (Map.Entry 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 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 ServerCall.Listener interceptCall( + final ServerCall call, + final Metadata headers, ServerCallHandler 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(){}; + } + return next.startCall(call, headers); + } + }; + } + + private static OrMatcher parsePermissionList(List permissions) { + List 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 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 principals) { + List 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 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); + } + } +} + diff --git a/xds/src/main/java/io/grpc/xds/internal/MatcherParser.java b/xds/src/main/java/io/grpc/xds/internal/MatcherParser.java new file mode 100644 index 0000000000..0a971655df --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/internal/MatcherParser.java @@ -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()); + } + } +} diff --git a/xds/src/main/java/io/grpc/xds/internal/rbac/engine/GrpcAuthorizationEngine.java b/xds/src/main/java/io/grpc/xds/internal/rbac/engine/GrpcAuthorizationEngine.java index 65f0b69bdc..6d275d322a 100644 --- a/xds/src/main/java/io/grpc/xds/internal/rbac/engine/GrpcAuthorizationEngine.java +++ b/xds/src/main/java/io/grpc/xds/internal/rbac/engine/GrpcAuthorizationEngine.java @@ -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) { diff --git a/xds/src/test/java/io/grpc/xds/RbacFilterTest.java b/xds/src/test/java/io/grpc/xds/RbacFilterTest.java new file mode 100644 index 0000000000..c5fe7b3d1b --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/RbacFilterTest.java @@ -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 permissionList = Arrays.asList( + Permission.newBuilder().setAndRules(Permission.Set.newBuilder() + .addRules(Permission.newBuilder().setDestinationIp(cidrRange).build()) + .addRules(Permission.newBuilder().setDestinationPort(9090).build()).build() + ).build()); + List 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 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 permissionList = Arrays.asList( + Permission.newBuilder().setUrlPath(pathMatcher).build()); + List principalList = Arrays.asList( + Principal.newBuilder().setUrlPath(pathMatcher).build()); + ConfigOrError result = parse(permissionList, principalList); + assertThat(result.errorDetail).isNull(); + ServerCall 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 permissionList = Arrays.asList( + Permission.newBuilder().setNotRule( + Permission.newBuilder().setRequestedServerName(STRING_MATCHER).build()).build()); + List 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.>asList(Arrays.asList(2, "/" + PATH))); + Attributes attributes = Attributes.newBuilder() + .set(Grpc.TRANSPORT_ATTR_SSL_SESSION, sslSession) + .build(); + ServerCall 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 permissionList = Arrays.asList( + Permission.newBuilder().setHeader(headerMatcher).build()); + List principalList = Arrays.asList( + Principal.newBuilder().setHeader(headerMatcher).build()); + ConfigOrError result = parseOverride(permissionList, principalList); + assertThat(result.errorDetail).isNull(); + ServerCall 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 permissionList = Arrays.asList( + Permission.newBuilder().setOrRules(Permission.Set.newBuilder().addRules( + Permission.newBuilder().setMetadata(metadataMatcher).build() + ).build()).build()); + List principalList = Arrays.asList( + Principal.newBuilder().setNotId( + Principal.newBuilder().setMetadata(metadataMatcher).build() + ).build()); + ConfigOrError result = parse(permissionList, principalList); + assertThat(result.errorDetail).isNull(); + assertThat(result.config).isInstanceOf(RbacConfig.class); + ServerCall 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 mockHandler = mock(ServerCallHandler.class); + ServerCall 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 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 permissionList = Arrays.asList( + Permission.newBuilder().setUrlPath(pathMatcher).build()); + List 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 mockHandler = mock(ServerCallHandler.class); + ServerCall 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 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 method() { + return MethodDescriptor.newBuilder() + .setType(MethodType.BIDI_STREAMING) + .setFullMethodName(PATH) + .setRequestMarshaller(TestMethodDescriptors.voidMarshaller()) + .setResponseMarshaller(TestMethodDescriptors.voidMarshaller()); + } + + private ConfigOrError parse(List permissionList, + List principalList) { + + return RbacFilter.parseRbacConfig(buildRbac(permissionList, principalList)); + } + + private ConfigOrError parseRaw(List permissionList, + List 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 permissionList, List 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 parseOverride(List permissionList, + List principalList) { + RBACPerRoute rbacPerRoute = RBACPerRoute.newBuilder().setRbac( + buildRbac(permissionList, principalList)).build(); + Any proto = Any.pack(rbacPerRoute); + return new RbacFilter().parseFilterConfigOverride(proto); + } +} diff --git a/xds/src/test/java/io/grpc/xds/internal/rbac/engine/GrpcAuthorizationEngineTest.java b/xds/src/test/java/io/grpc/xds/internal/rbac/engine/GrpcAuthorizationEngineTest.java index 4ed637a998..504c9e8df2 100644 --- a/xds/src/test/java/io/grpc/xds/internal/rbac/engine/GrpcAuthorizationEngineTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/rbac/engine/GrpcAuthorizationEngineTest.java @@ -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"));