• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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