• 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

71.23
/../xds/src/main/java/io/grpc/xds/GcpAuthenticationFilter.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 com.google.auth.oauth2.ComputeEngineCredentials;
20
import com.google.auth.oauth2.IdTokenCredentials;
21
import com.google.common.primitives.UnsignedLongs;
22
import com.google.protobuf.Any;
23
import com.google.protobuf.InvalidProtocolBufferException;
24
import com.google.protobuf.Message;
25
import io.envoyproxy.envoy.extensions.filters.http.gcp_authn.v3.Audience;
26
import io.envoyproxy.envoy.extensions.filters.http.gcp_authn.v3.GcpAuthnFilterConfig;
27
import io.envoyproxy.envoy.extensions.filters.http.gcp_authn.v3.TokenCacheConfig;
28
import io.grpc.CallCredentials;
29
import io.grpc.CallOptions;
30
import io.grpc.Channel;
31
import io.grpc.ClientCall;
32
import io.grpc.ClientInterceptor;
33
import io.grpc.CompositeCallCredentials;
34
import io.grpc.Metadata;
35
import io.grpc.MethodDescriptor;
36
import io.grpc.Status;
37
import io.grpc.auth.MoreCallCredentials;
38
import io.grpc.xds.MetadataRegistry.MetadataValueParser;
39
import java.util.LinkedHashMap;
40
import java.util.Map;
41
import java.util.concurrent.ScheduledExecutorService;
42
import java.util.function.Function;
43
import javax.annotation.Nullable;
44

45
/**
46
 * A {@link Filter} that injects a {@link CallCredentials} to handle
47
 * authentication for xDS credentials.
48
 */
49
final class GcpAuthenticationFilter implements Filter {
1✔
50

51
  static final String TYPE_URL =
52
      "type.googleapis.com/envoy.extensions.filters.http.gcp_authn.v3.GcpAuthnFilterConfig";
53

54
  static final class Provider implements Filter.Provider {
1✔
55
    @Override
56
    public String[] typeUrls() {
57
      return new String[]{TYPE_URL};
×
58
    }
59

60
    @Override
61
    public boolean isClientFilter() {
62
      return true;
1✔
63
    }
64

65
    @Override
66
    public GcpAuthenticationFilter newInstance() {
67
      return new GcpAuthenticationFilter();
×
68
    }
69

70
    @Override
71
    public ConfigOrError<GcpAuthenticationConfig> parseFilterConfig(Message rawProtoMessage) {
72
      GcpAuthnFilterConfig gcpAuthnProto;
73
      if (!(rawProtoMessage instanceof Any)) {
1✔
74
        return ConfigOrError.fromError("Invalid config type: " + rawProtoMessage.getClass());
1✔
75
      }
76
      Any anyMessage = (Any) rawProtoMessage;
1✔
77

78
      try {
79
        gcpAuthnProto = anyMessage.unpack(GcpAuthnFilterConfig.class);
1✔
80
      } catch (InvalidProtocolBufferException e) {
×
81
        return ConfigOrError.fromError("Invalid proto: " + e);
×
82
      }
1✔
83

84
      long cacheSize = 10;
1✔
85
      // Validate cache_config
86
      if (gcpAuthnProto.hasCacheConfig()) {
1✔
87
        TokenCacheConfig cacheConfig = gcpAuthnProto.getCacheConfig();
1✔
88
        cacheSize = cacheConfig.getCacheSize().getValue();
1✔
89
        if (cacheSize == 0) {
1✔
90
          return ConfigOrError.fromError(
1✔
91
              "cache_config.cache_size must be greater than zero");
92
        }
93
        // LruCache's size is an int and briefly exceeds its maximum size before evicting entries
94
        cacheSize = UnsignedLongs.min(cacheSize, Integer.MAX_VALUE - 1);
1✔
95
      }
96

97
      GcpAuthenticationConfig config = new GcpAuthenticationConfig((int) cacheSize);
1✔
98
      return ConfigOrError.fromConfig(config);
1✔
99
    }
100

101
    @Override
102
    public ConfigOrError<GcpAuthenticationConfig> parseFilterConfigOverride(
103
        Message rawProtoMessage) {
104
      return parseFilterConfig(rawProtoMessage);
×
105
    }
106
  }
107

108
  @Nullable
109
  @Override
110
  public ClientInterceptor buildClientInterceptor(FilterConfig config,
111
      @Nullable FilterConfig overrideConfig, ScheduledExecutorService scheduler) {
112

113
    ComputeEngineCredentials credentials = ComputeEngineCredentials.create();
1✔
114
    LruCache<String, CallCredentials> callCredentialsCache =
1✔
115
        new LruCache<>(((GcpAuthenticationConfig) config).getCacheSize());
1✔
116
    return new ClientInterceptor() {
1✔
117
      @Override
118
      public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
119
          MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
120

121
        /*String clusterName = callOptions.getOption(XdsAttributes.ATTR_CLUSTER_NAME);
122
        if (clusterName == null) {
123
          return next.newCall(method, callOptions);
124
        }*/
125

126
        // TODO: Fetch the CDS resource for the cluster.
127
        // If the CDS resource is not available, fail the RPC with Status.UNAVAILABLE.
128

129
        // TODO: Extract the audience from the CDS resource metadata.
130
        // If the audience is not found or is in the wrong format, fail the RPC.
131
        String audience = "TEST_AUDIENCE";
1✔
132

133
        try {
134
          CallCredentials existingCallCredentials = callOptions.getCredentials();
1✔
135
          CallCredentials newCallCredentials =
1✔
136
              getCallCredentials(callCredentialsCache, audience, credentials);
1✔
137
          if (existingCallCredentials != null) {
1✔
138
            callOptions = callOptions.withCallCredentials(
×
139
                new CompositeCallCredentials(existingCallCredentials, newCallCredentials));
140
          } else {
141
            callOptions = callOptions.withCallCredentials(newCallCredentials);
1✔
142
          }
143
        }
144
        catch (Exception e) {
×
145
          // If we fail to attach CallCredentials due to any reason, return a FailingClientCall
146
          return new FailingClientCall<>(Status.UNAUTHENTICATED
×
147
              .withDescription("Failed to attach CallCredentials.")
×
148
              .withCause(e));
×
149
        }
1✔
150
        return next.newCall(method, callOptions);
1✔
151
      }
152
    };
153
  }
154

155
  private CallCredentials getCallCredentials(LruCache<String, CallCredentials> cache,
156
      String audience, ComputeEngineCredentials credentials) {
157

158
    synchronized (cache) {
1✔
159
      return cache.getOrInsert(audience, key -> {
1✔
160
        IdTokenCredentials creds = IdTokenCredentials.newBuilder()
1✔
161
            .setIdTokenProvider(credentials)
1✔
162
            .setTargetAudience(audience)
1✔
163
            .build();
1✔
164
        return MoreCallCredentials.from(creds);
1✔
165
      });
166
    }
167
  }
168

169
  static final class GcpAuthenticationConfig implements FilterConfig {
170

171
    private final int cacheSize;
172

173
    public GcpAuthenticationConfig(int cacheSize) {
1✔
174
      this.cacheSize = cacheSize;
1✔
175
    }
1✔
176

177
    public int getCacheSize() {
178
      return cacheSize;
1✔
179
    }
180

181
    @Override
182
    public String typeUrl() {
183
      return GcpAuthenticationFilter.TYPE_URL;
×
184
    }
185
  }
186

187
  /** An implementation of {@link ClientCall} that fails when started. */
188
  private static final class FailingClientCall<ReqT, RespT> extends ClientCall<ReqT, RespT> {
189

190
    private final Status error;
191

192
    public FailingClientCall(Status error) {
×
193
      this.error = error;
×
194
    }
×
195

196
    @Override
197
    public void start(ClientCall.Listener<RespT> listener, Metadata headers) {
198
      listener.onClose(error, new Metadata());
×
199
    }
×
200

201
    @Override
202
    public void request(int numMessages) {}
×
203

204
    @Override
205
    public void cancel(String message, Throwable cause) {}
×
206

207
    @Override
208
    public void halfClose() {}
×
209

210
    @Override
211
    public void sendMessage(ReqT message) {}
×
212
  }
213

214
  private static final class LruCache<K, V> {
215

216
    private final Map<K, V> cache;
217

218
    LruCache(int maxSize) {
1✔
219
      this.cache = new LinkedHashMap<K, V>(
1✔
220
          maxSize,
221
          0.75f,
222
          true) {
1✔
223
        @Override
224
        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
225
          return size() > maxSize;
1✔
226
        }
227
      };
228
    }
1✔
229

230
    V getOrInsert(K key, Function<K, V> create) {
231
      return cache.computeIfAbsent(key, create);
1✔
232
    }
233
  }
234

235
  static class AudienceMetadataParser implements MetadataValueParser {
1✔
236

237
    @Override
238
    public String getTypeUrl() {
239
      return "type.googleapis.com/envoy.extensions.filters.http.gcp_authn.v3.Audience";
1✔
240
    }
241

242
    @Override
243
    public String parse(Any any) throws InvalidProtocolBufferException {
244
      Audience audience = any.unpack(Audience.class);
1✔
245
      String url = audience.getUrl();
1✔
246
      if (url.isEmpty()) {
1✔
247
        throw new InvalidProtocolBufferException(
×
248
            "Audience URL is empty. Metadata value must contain a valid URL.");
249
      }
250
      return url;
1✔
251
    }
252
  }
253
}
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