• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

grpc / grpc-java / #18906

20 Nov 2023 06:06PM UTC coverage: 88.197% (-0.03%) from 88.226%
#18906

push

github

ejona86
Remove getSubjectDN(), which is deprecated in Java 17

30361 of 34424 relevant lines covered (88.2%)

0.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.41
/../xds/src/main/java/io/grpc/xds/internal/rbac/engine/GrpcAuthorizationEngine.java
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());
1✔
57

58
  private final AuthConfig authConfig;
59

60
  /** Instantiated with envoy policyMatcher configuration. */
61
  public GrpcAuthorizationEngine(AuthConfig authConfig) {
1✔
62
    this.authConfig = authConfig;
1✔
63
  }
1✔
64

65
  /** Return the auth decision for the request argument against the policies. */
66
  public AuthDecision evaluate(Metadata metadata, ServerCall<?,?> serverCall) {
67
    checkNotNull(metadata, "metadata");
1✔
68
    checkNotNull(serverCall, "serverCall");
1✔
69
    String firstMatch = null;
1✔
70
    EvaluateArgs args = new EvaluateArgs(metadata, serverCall);
1✔
71
    for (PolicyMatcher policyMatcher : authConfig.policies()) {
1✔
72
      if (policyMatcher.matches(args)) {
1✔
73
        firstMatch = policyMatcher.name();
1✔
74
        break;
1✔
75
      }
76
    }
1✔
77
    Action decisionType = Action.DENY;
1✔
78
    if (Action.DENY.equals(authConfig.action()) == (firstMatch == null)) {
1✔
79
      decisionType = Action.ALLOW;
1✔
80
    }
81
    return AuthDecision.create(decisionType, firstMatch);
1✔
82
  }
83

84
  public enum Action {
1✔
85
    ALLOW,
1✔
86
    DENY,
1✔
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 {
1✔
94
    public abstract Action decision();
95

96
    @Nullable
97
    public abstract String matchingPolicyName();
98

99
    static AuthDecision create(Action decisionType, @Nullable String matchingPolicy) {
100
      return new AutoValue_GrpcAuthorizationEngine_AuthDecision(decisionType, matchingPolicy);
1✔
101
    }
102
  }
103

104
  /** Represents authorization config policy that the engine will evaluate against. */
105
  @AutoValue
106
  public abstract static class AuthConfig {
1✔
107
    public abstract ImmutableList<PolicyMatcher> policies();
108

109
    public abstract Action action();
110

111
    public static AuthConfig create(List<PolicyMatcher> policies, Action action) {
112
      return new AutoValue_GrpcAuthorizationEngine_AuthConfig(
1✔
113
          ImmutableList.copyOf(policies), action);
1✔
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 {
1✔
127
    public abstract String name();
128

129
    public abstract OrMatcher permissions();
130

131
    public abstract OrMatcher principals();
132

133
    /** Constructs a matcher for one RBAC policy. */
134
    public static PolicyMatcher create(String name, OrMatcher permissions, OrMatcher principals) {
135
      return new AutoValue_GrpcAuthorizationEngine_PolicyMatcher(name, permissions, principals);
1✔
136
    }
137

138
    @Override
139
    public boolean matches(EvaluateArgs args) {
140
      return permissions().matches(args) && principals().matches(args);
1✔
141
    }
142
  }
143

144
  @AutoValue
145
  public abstract static class AuthenticatedMatcher implements Matcher {
1✔
146
    @Nullable
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
     * */
153
    public static AuthenticatedMatcher create(@Nullable Matchers.StringMatcher delegate) {
154
      return new AutoValue_GrpcAuthorizationEngine_AuthenticatedMatcher(delegate);
1✔
155
    }
156

157
    @Override
158
    public boolean matches(EvaluateArgs args) {
159
      Collection<String> principalNames = args.getPrincipalNames();
1✔
160
      log.log(Level.FINER, "Matching principal names: {0}", new Object[]{principalNames});
1✔
161
      // Null means unauthenticated connection.
162
      if (principalNames == null) {
1✔
163
        return false;
1✔
164
      }
165
      // Connection is authenticated, so returns match when delegated string matcher is not present.
166
      if (delegate() == null) {
1✔
167
        return true;
1✔
168
      }
169
      for (String name : principalNames) {
1✔
170
        if (delegate().matches(name)) {
1✔
171
          return true;
1✔
172
        }
173
      }
1✔
174
      return false;
1✔
175
    }
176
  }
177

178
  @AutoValue
179
  public abstract static class DestinationIpMatcher implements Matcher {
1✔
180
    public abstract Matchers.CidrMatcher delegate();
181

182
    public static DestinationIpMatcher create(Matchers.CidrMatcher delegate) {
183
      return new AutoValue_GrpcAuthorizationEngine_DestinationIpMatcher(delegate);
1✔
184
    }
185

186
    @Override
187
    public boolean matches(EvaluateArgs args) {
188
      return delegate().matches(args.getDestinationIp());
1✔
189
    }
190
  }
191

192
  @AutoValue
193
  public abstract static class SourceIpMatcher implements Matcher {
1✔
194
    public abstract Matchers.CidrMatcher delegate();
195

196
    public static SourceIpMatcher create(Matchers.CidrMatcher delegate) {
197
      return new AutoValue_GrpcAuthorizationEngine_SourceIpMatcher(delegate);
1✔
198
    }
199

200
    @Override
201
    public boolean matches(EvaluateArgs args) {
202
      return delegate().matches(args.getSourceIp());
1✔
203
    }
204
  }
205

206
  @AutoValue
207
  public abstract static class PathMatcher implements Matcher {
1✔
208
    public abstract Matchers.StringMatcher delegate();
209

210
    public static PathMatcher create(Matchers.StringMatcher delegate) {
211
      return new AutoValue_GrpcAuthorizationEngine_PathMatcher(delegate);
1✔
212
    }
213

214
    @Override
215
    public boolean matches(EvaluateArgs args) {
216
      return delegate().matches(args.getPath());
1✔
217
    }
218
  }
219

220
  @AutoValue
221
  public abstract static class AuthHeaderMatcher implements Matcher {
1✔
222
    public abstract Matchers.HeaderMatcher delegate();
223

224
    public static AuthHeaderMatcher create(Matchers.HeaderMatcher delegate) {
225
      return new AutoValue_GrpcAuthorizationEngine_AuthHeaderMatcher(delegate);
1✔
226
    }
227

228
    @Override
229
    public boolean matches(EvaluateArgs args) {
230
      return delegate().matches(args.getHeader(delegate().name()));
1✔
231
    }
232
  }
233

234
  @AutoValue
235
  public abstract static class DestinationPortMatcher implements Matcher {
1✔
236
    public abstract int port();
237

238
    public static DestinationPortMatcher create(int port) {
239
      return new AutoValue_GrpcAuthorizationEngine_DestinationPortMatcher(port);
1✔
240
    }
241

242
    @Override
243
    public boolean matches(EvaluateArgs args) {
244
      return port() == args.getDestinationPort();
1✔
245
    }
246
  }
247

248
  @AutoValue
249
  public abstract static class DestinationPortRangeMatcher implements Matcher {
1✔
250
    public abstract int start();
251

252
    public abstract int end();
253

254
    /** Start of the range is inclusive. End of the range is exclusive.*/
255
    public static DestinationPortRangeMatcher create(int start, int end) {
256
      return new AutoValue_GrpcAuthorizationEngine_DestinationPortRangeMatcher(start, end);
1✔
257
    }
258

259
    @Override
260
    public boolean matches(EvaluateArgs args) {
261
      int port = args.getDestinationPort();
1✔
262
      return  port >= start() && port < end();
1✔
263
    }
264
  }
265

266
  @AutoValue
267
  public abstract static class RequestedServerNameMatcher implements Matcher {
1✔
268
    public abstract Matchers.StringMatcher delegate();
269

270
    public static RequestedServerNameMatcher create(Matchers.StringMatcher delegate) {
271
      return new AutoValue_GrpcAuthorizationEngine_RequestedServerNameMatcher(delegate);
1✔
272
    }
273

274
    @Override
275
    public boolean matches(EvaluateArgs args) {
276
      return delegate().matches(args.getRequestedServerName());
1✔
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

287
    private EvaluateArgs(Metadata metadata, ServerCall<?,?> serverCall) {
1✔
288
      this.metadata = metadata;
1✔
289
      this.serverCall = serverCall;
1✔
290
    }
1✔
291

292
    private String getPath() {
293
      return "/" + serverCall.getMethodDescriptor().getFullMethodName();
1✔
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
303
    private Collection<String> getPrincipalNames() {
304
      SSLSession sslSession = serverCall.getAttributes().get(Grpc.TRANSPORT_ATTR_SSL_SESSION);
1✔
305
      if (sslSession == null) {
1✔
306
        return null;
1✔
307
      }
308
      try {
309
        Certificate[] certs = sslSession.getPeerCertificates();
1✔
310
        if (certs == null || certs.length < 1) {
1✔
311
          return Collections.singleton("");
×
312
        }
313
        X509Certificate cert = (X509Certificate)certs[0];
1✔
314
        if (cert == null) {
1✔
315
          return Collections.singleton("");
×
316
        }
317
        Collection<List<?>> names = cert.getSubjectAlternativeNames();
1✔
318
        List<String> principalNames = new ArrayList<>();
1✔
319
        if (names != null) {
1✔
320
          for (List<?> name : names) {
1✔
321
            if (URI_SAN == (Integer) name.get(0)) {
1✔
322
              principalNames.add((String) name.get(1));
1✔
323
            }
324
          }
1✔
325
          if (!principalNames.isEmpty()) {
1✔
326
            return Collections.unmodifiableCollection(principalNames);
1✔
327
          }
328
          for (List<?> name : names) {
1✔
329
            if (DNS_SAN == (Integer) name.get(0)) {
1✔
330
              principalNames.add((String) name.get(1));
1✔
331
            }
332
          }
1✔
333
          if (!principalNames.isEmpty()) {
1✔
334
            return Collections.unmodifiableCollection(principalNames);
1✔
335
          }
336
        }
337
        if (cert.getSubjectX500Principal() == null
1✔
338
            || cert.getSubjectX500Principal().getName() == null) {
1✔
339
          return Collections.singleton("");
1✔
340
        }
341
        return Collections.singleton(cert.getSubjectX500Principal().getName());
1✔
342
      } catch (SSLPeerUnverifiedException | CertificateParsingException ex) {
×
343
        log.log(Level.FINE, "Unexpected getPrincipalNames error.", ex);
×
344
        return Collections.singleton("");
×
345
      }
346
    }
347

348
    @Nullable
349
    private String getHeader(String headerName) {
350
      headerName = headerName.toLowerCase(Locale.ROOT);
1✔
351
      if ("te".equals(headerName)) {
1✔
352
        return null;
×
353
      }
354
      if (":authority".equals(headerName)) {
1✔
355
        headerName = "host";
×
356
      }
357
      if ("host".equals(headerName)) {
1✔
358
        return serverCall.getAuthority();
1✔
359
      }
360
      if (":path".equals(headerName)) {
1✔
361
        return getPath();
1✔
362
      }
363
      if (":method".equals(headerName)) {
1✔
364
        return "POST";
1✔
365
      }
366
      return deserializeHeader(headerName);
1✔
367
    }
368

369
    @Nullable
370
    private String deserializeHeader(String headerName) {
371
      if (headerName.endsWith(Metadata.BINARY_HEADER_SUFFIX)) {
1✔
372
        Metadata.Key<byte[]> key;
373
        try {
374
          key = Metadata.Key.of(headerName, Metadata.BINARY_BYTE_MARSHALLER);
1✔
375
        } catch (IllegalArgumentException e) {
×
376
          return null;
×
377
        }
1✔
378
        Iterable<byte[]> values = metadata.getAll(key);
1✔
379
        if (values == null) {
1✔
380
          return null;
1✔
381
        }
382
        List<String> encoded = new ArrayList<>();
1✔
383
        for (byte[] v : values) {
1✔
384
          encoded.add(BaseEncoding.base64().omitPadding().encode(v));
1✔
385
        }
1✔
386
        return Joiner.on(",").join(encoded);
1✔
387
      }
388
      Metadata.Key<String> key;
389
      try {
390
        key = Metadata.Key.of(headerName, Metadata.ASCII_STRING_MARSHALLER);
1✔
391
      } catch (IllegalArgumentException e) {
×
392
        return null;
×
393
      }
1✔
394
      Iterable<String> values = metadata.getAll(key);
1✔
395
      return values == null ? null : Joiner.on(",").join(values);
1✔
396
    }
397

398
    private InetAddress getDestinationIp() {
399
      SocketAddress addr = serverCall.getAttributes().get(Grpc.TRANSPORT_ATTR_LOCAL_ADDR);
1✔
400
      return addr == null ? null : ((InetSocketAddress) addr).getAddress();
1✔
401
    }
402

403
    private InetAddress getSourceIp() {
404
      SocketAddress addr = serverCall.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR);
1✔
405
      return addr == null ? null : ((InetSocketAddress) addr).getAddress();
1✔
406
    }
407

408
    private int getDestinationPort() {
409
      SocketAddress addr = serverCall.getAttributes().get(Grpc.TRANSPORT_ATTR_LOCAL_ADDR);
1✔
410
      return addr == null ? -1 : ((InetSocketAddress) addr).getPort();
1✔
411
    }
412

413
    private String getRequestedServerName() {
414
      return "";
1✔
415
    }
416
  }
417

418
  public interface Matcher {
419
    boolean matches(EvaluateArgs args);
420
  }
421

422
  @AutoValue
423
  public abstract static class OrMatcher implements Matcher {
1✔
424
    public abstract ImmutableList<? extends Matcher> anyMatch();
425

426
    /** Matches when any of the matcher matches. */
427
    public static OrMatcher create(List<? extends Matcher> matchers) {
428
      checkNotNull(matchers, "matchers");
1✔
429
      for (Matcher matcher : matchers) {
1✔
430
        checkNotNull(matcher, "matcher");
1✔
431
      }
1✔
432
      return new AutoValue_GrpcAuthorizationEngine_OrMatcher(ImmutableList.copyOf(matchers));
1✔
433
    }
434

435
    public static OrMatcher create(Matcher...matchers) {
436
      return OrMatcher.create(Arrays.asList(matchers));
1✔
437
    }
438

439
    @Override
440
    public boolean matches(EvaluateArgs args) {
441
      for (Matcher m : anyMatch()) {
1✔
442
        if (m.matches(args)) {
1✔
443
          return true;
1✔
444
        }
445
      }
1✔
446
      return false;
1✔
447
    }
448
  }
449

450
  @AutoValue
451
  public abstract static class AndMatcher implements Matcher {
1✔
452
    public abstract ImmutableList<? extends Matcher> allMatch();
453

454
    /** Matches when all of the matchers match. */
455
    public static AndMatcher create(List<? extends Matcher> matchers) {
456
      checkNotNull(matchers, "matchers");
1✔
457
      for (Matcher matcher : matchers) {
1✔
458
        checkNotNull(matcher, "matcher");
1✔
459
      }
1✔
460
      return new AutoValue_GrpcAuthorizationEngine_AndMatcher(ImmutableList.copyOf(matchers));
1✔
461
    }
462

463
    public static AndMatcher create(Matcher...matchers) {
464
      return AndMatcher.create(Arrays.asList(matchers));
1✔
465
    }
466

467
    @Override
468
    public boolean matches(EvaluateArgs args) {
469
      for (Matcher m : allMatch()) {
1✔
470
        if (!m.matches(args)) {
1✔
471
          return false;
1✔
472
        }
473
      }
1✔
474
      return true;
1✔
475
    }
476
  }
477

478
  /** Always true matcher.*/
479
  @AutoValue
480
  public abstract static class AlwaysTrueMatcher implements Matcher {
1✔
481
    public static AlwaysTrueMatcher INSTANCE =
1✔
482
        new AutoValue_GrpcAuthorizationEngine_AlwaysTrueMatcher();
483

484
    @Override
485
    public boolean matches(EvaluateArgs args) {
486
      return true;
1✔
487
    }
488
  }
489

490
  /** Negate matcher.*/
491
  @AutoValue
492
  public abstract static class InvertMatcher implements Matcher {
1✔
493
    public abstract Matcher toInvertMatcher();
494

495
    public static InvertMatcher create(Matcher matcher) {
496
      return new AutoValue_GrpcAuthorizationEngine_InvertMatcher(matcher);
1✔
497
    }
498

499
    @Override
500
    public boolean matches(EvaluateArgs args) {
501
      return !toInvertMatcher().matches(args);
1✔
502
    }
503
  }
504
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc