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

grpc / grpc-java / #19827

23 May 2025 10:54AM UTC coverage: 88.613% (-0.002%) from 88.615%
#19827

push

github

web-flow
xds: Change how xDS filters are created by introducing Filter.Provider (#11883) (#12089)

This is the first step towards supporting filter state retention in
Java. The mechanism will be similar to the one described in [A83]
(https://github.com/grpc/proposal/blob/master/A83-xds-gcp-authn-filter.md#filter-call-credentials-cache)
for C-core, and will serve the same purpose. However, the
implementation details are very different due to the different nature
of xDS HTTP filter support in C-core and Java.

In Java, xDS HTTP filters are backed by classes implementing
`io.grpc.xds.Filter`, from here just called "Filters". To support
Filter state retention (next PR), Java's xDS implementation must be
able to create unique Filter instances per:
- Per HCM
  `envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager`
- Per filter name as specified in
  `envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter.name`

This PR **does not** implements Filter state retention, but lays the
groundwork for it by changing how filters are registered and
instantiated. To achieve this, all existing Filter classes had to be
updated to the new instantiation mechanism described below.

Prior to these this PR, Filters had no livecycle. FilterRegistry
provided singleton instances for a given typeUrl. This PR introduces
a new interface `Filter.Provider`, which instantiates Filter classes.
All functionality that doesn't need an instance of a Filter is moved
to the Filter.Provider. This includes parsing filter config proto
into FilterConfig and determining the filter kind
(client-side, server-side, or both).

This PR is limited to refactoring, and there's no changes to the
existing behavior. Note that all Filter Providers still return
singleton Filter instances. However, with this PR, it is now possible
to create Providers that return a new Filter instance each time
`newInstance` is called.

Co-authored-by: Sergii Tkachenko <sergiitk@google.com>

34286 of 38692 relevant lines covered (88.61%)

0.89 hits per line

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

86.23
/../xds/src/main/java/io/grpc/xds/RbacFilter.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;
18

19
import static com.google.common.base.Preconditions.checkNotNull;
20

21
import com.google.protobuf.Any;
22
import com.google.protobuf.InvalidProtocolBufferException;
23
import com.google.protobuf.Message;
24
import io.envoyproxy.envoy.config.core.v3.CidrRange;
25
import io.envoyproxy.envoy.config.rbac.v3.Permission;
26
import io.envoyproxy.envoy.config.rbac.v3.Policy;
27
import io.envoyproxy.envoy.config.rbac.v3.Principal;
28
import io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBAC;
29
import io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBACPerRoute;
30
import io.envoyproxy.envoy.type.v3.Int32Range;
31
import io.grpc.Metadata;
32
import io.grpc.ServerCall;
33
import io.grpc.ServerCallHandler;
34
import io.grpc.ServerInterceptor;
35
import io.grpc.Status;
36
import io.grpc.xds.internal.MatcherParser;
37
import io.grpc.xds.internal.Matchers;
38
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine;
39
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AlwaysTrueMatcher;
40
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AndMatcher;
41
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AuthConfig;
42
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AuthDecision;
43
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AuthHeaderMatcher;
44
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.AuthenticatedMatcher;
45
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.DestinationIpMatcher;
46
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.DestinationPortMatcher;
47
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.DestinationPortRangeMatcher;
48
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.InvertMatcher;
49
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.Matcher;
50
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.OrMatcher;
51
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.PathMatcher;
52
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.PolicyMatcher;
53
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.RequestedServerNameMatcher;
54
import io.grpc.xds.internal.rbac.engine.GrpcAuthorizationEngine.SourceIpMatcher;
55
import java.net.InetAddress;
56
import java.net.UnknownHostException;
57
import java.util.ArrayList;
58
import java.util.List;
59
import java.util.Map;
60
import java.util.Map.Entry;
61
import java.util.logging.Level;
62
import java.util.logging.Logger;
63
import java.util.stream.Collectors;
64
import javax.annotation.Nullable;
65

66
/** RBAC Http filter implementation. */
67
final class RbacFilter implements Filter {
68
  private static final Logger logger = Logger.getLogger(RbacFilter.class.getName());
1✔
69

70
  private static final RbacFilter INSTANCE = new RbacFilter();
1✔
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

78
  private RbacFilter() {}
79

80
  static final class Provider implements Filter.Provider {
1✔
81
    @Override
82
    public String[] typeUrls() {
83
      return new String[] {TYPE_URL, TYPE_URL_OVERRIDE_CONFIG};
1✔
84
    }
85

86
    @Override
87
    public boolean isServerFilter() {
88
      return true;
1✔
89
    }
90

91
    @Override
92
    public RbacFilter newInstance() {
93
      return INSTANCE;
1✔
94
    }
95

96
    @Override
97
    public ConfigOrError<RbacConfig> parseFilterConfig(Message rawProtoMessage) {
98
      RBAC rbacProto;
99
      if (!(rawProtoMessage instanceof Any)) {
1✔
100
        return ConfigOrError.fromError("Invalid config type: " + rawProtoMessage.getClass());
×
101
      }
102
      Any anyMessage = (Any) rawProtoMessage;
1✔
103
      try {
104
        rbacProto = anyMessage.unpack(RBAC.class);
1✔
105
      } catch (InvalidProtocolBufferException e) {
×
106
        return ConfigOrError.fromError("Invalid proto: " + e);
×
107
      }
1✔
108
      return parseRbacConfig(rbacProto);
1✔
109
    }
110

111
    @Override
112
    public ConfigOrError<RbacConfig> parseFilterConfigOverride(Message rawProtoMessage) {
113
      RBACPerRoute rbacPerRoute;
114
      if (!(rawProtoMessage instanceof Any)) {
1✔
115
        return ConfigOrError.fromError("Invalid config type: " + rawProtoMessage.getClass());
×
116
      }
117
      Any anyMessage = (Any) rawProtoMessage;
1✔
118
      try {
119
        rbacPerRoute = anyMessage.unpack(RBACPerRoute.class);
1✔
120
      } catch (InvalidProtocolBufferException e) {
×
121
        return ConfigOrError.fromError("Invalid proto: " + e);
×
122
      }
1✔
123
      if (rbacPerRoute.hasRbac()) {
1✔
124
        return parseRbacConfig(rbacPerRoute.getRbac());
1✔
125
      } else {
126
        return ConfigOrError.fromConfig(RbacConfig.create(null));
1✔
127
      }
128
    }
129

130
    static ConfigOrError<RbacConfig> parseRbacConfig(RBAC rbac) {
131
      if (!rbac.hasRules()) {
1✔
132
        return ConfigOrError.fromConfig(RbacConfig.create(null));
×
133
      }
134
      io.envoyproxy.envoy.config.rbac.v3.RBAC rbacConfig = rbac.getRules();
1✔
135
      GrpcAuthorizationEngine.Action authAction;
136
      switch (rbacConfig.getAction()) {
1✔
137
        case ALLOW:
138
          authAction = GrpcAuthorizationEngine.Action.ALLOW;
1✔
139
          break;
1✔
140
        case DENY:
141
          authAction = GrpcAuthorizationEngine.Action.DENY;
1✔
142
          break;
1✔
143
        case LOG:
144
          return ConfigOrError.fromConfig(RbacConfig.create(null));
1✔
145
        case UNRECOGNIZED:
146
        default:
147
          return ConfigOrError.fromError(
×
148
              "Unknown rbacConfig action type: " + rbacConfig.getAction());
×
149
      }
150
      List<GrpcAuthorizationEngine.PolicyMatcher> policyMatchers = new ArrayList<>();
1✔
151
      List<Entry<String, Policy>> sortedPolicyEntries = rbacConfig.getPoliciesMap().entrySet()
1✔
152
          .stream()
1✔
153
          .sorted((a,b) -> a.getKey().compareTo(b.getKey()))
1✔
154
          .collect(Collectors.toList());
1✔
155
      for (Map.Entry<String, Policy> entry: sortedPolicyEntries) {
1✔
156
        try {
157
          Policy policy = entry.getValue();
1✔
158
          if (policy.hasCondition() || policy.hasCheckedCondition()) {
1✔
159
            return ConfigOrError.fromError(
1✔
160
                "Policy.condition and Policy.checked_condition must not set: " + entry.getKey());
1✔
161
          }
162
          policyMatchers.add(PolicyMatcher.create(entry.getKey(),
1✔
163
              parsePermissionList(policy.getPermissionsList()),
1✔
164
              parsePrincipalList(policy.getPrincipalsList())));
1✔
165
        } catch (Exception e) {
1✔
166
          return ConfigOrError.fromError("Encountered error parsing policy: " + e);
1✔
167
        }
1✔
168
      }
1✔
169
      return ConfigOrError.fromConfig(RbacConfig.create(
1✔
170
          AuthConfig.create(policyMatchers, authAction)));
1✔
171
    }
172
  }
173

174
  @Nullable
175
  @Override
176
  public ServerInterceptor buildServerInterceptor(FilterConfig config,
177
                                                  @Nullable FilterConfig overrideConfig) {
178
    checkNotNull(config, "config");
1✔
179
    if (overrideConfig != null) {
1✔
180
      config = overrideConfig;
1✔
181
    }
182
    AuthConfig authConfig = ((RbacConfig) config).authConfig();
1✔
183
    return authConfig == null ? null : generateAuthorizationInterceptor(authConfig);
1✔
184
  }
185

186
  private ServerInterceptor generateAuthorizationInterceptor(AuthConfig config) {
187
    checkNotNull(config, "config");
1✔
188
    final GrpcAuthorizationEngine authEngine = new GrpcAuthorizationEngine(config);
1✔
189
    return new ServerInterceptor() {
1✔
190
        @Override
191
        public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
192
                final ServerCall<ReqT, RespT> call,
193
                final Metadata headers, ServerCallHandler<ReqT, RespT> next) {
194
          AuthDecision authResult = authEngine.evaluate(headers, call);
1✔
195
          if (logger.isLoggable(Level.FINE)) {
1✔
196
            logger.log(Level.FINE,
×
197
                "Authorization result for serverCall {0}: {1}, matching policy: {2}.",
198
                new Object[]{call, authResult.decision(), authResult.matchingPolicyName()});
×
199
          }
200
          if (GrpcAuthorizationEngine.Action.DENY.equals(authResult.decision())) {
1✔
201
            Status status = Status.PERMISSION_DENIED.withDescription("Access Denied");
1✔
202
            call.close(status, new Metadata());
1✔
203
            return new ServerCall.Listener<ReqT>(){};
1✔
204
          }
205
          return next.startCall(call, headers);
1✔
206
        }
207
    };
208
  }
209

210
  private static OrMatcher parsePermissionList(List<Permission> permissions) {
211
    List<Matcher> anyMatch = new ArrayList<>();
1✔
212
    for (Permission permission : permissions) {
1✔
213
      anyMatch.add(parsePermission(permission));
1✔
214
    }
1✔
215
    return OrMatcher.create(anyMatch);
1✔
216
  }
217

218
  private static Matcher parsePermission(Permission permission) {
219
    switch (permission.getRuleCase()) {
1✔
220
      case AND_RULES:
221
        List<Matcher> andMatch = new ArrayList<>();
1✔
222
        for (Permission p : permission.getAndRules().getRulesList()) {
1✔
223
          andMatch.add(parsePermission(p));
1✔
224
        }
1✔
225
        return AndMatcher.create(andMatch);
1✔
226
      case OR_RULES:
227
        return parsePermissionList(permission.getOrRules().getRulesList());
1✔
228
      case ANY:
229
        return AlwaysTrueMatcher.INSTANCE;
1✔
230
      case HEADER:
231
        return parseHeaderMatcher(permission.getHeader());
1✔
232
      case URL_PATH:
233
        return parsePathMatcher(permission.getUrlPath());
1✔
234
      case DESTINATION_IP:
235
        return createDestinationIpMatcher(permission.getDestinationIp());
1✔
236
      case DESTINATION_PORT:
237
        return createDestinationPortMatcher(permission.getDestinationPort());
1✔
238
      case DESTINATION_PORT_RANGE:
239
        return parseDestinationPortRangeMatcher(permission.getDestinationPortRange());
1✔
240
      case NOT_RULE:
241
        return InvertMatcher.create(parsePermission(permission.getNotRule()));
1✔
242
      case METADATA: // hard coded, never match.
243
        return InvertMatcher.create(AlwaysTrueMatcher.INSTANCE);
1✔
244
      case REQUESTED_SERVER_NAME:
245
        return parseRequestedServerNameMatcher(permission.getRequestedServerName());
1✔
246
      case RULE_NOT_SET:
247
      default:
248
        throw new IllegalArgumentException(
1✔
249
                "Unknown permission rule case: " + permission.getRuleCase());
1✔
250
    }
251
  }
252

253
  private static OrMatcher parsePrincipalList(List<Principal> principals) {
254
    List<Matcher> anyMatch = new ArrayList<>();
1✔
255
    for (Principal principal: principals) {
1✔
256
      anyMatch.add(parsePrincipal(principal));
1✔
257
    }
1✔
258
    return OrMatcher.create(anyMatch);
1✔
259
  }
260

261
  private static Matcher parsePrincipal(Principal principal) {
262
    switch (principal.getIdentifierCase()) {
1✔
263
      case OR_IDS:
264
        return parsePrincipalList(principal.getOrIds().getIdsList());
×
265
      case AND_IDS:
266
        List<Matcher> nextMatchers = new ArrayList<>();
1✔
267
        for (Principal next : principal.getAndIds().getIdsList()) {
1✔
268
          nextMatchers.add(parsePrincipal(next));
1✔
269
        }
1✔
270
        return AndMatcher.create(nextMatchers);
1✔
271
      case ANY:
272
        return AlwaysTrueMatcher.INSTANCE;
1✔
273
      case AUTHENTICATED:
274
        return parseAuthenticatedMatcher(principal.getAuthenticated());
1✔
275
      case DIRECT_REMOTE_IP:
276
        return createSourceIpMatcher(principal.getDirectRemoteIp());
1✔
277
      case REMOTE_IP:
278
        return createSourceIpMatcher(principal.getRemoteIp());
1✔
279
      case SOURCE_IP:
280
        return createSourceIpMatcher(principal.getSourceIp());
1✔
281
      case HEADER:
282
        return parseHeaderMatcher(principal.getHeader());
1✔
283
      case NOT_ID:
284
        return InvertMatcher.create(parsePrincipal(principal.getNotId()));
1✔
285
      case URL_PATH:
286
        return parsePathMatcher(principal.getUrlPath());
1✔
287
      case METADATA: // hard coded, never match.
288
        return InvertMatcher.create(AlwaysTrueMatcher.INSTANCE);
1✔
289
      case IDENTIFIER_NOT_SET:
290
      default:
291
        throw new IllegalArgumentException(
×
292
                "Unknown principal identifier case: " + principal.getIdentifierCase());
×
293
    }
294
  }
295

296
  private static PathMatcher parsePathMatcher(
297
          io.envoyproxy.envoy.type.matcher.v3.PathMatcher proto) {
298
    switch (proto.getRuleCase()) {
1✔
299
      case PATH:
300
        return PathMatcher.create(MatcherParser.parseStringMatcher(proto.getPath()));
1✔
301
      case RULE_NOT_SET:
302
      default:
303
        throw new IllegalArgumentException(
×
304
                "Unknown path matcher rule type: " + proto.getRuleCase());
×
305
    }
306
  }
307

308
  private static RequestedServerNameMatcher parseRequestedServerNameMatcher(
309
          io.envoyproxy.envoy.type.matcher.v3.StringMatcher proto) {
310
    return RequestedServerNameMatcher.create(MatcherParser.parseStringMatcher(proto));
1✔
311
  }
312

313
  private static AuthHeaderMatcher parseHeaderMatcher(
314
          io.envoyproxy.envoy.config.route.v3.HeaderMatcher proto) {
315
    if (proto.getName().startsWith("grpc-")) {
1✔
316
      throw new IllegalArgumentException("Invalid header matcher config: [grpc-] prefixed "
1✔
317
          + "header name is not allowed.");
318
    }
319
    if (":scheme".equals(proto.getName())) {
1✔
320
      throw new IllegalArgumentException("Invalid header matcher config: header name [:scheme] "
×
321
          + "is not allowed.");
322
    }
323
    return AuthHeaderMatcher.create(MatcherParser.parseHeaderMatcher(proto));
1✔
324
  }
325

326
  private static AuthenticatedMatcher parseAuthenticatedMatcher(
327
          Principal.Authenticated proto) {
328
    Matchers.StringMatcher matcher = MatcherParser.parseStringMatcher(proto.getPrincipalName());
1✔
329
    return AuthenticatedMatcher.create(matcher);
1✔
330
  }
331

332
  private static DestinationPortMatcher createDestinationPortMatcher(int port) {
333
    return DestinationPortMatcher.create(port);
1✔
334
  }
335

336
  private static DestinationPortRangeMatcher parseDestinationPortRangeMatcher(Int32Range range) {
337
    return DestinationPortRangeMatcher.create(range.getStart(), range.getEnd());
1✔
338
  }
339

340
  private static DestinationIpMatcher createDestinationIpMatcher(CidrRange cidrRange) {
341
    return DestinationIpMatcher.create(Matchers.CidrMatcher.create(
1✔
342
            resolve(cidrRange), cidrRange.getPrefixLen().getValue()));
1✔
343
  }
344

345
  private static SourceIpMatcher createSourceIpMatcher(CidrRange cidrRange) {
346
    return SourceIpMatcher.create(Matchers.CidrMatcher.create(
1✔
347
            resolve(cidrRange), cidrRange.getPrefixLen().getValue()));
1✔
348
  }
349

350
  private static InetAddress resolve(CidrRange cidrRange) {
351
    try {
352
      return InetAddress.getByName(cidrRange.getAddressPrefix());
1✔
353
    } catch (UnknownHostException ex) {
×
354
      throw new IllegalArgumentException("IP address can not be found: " + ex);
×
355
    }
356
  }
357
}
358

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