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.internal.rbac.engine; 18 19 import static com.google.common.base.Preconditions.checkNotNull; 20 21 import com.google.auto.value.AutoValue; 22 import com.google.common.base.Joiner; 23 import com.google.common.collect.ImmutableList; 24 import com.google.common.io.BaseEncoding; 25 import io.grpc.Grpc; 26 import io.grpc.Metadata; 27 import io.grpc.ServerCall; 28 import io.grpc.xds.internal.Matchers; 29 import java.net.InetAddress; 30 import java.net.InetSocketAddress; 31 import java.net.SocketAddress; 32 import java.security.cert.Certificate; 33 import java.security.cert.CertificateParsingException; 34 import java.security.cert.X509Certificate; 35 import java.util.ArrayList; 36 import java.util.Arrays; 37 import java.util.Collection; 38 import java.util.Collections; 39 import java.util.List; 40 import java.util.Locale; 41 import java.util.logging.Level; 42 import java.util.logging.Logger; 43 import javax.annotation.Nullable; 44 import javax.net.ssl.SSLPeerUnverifiedException; 45 import javax.net.ssl.SSLSession; 46 47 /** 48 * Implementation of gRPC server access control based on envoy RBAC protocol: 49 * https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/rbac/v3/rbac.proto 50 * 51 * <p>One GrpcAuthorizationEngine is initialized with one action type and a list of policies. 52 * Policies are examined sequentially in order in an any match fashion, and the first matched policy 53 * will be returned. If not matched at all, the opposite action type is returned as a result. 54 */ 55 public final class GrpcAuthorizationEngine { 56 private static final Logger log = Logger.getLogger(GrpcAuthorizationEngine.class.getName()); 57 58 private final AuthConfig authConfig; 59 60 /** Instantiated with envoy policyMatcher configuration. */ GrpcAuthorizationEngine(AuthConfig authConfig)61 public GrpcAuthorizationEngine(AuthConfig authConfig) { 62 this.authConfig = authConfig; 63 } 64 65 /** Return the auth decision for the request argument against the policies. */ evaluate(Metadata metadata, ServerCall<?,?> serverCall)66 public AuthDecision evaluate(Metadata metadata, ServerCall<?,?> serverCall) { 67 checkNotNull(metadata, "metadata"); 68 checkNotNull(serverCall, "serverCall"); 69 String firstMatch = null; 70 EvaluateArgs args = new EvaluateArgs(metadata, serverCall); 71 for (PolicyMatcher policyMatcher : authConfig.policies()) { 72 if (policyMatcher.matches(args)) { 73 firstMatch = policyMatcher.name(); 74 break; 75 } 76 } 77 Action decisionType = Action.DENY; 78 if (Action.DENY.equals(authConfig.action()) == (firstMatch == null)) { 79 decisionType = Action.ALLOW; 80 } 81 return AuthDecision.create(decisionType, firstMatch); 82 } 83 84 public enum Action { 85 ALLOW, 86 DENY, 87 } 88 89 /** 90 * An authorization decision provides information about the decision type and the policy name 91 * identifier based on the authorization engine evaluation. */ 92 @AutoValue 93 public abstract static class AuthDecision { decision()94 public abstract Action decision(); 95 96 @Nullable matchingPolicyName()97 public abstract String matchingPolicyName(); 98 create(Action decisionType, @Nullable String matchingPolicy)99 static AuthDecision create(Action decisionType, @Nullable String matchingPolicy) { 100 return new AutoValue_GrpcAuthorizationEngine_AuthDecision(decisionType, matchingPolicy); 101 } 102 } 103 104 /** Represents authorization config policy that the engine will evaluate against. */ 105 @AutoValue 106 public abstract static class AuthConfig { policies()107 public abstract ImmutableList<PolicyMatcher> policies(); 108 action()109 public abstract Action action(); 110 create(List<PolicyMatcher> policies, Action action)111 public static AuthConfig create(List<PolicyMatcher> policies, Action action) { 112 return new AutoValue_GrpcAuthorizationEngine_AuthConfig( 113 ImmutableList.copyOf(policies), action); 114 } 115 } 116 117 /** 118 * Implements a top level {@link Matcher} for a single RBAC policy configuration per envoy 119 * protocol: 120 * https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/rbac/v3/rbac.proto#config-rbac-v3-policy. 121 * 122 * <p>Currently we only support matching some of the request fields. Those unsupported fields are 123 * considered not match until we stop ignoring them. 124 */ 125 @AutoValue 126 public abstract static class PolicyMatcher implements Matcher { name()127 public abstract String name(); 128 permissions()129 public abstract OrMatcher permissions(); 130 principals()131 public abstract OrMatcher principals(); 132 133 /** Constructs a matcher for one RBAC policy. */ create(String name, OrMatcher permissions, OrMatcher principals)134 public static PolicyMatcher create(String name, OrMatcher permissions, OrMatcher principals) { 135 return new AutoValue_GrpcAuthorizationEngine_PolicyMatcher(name, permissions, principals); 136 } 137 138 @Override matches(EvaluateArgs args)139 public boolean matches(EvaluateArgs args) { 140 return permissions().matches(args) && principals().matches(args); 141 } 142 } 143 144 @AutoValue 145 public abstract static class AuthenticatedMatcher implements Matcher { 146 @Nullable delegate()147 public abstract Matchers.StringMatcher delegate(); 148 149 /** 150 * Passing in null will match all authenticated user, i.e. SSL session is present. 151 * https://github.com/envoyproxy/envoy/blob/3975bf5dadb43421907bbc52df57c0e8539c9a06/api/envoy/config/rbac/v3/rbac.proto#L253 152 * */ create(@ullable Matchers.StringMatcher delegate)153 public static AuthenticatedMatcher create(@Nullable Matchers.StringMatcher delegate) { 154 return new AutoValue_GrpcAuthorizationEngine_AuthenticatedMatcher(delegate); 155 } 156 157 @Override matches(EvaluateArgs args)158 public boolean matches(EvaluateArgs args) { 159 Collection<String> principalNames = args.getPrincipalNames(); 160 log.log(Level.FINER, "Matching principal names: {0}", new Object[]{principalNames}); 161 // Null means unauthenticated connection. 162 if (principalNames == null) { 163 return false; 164 } 165 // Connection is authenticated, so returns match when delegated string matcher is not present. 166 if (delegate() == null) { 167 return true; 168 } 169 for (String name : principalNames) { 170 if (delegate().matches(name)) { 171 return true; 172 } 173 } 174 return false; 175 } 176 } 177 178 @AutoValue 179 public abstract static class DestinationIpMatcher implements Matcher { delegate()180 public abstract Matchers.CidrMatcher delegate(); 181 create(Matchers.CidrMatcher delegate)182 public static DestinationIpMatcher create(Matchers.CidrMatcher delegate) { 183 return new AutoValue_GrpcAuthorizationEngine_DestinationIpMatcher(delegate); 184 } 185 186 @Override matches(EvaluateArgs args)187 public boolean matches(EvaluateArgs args) { 188 return delegate().matches(args.getDestinationIp()); 189 } 190 } 191 192 @AutoValue 193 public abstract static class SourceIpMatcher implements Matcher { delegate()194 public abstract Matchers.CidrMatcher delegate(); 195 create(Matchers.CidrMatcher delegate)196 public static SourceIpMatcher create(Matchers.CidrMatcher delegate) { 197 return new AutoValue_GrpcAuthorizationEngine_SourceIpMatcher(delegate); 198 } 199 200 @Override matches(EvaluateArgs args)201 public boolean matches(EvaluateArgs args) { 202 return delegate().matches(args.getSourceIp()); 203 } 204 } 205 206 @AutoValue 207 public abstract static class PathMatcher implements Matcher { delegate()208 public abstract Matchers.StringMatcher delegate(); 209 create(Matchers.StringMatcher delegate)210 public static PathMatcher create(Matchers.StringMatcher delegate) { 211 return new AutoValue_GrpcAuthorizationEngine_PathMatcher(delegate); 212 } 213 214 @Override matches(EvaluateArgs args)215 public boolean matches(EvaluateArgs args) { 216 return delegate().matches(args.getPath()); 217 } 218 } 219 220 @AutoValue 221 public abstract static class AuthHeaderMatcher implements Matcher { delegate()222 public abstract Matchers.HeaderMatcher delegate(); 223 create(Matchers.HeaderMatcher delegate)224 public static AuthHeaderMatcher create(Matchers.HeaderMatcher delegate) { 225 return new AutoValue_GrpcAuthorizationEngine_AuthHeaderMatcher(delegate); 226 } 227 228 @Override matches(EvaluateArgs args)229 public boolean matches(EvaluateArgs args) { 230 return delegate().matches(args.getHeader(delegate().name())); 231 } 232 } 233 234 @AutoValue 235 public abstract static class DestinationPortMatcher implements Matcher { port()236 public abstract int port(); 237 create(int port)238 public static DestinationPortMatcher create(int port) { 239 return new AutoValue_GrpcAuthorizationEngine_DestinationPortMatcher(port); 240 } 241 242 @Override matches(EvaluateArgs args)243 public boolean matches(EvaluateArgs args) { 244 return port() == args.getDestinationPort(); 245 } 246 } 247 248 @AutoValue 249 public abstract static class DestinationPortRangeMatcher implements Matcher { start()250 public abstract int start(); 251 end()252 public abstract int end(); 253 254 /** Start of the range is inclusive. End of the range is exclusive.*/ create(int start, int end)255 public static DestinationPortRangeMatcher create(int start, int end) { 256 return new AutoValue_GrpcAuthorizationEngine_DestinationPortRangeMatcher(start, end); 257 } 258 259 @Override matches(EvaluateArgs args)260 public boolean matches(EvaluateArgs args) { 261 int port = args.getDestinationPort(); 262 return port >= start() && port < end(); 263 } 264 } 265 266 @AutoValue 267 public abstract static class RequestedServerNameMatcher implements Matcher { delegate()268 public abstract Matchers.StringMatcher delegate(); 269 create(Matchers.StringMatcher delegate)270 public static RequestedServerNameMatcher create(Matchers.StringMatcher delegate) { 271 return new AutoValue_GrpcAuthorizationEngine_RequestedServerNameMatcher(delegate); 272 } 273 274 @Override matches(EvaluateArgs args)275 public boolean matches(EvaluateArgs args) { 276 return delegate().matches(args.getRequestedServerName()); 277 } 278 } 279 280 private static final class EvaluateArgs { 281 private final Metadata metadata; 282 private final ServerCall<?,?> serverCall; 283 // https://github.com/envoyproxy/envoy/blob/63619d578e1abe0c1725ea28ba02f361466662e1/api/envoy/config/rbac/v3/rbac.proto#L238-L240 284 private static final int URI_SAN = 6; 285 private static final int DNS_SAN = 2; 286 EvaluateArgs(Metadata metadata, ServerCall<?,?> serverCall)287 private EvaluateArgs(Metadata metadata, ServerCall<?,?> serverCall) { 288 this.metadata = metadata; 289 this.serverCall = serverCall; 290 } 291 getPath()292 private String getPath() { 293 return "/" + serverCall.getMethodDescriptor().getFullMethodName(); 294 } 295 296 /** 297 * Returns null for unauthenticated connection. 298 * Returns empty string collection if no valid certificate and no 299 * principal names we are interested in. 300 * https://github.com/envoyproxy/envoy/blob/0fae6970ddaf93f024908ba304bbd2b34e997a51/envoy/ssl/connection.h#L70 301 */ 302 @Nullable getPrincipalNames()303 private Collection<String> getPrincipalNames() { 304 SSLSession sslSession = serverCall.getAttributes().get(Grpc.TRANSPORT_ATTR_SSL_SESSION); 305 if (sslSession == null) { 306 return null; 307 } 308 try { 309 Certificate[] certs = sslSession.getPeerCertificates(); 310 if (certs == null || certs.length < 1) { 311 return Collections.singleton(""); 312 } 313 X509Certificate cert = (X509Certificate)certs[0]; 314 if (cert == null) { 315 return Collections.singleton(""); 316 } 317 Collection<List<?>> names = cert.getSubjectAlternativeNames(); 318 List<String> principalNames = new ArrayList<>(); 319 if (names != null) { 320 for (List<?> name : names) { 321 if (URI_SAN == (Integer) name.get(0)) { 322 principalNames.add((String) name.get(1)); 323 } 324 } 325 if (!principalNames.isEmpty()) { 326 return Collections.unmodifiableCollection(principalNames); 327 } 328 for (List<?> name : names) { 329 if (DNS_SAN == (Integer) name.get(0)) { 330 principalNames.add((String) name.get(1)); 331 } 332 } 333 if (!principalNames.isEmpty()) { 334 return Collections.unmodifiableCollection(principalNames); 335 } 336 } 337 if (cert.getSubjectDN() == null || cert.getSubjectDN().getName() == null) { 338 return Collections.singleton(""); 339 } 340 return Collections.singleton(cert.getSubjectDN().getName()); 341 } catch (SSLPeerUnverifiedException | CertificateParsingException ex) { 342 log.log(Level.FINE, "Unexpected getPrincipalNames error.", ex); 343 return Collections.singleton(""); 344 } 345 } 346 347 @Nullable getHeader(String headerName)348 private String getHeader(String headerName) { 349 headerName = headerName.toLowerCase(Locale.ROOT); 350 if ("te".equals(headerName)) { 351 return null; 352 } 353 if (":authority".equals(headerName)) { 354 headerName = "host"; 355 } 356 if ("host".equals(headerName)) { 357 return serverCall.getAuthority(); 358 } 359 if (":path".equals(headerName)) { 360 return getPath(); 361 } 362 if (":method".equals(headerName)) { 363 return "POST"; 364 } 365 return deserializeHeader(headerName); 366 } 367 368 @Nullable deserializeHeader(String headerName)369 private String deserializeHeader(String headerName) { 370 if (headerName.endsWith(Metadata.BINARY_HEADER_SUFFIX)) { 371 Metadata.Key<byte[]> key; 372 try { 373 key = Metadata.Key.of(headerName, Metadata.BINARY_BYTE_MARSHALLER); 374 } catch (IllegalArgumentException e) { 375 return null; 376 } 377 Iterable<byte[]> values = metadata.getAll(key); 378 if (values == null) { 379 return null; 380 } 381 List<String> encoded = new ArrayList<>(); 382 for (byte[] v : values) { 383 encoded.add(BaseEncoding.base64().omitPadding().encode(v)); 384 } 385 return Joiner.on(",").join(encoded); 386 } 387 Metadata.Key<String> key; 388 try { 389 key = Metadata.Key.of(headerName, Metadata.ASCII_STRING_MARSHALLER); 390 } catch (IllegalArgumentException e) { 391 return null; 392 } 393 Iterable<String> values = metadata.getAll(key); 394 return values == null ? null : Joiner.on(",").join(values); 395 } 396 getDestinationIp()397 private InetAddress getDestinationIp() { 398 SocketAddress addr = serverCall.getAttributes().get(Grpc.TRANSPORT_ATTR_LOCAL_ADDR); 399 return addr == null ? null : ((InetSocketAddress) addr).getAddress(); 400 } 401 getSourceIp()402 private InetAddress getSourceIp() { 403 SocketAddress addr = serverCall.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR); 404 return addr == null ? null : ((InetSocketAddress) addr).getAddress(); 405 } 406 getDestinationPort()407 private int getDestinationPort() { 408 SocketAddress addr = serverCall.getAttributes().get(Grpc.TRANSPORT_ATTR_LOCAL_ADDR); 409 return addr == null ? -1 : ((InetSocketAddress) addr).getPort(); 410 } 411 getRequestedServerName()412 private String getRequestedServerName() { 413 return ""; 414 } 415 } 416 417 public interface Matcher { matches(EvaluateArgs args)418 boolean matches(EvaluateArgs args); 419 } 420 421 @AutoValue 422 public abstract static class OrMatcher implements Matcher { anyMatch()423 public abstract ImmutableList<? extends Matcher> anyMatch(); 424 425 /** Matches when any of the matcher matches. */ create(List<? extends Matcher> matchers)426 public static OrMatcher create(List<? extends Matcher> matchers) { 427 checkNotNull(matchers, "matchers"); 428 for (Matcher matcher : matchers) { 429 checkNotNull(matcher, "matcher"); 430 } 431 return new AutoValue_GrpcAuthorizationEngine_OrMatcher(ImmutableList.copyOf(matchers)); 432 } 433 create(Matcher...matchers)434 public static OrMatcher create(Matcher...matchers) { 435 return OrMatcher.create(Arrays.asList(matchers)); 436 } 437 438 @Override matches(EvaluateArgs args)439 public boolean matches(EvaluateArgs args) { 440 for (Matcher m : anyMatch()) { 441 if (m.matches(args)) { 442 return true; 443 } 444 } 445 return false; 446 } 447 } 448 449 @AutoValue 450 public abstract static class AndMatcher implements Matcher { allMatch()451 public abstract ImmutableList<? extends Matcher> allMatch(); 452 453 /** Matches when all of the matchers match. */ create(List<? extends Matcher> matchers)454 public static AndMatcher create(List<? extends Matcher> matchers) { 455 checkNotNull(matchers, "matchers"); 456 for (Matcher matcher : matchers) { 457 checkNotNull(matcher, "matcher"); 458 } 459 return new AutoValue_GrpcAuthorizationEngine_AndMatcher(ImmutableList.copyOf(matchers)); 460 } 461 create(Matcher...matchers)462 public static AndMatcher create(Matcher...matchers) { 463 return AndMatcher.create(Arrays.asList(matchers)); 464 } 465 466 @Override matches(EvaluateArgs args)467 public boolean matches(EvaluateArgs args) { 468 for (Matcher m : allMatch()) { 469 if (!m.matches(args)) { 470 return false; 471 } 472 } 473 return true; 474 } 475 } 476 477 /** Always true matcher.*/ 478 @AutoValue 479 public abstract static class AlwaysTrueMatcher implements Matcher { 480 public static AlwaysTrueMatcher INSTANCE = 481 new AutoValue_GrpcAuthorizationEngine_AlwaysTrueMatcher(); 482 483 @Override matches(EvaluateArgs args)484 public boolean matches(EvaluateArgs args) { 485 return true; 486 } 487 } 488 489 /** Negate matcher.*/ 490 @AutoValue 491 public abstract static class InvertMatcher implements Matcher { toInvertMatcher()492 public abstract Matcher toInvertMatcher(); 493 create(Matcher matcher)494 public static InvertMatcher create(Matcher matcher) { 495 return new AutoValue_GrpcAuthorizationEngine_InvertMatcher(matcher); 496 } 497 498 @Override matches(EvaluateArgs args)499 public boolean matches(EvaluateArgs args) { 500 return !toInvertMatcher().matches(args); 501 } 502 } 503 } 504