1 /* 2 * Copyright 2021 The gRPC Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package io.grpc.xds; 18 19 import static com.google.common.base.Preconditions.checkNotNull; 20 21 import com.google.common.annotations.VisibleForTesting; 22 import com.google.protobuf.Any; 23 import com.google.protobuf.InvalidProtocolBufferException; 24 import com.google.protobuf.Message; 25 import io.envoyproxy.envoy.config.core.v3.CidrRange; 26 import io.envoyproxy.envoy.config.rbac.v3.Permission; 27 import io.envoyproxy.envoy.config.rbac.v3.Policy; 28 import io.envoyproxy.envoy.config.rbac.v3.Principal; 29 import io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBAC; 30 import io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBACPerRoute; 31 import io.envoyproxy.envoy.type.v3.Int32Range; 32 import io.grpc.Metadata; 33 import io.grpc.ServerCall; 34 import io.grpc.ServerCallHandler; 35 import io.grpc.ServerInterceptor; 36 import io.grpc.Status; 37 import io.grpc.xds.Filter.ServerInterceptorBuilder; 38 import io.grpc.xds.internal.MatcherParser; 39 import io.grpc.xds.internal.Matchers; 40 import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine; 41 import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AlwaysTrueMatcher; 42 import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AndMatcher; 43 import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AuthConfig; 44 import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AuthDecision; 45 import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AuthHeaderMatcher; 46 import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AuthenticatedMatcher; 47 import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.DestinationIpMatcher; 48 import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.DestinationPortMatcher; 49 import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.DestinationPortRangeMatcher; 50 import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.InvertMatcher; 51 import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.Matcher; 52 import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.OrMatcher; 53 import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.PathMatcher; 54 import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.PolicyMatcher; 55 import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.RequestedServerNameMatcher; 56 import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.SourceIpMatcher; 57 import java.net.InetAddress; 58 import java.net.UnknownHostException; 59 import java.util.ArrayList; 60 import java.util.List; 61 import java.util.Map; 62 import java.util.logging.Level; 63 import java.util.logging.Logger; 64 import javax.annotation.Nullable; 65 66 /** RBAC Http filter implementation. */ 67 final class RbacFilter implements Filter, ServerInterceptorBuilder { 68 private static final Logger logger = Logger.getLogger(RbacFilter.class.getName()); 69 70 static final RbacFilter INSTANCE = new RbacFilter(); 71 72 static final String TYPE_URL = 73 "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC"; 74 75 private static final String TYPE_URL_OVERRIDE_CONFIG = 76 "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute"; 77 RbacFilter()78 RbacFilter() {} 79 80 @Override typeUrls()81 public String[] typeUrls() { 82 return new String[] { TYPE_URL, TYPE_URL_OVERRIDE_CONFIG }; 83 } 84 85 @Override parseFilterConfig(Message rawProtoMessage)86 public ConfigOrError<RbacConfig> parseFilterConfig(Message rawProtoMessage) { 87 RBAC rbacProto; 88 if (!(rawProtoMessage instanceof Any)) { 89 return ConfigOrError.fromError("Invalid config type: " + rawProtoMessage.getClass()); 90 } 91 Any anyMessage = (Any) rawProtoMessage; 92 try { 93 rbacProto = anyMessage.unpack(RBAC.class); 94 } catch (InvalidProtocolBufferException e) { 95 return ConfigOrError.fromError("Invalid proto: " + e); 96 } 97 return parseRbacConfig(rbacProto); 98 } 99 100 @VisibleForTesting parseRbacConfig(RBAC rbac)101 static ConfigOrError<RbacConfig> parseRbacConfig(RBAC rbac) { 102 if (!rbac.hasRules()) { 103 return ConfigOrError.fromConfig(RbacConfig.create(null)); 104 } 105 io.envoyproxy.envoy.config.rbac.v3.RBAC rbacConfig = rbac.getRules(); 106 GrpcAuthorizationEngine.Action authAction; 107 switch (rbacConfig.getAction()) { 108 case ALLOW: 109 authAction = GrpcAuthorizationEngine.Action.ALLOW; 110 break; 111 case DENY: 112 authAction = GrpcAuthorizationEngine.Action.DENY; 113 break; 114 case LOG: 115 return ConfigOrError.fromConfig(RbacConfig.create(null)); 116 case UNRECOGNIZED: 117 default: 118 return ConfigOrError.fromError("Unknown rbacConfig action type: " + rbacConfig.getAction()); 119 } 120 Map<String, Policy> policyMap = rbacConfig.getPoliciesMap(); 121 List<GrpcAuthorizationEngine.PolicyMatcher> policyMatchers = new ArrayList<>(); 122 for (Map.Entry<String, Policy> entry: policyMap.entrySet()) { 123 try { 124 Policy policy = entry.getValue(); 125 if (policy.hasCondition() || policy.hasCheckedCondition()) { 126 return ConfigOrError.fromError( 127 "Policy.condition and Policy.checked_condition must not set: " + entry.getKey()); 128 } 129 policyMatchers.add(PolicyMatcher.create(entry.getKey(), 130 parsePermissionList(policy.getPermissionsList()), 131 parsePrincipalList(policy.getPrincipalsList()))); 132 } catch (Exception e) { 133 return ConfigOrError.fromError("Encountered error parsing policy: " + e); 134 } 135 } 136 return ConfigOrError.fromConfig(RbacConfig.create( 137 AuthConfig.create(policyMatchers, authAction))); 138 } 139 140 @Override parseFilterConfigOverride(Message rawProtoMessage)141 public ConfigOrError<RbacConfig> parseFilterConfigOverride(Message rawProtoMessage) { 142 RBACPerRoute rbacPerRoute; 143 if (!(rawProtoMessage instanceof Any)) { 144 return ConfigOrError.fromError("Invalid config type: " + rawProtoMessage.getClass()); 145 } 146 Any anyMessage = (Any) rawProtoMessage; 147 try { 148 rbacPerRoute = anyMessage.unpack(RBACPerRoute.class); 149 } catch (InvalidProtocolBufferException e) { 150 return ConfigOrError.fromError("Invalid proto: " + e); 151 } 152 if (rbacPerRoute.hasRbac()) { 153 return parseRbacConfig(rbacPerRoute.getRbac()); 154 } else { 155 return ConfigOrError.fromConfig(RbacConfig.create(null)); 156 } 157 } 158 159 @Nullable 160 @Override buildServerInterceptor(FilterConfig config, @Nullable FilterConfig overrideConfig)161 public ServerInterceptor buildServerInterceptor(FilterConfig config, 162 @Nullable FilterConfig overrideConfig) { 163 checkNotNull(config, "config"); 164 if (overrideConfig != null) { 165 config = overrideConfig; 166 } 167 AuthConfig authConfig = ((RbacConfig) config).authConfig(); 168 return authConfig == null ? null : generateAuthorizationInterceptor(authConfig); 169 } 170 generateAuthorizationInterceptor(AuthConfig config)171 private ServerInterceptor generateAuthorizationInterceptor(AuthConfig config) { 172 checkNotNull(config, "config"); 173 final GrpcAuthorizationEngine authEngine = new GrpcAuthorizationEngine(config); 174 return new ServerInterceptor() { 175 @Override 176 public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall( 177 final ServerCall<ReqT, RespT> call, 178 final Metadata headers, ServerCallHandler<ReqT, RespT> next) { 179 AuthDecision authResult = authEngine.evaluate(headers, call); 180 if (logger.isLoggable(Level.FINE)) { 181 logger.log(Level.FINE, 182 "Authorization result for serverCall {0}: {1}, matching policy: {2}.", 183 new Object[]{call, authResult.decision(), authResult.matchingPolicyName()}); 184 } 185 if (GrpcAuthorizationEngine.Action.DENY.equals(authResult.decision())) { 186 Status status = Status.PERMISSION_DENIED.withDescription("Access Denied"); 187 call.close(status, new Metadata()); 188 return new ServerCall.Listener<ReqT>(){}; 189 } 190 return next.startCall(call, headers); 191 } 192 }; 193 } 194 195 private static OrMatcher parsePermissionList(List<Permission> permissions) { 196 List<Matcher> anyMatch = new ArrayList<>(); 197 for (Permission permission : permissions) { 198 anyMatch.add(parsePermission(permission)); 199 } 200 return OrMatcher.create(anyMatch); 201 } 202 203 private static Matcher parsePermission(Permission permission) { 204 switch (permission.getRuleCase()) { 205 case AND_RULES: 206 List<Matcher> andMatch = new ArrayList<>(); 207 for (Permission p : permission.getAndRules().getRulesList()) { 208 andMatch.add(parsePermission(p)); 209 } 210 return AndMatcher.create(andMatch); 211 case OR_RULES: 212 return parsePermissionList(permission.getOrRules().getRulesList()); 213 case ANY: 214 return AlwaysTrueMatcher.INSTANCE; 215 case HEADER: 216 return parseHeaderMatcher(permission.getHeader()); 217 case URL_PATH: 218 return parsePathMatcher(permission.getUrlPath()); 219 case DESTINATION_IP: 220 return createDestinationIpMatcher(permission.getDestinationIp()); 221 case DESTINATION_PORT: 222 return createDestinationPortMatcher(permission.getDestinationPort()); 223 case DESTINATION_PORT_RANGE: 224 return parseDestinationPortRangeMatcher(permission.getDestinationPortRange()); 225 case NOT_RULE: 226 return InvertMatcher.create(parsePermission(permission.getNotRule())); 227 case METADATA: // hard coded, never match. 228 return InvertMatcher.create(AlwaysTrueMatcher.INSTANCE); 229 case REQUESTED_SERVER_NAME: 230 return parseRequestedServerNameMatcher(permission.getRequestedServerName()); 231 case RULE_NOT_SET: 232 default: 233 throw new IllegalArgumentException( 234 "Unknown permission rule case: " + permission.getRuleCase()); 235 } 236 } 237 238 private static OrMatcher parsePrincipalList(List<Principal> principals) { 239 List<Matcher> anyMatch = new ArrayList<>(); 240 for (Principal principal: principals) { 241 anyMatch.add(parsePrincipal(principal)); 242 } 243 return OrMatcher.create(anyMatch); 244 } 245 246 private static Matcher parsePrincipal(Principal principal) { 247 switch (principal.getIdentifierCase()) { 248 case OR_IDS: 249 return parsePrincipalList(principal.getOrIds().getIdsList()); 250 case AND_IDS: 251 List<Matcher> nextMatchers = new ArrayList<>(); 252 for (Principal next : principal.getAndIds().getIdsList()) { 253 nextMatchers.add(parsePrincipal(next)); 254 } 255 return AndMatcher.create(nextMatchers); 256 case ANY: 257 return AlwaysTrueMatcher.INSTANCE; 258 case AUTHENTICATED: 259 return parseAuthenticatedMatcher(principal.getAuthenticated()); 260 case DIRECT_REMOTE_IP: 261 return createSourceIpMatcher(principal.getDirectRemoteIp()); 262 case REMOTE_IP: 263 return createSourceIpMatcher(principal.getRemoteIp()); 264 case SOURCE_IP: 265 return createSourceIpMatcher(principal.getSourceIp()); 266 case HEADER: 267 return parseHeaderMatcher(principal.getHeader()); 268 case NOT_ID: 269 return InvertMatcher.create(parsePrincipal(principal.getNotId())); 270 case URL_PATH: 271 return parsePathMatcher(principal.getUrlPath()); 272 case METADATA: // hard coded, never match. 273 return InvertMatcher.create(AlwaysTrueMatcher.INSTANCE); 274 case IDENTIFIER_NOT_SET: 275 default: 276 throw new IllegalArgumentException( 277 "Unknown principal identifier case: " + principal.getIdentifierCase()); 278 } 279 } 280 281 private static PathMatcher parsePathMatcher( 282 io.envoyproxy.envoy.type.matcher.v3.PathMatcher proto) { 283 switch (proto.getRuleCase()) { 284 case PATH: 285 return PathMatcher.create(MatcherParser.parseStringMatcher(proto.getPath())); 286 case RULE_NOT_SET: 287 default: 288 throw new IllegalArgumentException( 289 "Unknown path matcher rule type: " + proto.getRuleCase()); 290 } 291 } 292 293 private static RequestedServerNameMatcher parseRequestedServerNameMatcher( 294 io.envoyproxy.envoy.type.matcher.v3.StringMatcher proto) { 295 return RequestedServerNameMatcher.create(MatcherParser.parseStringMatcher(proto)); 296 } 297 298 private static AuthHeaderMatcher parseHeaderMatcher( 299 io.envoyproxy.envoy.config.route.v3.HeaderMatcher proto) { 300 if (proto.getName().startsWith("grpc-")) { 301 throw new IllegalArgumentException("Invalid header matcher config: [grpc-] prefixed " 302 + "header name is not allowed."); 303 } 304 if (":scheme".equals(proto.getName())) { 305 throw new IllegalArgumentException("Invalid header matcher config: header name [:scheme] " 306 + "is not allowed."); 307 } 308 return AuthHeaderMatcher.create(MatcherParser.parseHeaderMatcher(proto)); 309 } 310 311 private static AuthenticatedMatcher parseAuthenticatedMatcher( 312 Principal.Authenticated proto) { 313 Matchers.StringMatcher matcher = MatcherParser.parseStringMatcher(proto.getPrincipalName()); 314 return AuthenticatedMatcher.create(matcher); 315 } 316 317 private static DestinationPortMatcher createDestinationPortMatcher(int port) { 318 return DestinationPortMatcher.create(port); 319 } 320 321 private static DestinationPortRangeMatcher parseDestinationPortRangeMatcher(Int32Range range) { 322 return DestinationPortRangeMatcher.create(range.getStart(), range.getEnd()); 323 } 324 325 private static DestinationIpMatcher createDestinationIpMatcher(CidrRange cidrRange) { 326 return DestinationIpMatcher.create(Matchers.CidrMatcher.create( 327 resolve(cidrRange), cidrRange.getPrefixLen().getValue())); 328 } 329 330 private static SourceIpMatcher createSourceIpMatcher(CidrRange cidrRange) { 331 return SourceIpMatcher.create(Matchers.CidrMatcher.create( 332 resolve(cidrRange), cidrRange.getPrefixLen().getValue())); 333 } 334 335 private static InetAddress resolve(CidrRange cidrRange) { 336 try { 337 return InetAddress.getByName(cidrRange.getAddressPrefix()); 338 } catch (UnknownHostException ex) { 339 throw new IllegalArgumentException("IP address can not be found: " + ex); 340 } 341 } 342 } 343 344