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

grpc / grpc-java / #20337

26 Jun 2026 11:37AM UTC coverage: 89.06% (+0.2%) from 88.876%
#20337

push

github

web-flow
Ext_proc filter and client interceptor (#12792)

Implements ext_proc filter from [grfc A93](https://github.com/grpc/proposal/pull/484)  (internal [design doc](http://go/ext-proc-design-java)).

37585 of 42202 relevant lines covered (89.06%)

0.89 hits per line

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

86.44
/../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 static com.google.common.base.Preconditions.checkNotNull;
20
import static io.grpc.xds.XdsNameResolver.CLUSTER_SELECTION_KEY;
21
import static io.grpc.xds.XdsNameResolver.XDS_CONFIG_CALL_OPTION_KEY;
22

23
import com.google.auth.oauth2.ComputeEngineCredentials;
24
import com.google.auth.oauth2.IdTokenCredentials;
25
import com.google.common.annotations.VisibleForTesting;
26
import com.google.common.primitives.UnsignedLongs;
27
import com.google.protobuf.Any;
28
import com.google.protobuf.InvalidProtocolBufferException;
29
import com.google.protobuf.Message;
30
import io.envoyproxy.envoy.extensions.filters.http.gcp_authn.v3.Audience;
31
import io.envoyproxy.envoy.extensions.filters.http.gcp_authn.v3.GcpAuthnFilterConfig;
32
import io.envoyproxy.envoy.extensions.filters.http.gcp_authn.v3.TokenCacheConfig;
33
import io.grpc.CallCredentials;
34
import io.grpc.CallOptions;
35
import io.grpc.Channel;
36
import io.grpc.ClientCall;
37
import io.grpc.ClientInterceptor;
38
import io.grpc.CompositeCallCredentials;
39
import io.grpc.Metadata;
40
import io.grpc.MethodDescriptor;
41
import io.grpc.Status;
42
import io.grpc.StatusOr;
43
import io.grpc.auth.MoreCallCredentials;
44
import io.grpc.xds.Filter.FilterConfigParseContext;
45
import io.grpc.xds.Filter.FilterContext;
46
import io.grpc.xds.GcpAuthenticationFilter.AudienceMetadataParser.AudienceWrapper;
47
import io.grpc.xds.MetadataRegistry.MetadataValueParser;
48
import io.grpc.xds.XdsConfig.XdsClusterConfig;
49
import io.grpc.xds.client.XdsResourceType.ResourceInvalidException;
50
import java.util.LinkedHashMap;
51
import java.util.Map;
52
import java.util.concurrent.ScheduledExecutorService;
53
import java.util.function.Function;
54
import javax.annotation.Nullable;
55

56
/**
57
 * A {@link Filter} that injects a {@link CallCredentials} to handle
58
 * authentication for xDS credentials.
59
 */
60
final class GcpAuthenticationFilter implements Filter {
61

62
  static final String TYPE_URL =
63
      "type.googleapis.com/envoy.extensions.filters.http.gcp_authn.v3.GcpAuthnFilterConfig";
64
  private final LruCache<String, CallCredentials> callCredentialsCache;
65
  final String filterInstanceName;
66

67
  GcpAuthenticationFilter(String name, int cacheSize) {
1✔
68
    filterInstanceName = checkNotNull(name, "name");
1✔
69
    this.callCredentialsCache = new LruCache<>(cacheSize);
1✔
70
  }
1✔
71

72
  static final class Provider implements Filter.Provider {
1✔
73
    private final int cacheSize = 10;
1✔
74

75
    @Override
76
    public String[] typeUrls() {
77
      return new String[]{TYPE_URL};
1✔
78
    }
79

80
    @Override
81
    public boolean isClientFilter() {
82
      return true;
1✔
83
    }
84

85
    @Override
86
    public GcpAuthenticationFilter newInstance(FilterContext context) {
87
      return new GcpAuthenticationFilter(context.filterName(), cacheSize);
×
88
    }
89

90
    @Override
91
    public ConfigOrError<GcpAuthenticationConfig> parseFilterConfig(
92
        Message rawProtoMessage, FilterConfigParseContext context) {
93
      GcpAuthnFilterConfig gcpAuthnProto;
94
      if (!(rawProtoMessage instanceof Any)) {
1✔
95
        return ConfigOrError.fromError("Invalid config type: " + rawProtoMessage.getClass());
1✔
96
      }
97
      Any anyMessage = (Any) rawProtoMessage;
1✔
98

99
      try {
100
        gcpAuthnProto = anyMessage.unpack(GcpAuthnFilterConfig.class);
1✔
101
      } catch (InvalidProtocolBufferException e) {
×
102
        return ConfigOrError.fromError("Invalid proto: " + e);
×
103
      }
1✔
104

105
      long cacheSize = 10;
1✔
106
      // Validate cache_config
107
      if (gcpAuthnProto.hasCacheConfig()) {
1✔
108
        TokenCacheConfig cacheConfig = gcpAuthnProto.getCacheConfig();
1✔
109
        if (cacheConfig.hasCacheSize()) {
1✔
110
          cacheSize = cacheConfig.getCacheSize().getValue();
1✔
111
          if (cacheSize == 0) {
1✔
112
            return ConfigOrError.fromError(
1✔
113
                "cache_config.cache_size must be greater than zero");
114
          }
115
        }
116

117
        // LruCache's size is an int and briefly exceeds its maximum size before evicting entries
118
        cacheSize = UnsignedLongs.min(cacheSize, Integer.MAX_VALUE - 1);
1✔
119
      }
120

121
      GcpAuthenticationConfig config = new GcpAuthenticationConfig((int) cacheSize);
1✔
122
      return ConfigOrError.fromConfig(config);
1✔
123
    }
124

125
    @Override
126
    public ConfigOrError<GcpAuthenticationConfig> parseFilterConfigOverride(
127
        Message rawProtoMessage, FilterConfigParseContext context) {
128
      return parseFilterConfig(rawProtoMessage, context);
×
129
    }
130
  }
131

132
  @Nullable
133
  @Override
134
  public ClientInterceptor buildClientInterceptor(FilterConfig config,
135
      @Nullable FilterConfig overrideConfig, ScheduledExecutorService scheduler) {
136

137
    ComputeEngineCredentials credentials = ComputeEngineCredentials.create();
1✔
138
    synchronized (callCredentialsCache) {
1✔
139
      callCredentialsCache.resizeCache(((GcpAuthenticationConfig) config).getCacheSize());
1✔
140
    }
1✔
141
    return new ClientInterceptor() {
1✔
142
      @Override
143
      public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
144
          MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
145

146
        String clusterName = callOptions.getOption(CLUSTER_SELECTION_KEY);
1✔
147
        if (clusterName == null) {
1✔
148
          return new FailingClientCall<>(
1✔
149
              Status.UNAVAILABLE.withDescription(
1✔
150
                  String.format(
1✔
151
                      "GCP Authn for %s does not contain cluster resource", filterInstanceName)));
152
        }
153

154
        if (!clusterName.startsWith("cluster:")) {
1✔
155
          return next.newCall(method, callOptions);
1✔
156
        }
157
        XdsConfig xdsConfig = callOptions.getOption(XDS_CONFIG_CALL_OPTION_KEY);
1✔
158
        if (xdsConfig == null) {
1✔
159
          return new FailingClientCall<>(
1✔
160
              Status.UNAVAILABLE.withDescription(
1✔
161
                  String.format(
1✔
162
                      "GCP Authn for %s with %s does not contain xds configuration",
163
                      filterInstanceName, clusterName)));
164
        }
165
        StatusOr<XdsClusterConfig> xdsCluster =
1✔
166
            xdsConfig.getClusters().get(clusterName.substring("cluster:".length()));
1✔
167
        if (xdsCluster == null) {
1✔
168
          return new FailingClientCall<>(
1✔
169
              Status.UNAVAILABLE.withDescription(
1✔
170
                  String.format(
1✔
171
                      "GCP Authn for %s with %s - xds cluster config does not contain xds cluster",
172
                      filterInstanceName, clusterName)));
173
        }
174
        if (!xdsCluster.hasValue()) {
1✔
175
          return new FailingClientCall<>(xdsCluster.getStatus());
1✔
176
        }
177
        Object audienceObj =
1✔
178
            xdsCluster.getValue().getClusterResource().parsedMetadata().get(filterInstanceName);
1✔
179
        if (audienceObj == null) {
1✔
180
          return next.newCall(method, callOptions);
×
181
        }
182
        if (!(audienceObj instanceof AudienceWrapper)) {
1✔
183
          return new FailingClientCall<>(
1✔
184
              Status.UNAVAILABLE.withDescription(
1✔
185
                  String.format("GCP Authn found wrong type in %s metadata: %s=%s",
1✔
186
                      clusterName, filterInstanceName, audienceObj.getClass())));
1✔
187
        }
188
        AudienceWrapper audience = (AudienceWrapper) audienceObj;
1✔
189
        CallCredentials existingCallCredentials = callOptions.getCredentials();
1✔
190
        CallCredentials newCallCredentials =
1✔
191
            getCallCredentials(callCredentialsCache, audience.audience, credentials);
1✔
192
        if (existingCallCredentials != null) {
1✔
193
          callOptions = callOptions.withCallCredentials(
×
194
              new CompositeCallCredentials(existingCallCredentials, newCallCredentials));
195
        } else {
196
          callOptions = callOptions.withCallCredentials(newCallCredentials);
1✔
197
        }
198
        return next.newCall(method, callOptions);
1✔
199
      }
200
    };
201
  }
202

203
  private CallCredentials getCallCredentials(LruCache<String, CallCredentials> cache,
204
      String audience, ComputeEngineCredentials credentials) {
205

206
    synchronized (cache) {
1✔
207
      return cache.getOrInsert(audience, key -> {
1✔
208
        IdTokenCredentials creds = IdTokenCredentials.newBuilder()
1✔
209
            .setIdTokenProvider(credentials)
1✔
210
            .setTargetAudience(audience)
1✔
211
            .build();
1✔
212
        return MoreCallCredentials.from(creds);
1✔
213
      });
214
    }
215
  }
216

217
  static final class GcpAuthenticationConfig implements FilterConfig {
218

219
    private final int cacheSize;
220

221
    public GcpAuthenticationConfig(int cacheSize) {
1✔
222
      this.cacheSize = cacheSize;
1✔
223
    }
1✔
224

225
    public int getCacheSize() {
226
      return cacheSize;
1✔
227
    }
228

229
    @Override
230
    public String typeUrl() {
231
      return GcpAuthenticationFilter.TYPE_URL;
×
232
    }
233
  }
234

235
  /** An implementation of {@link ClientCall} that fails when started. */
236
  @VisibleForTesting
237
  static final class FailingClientCall<ReqT, RespT> extends ClientCall<ReqT, RespT> {
238

239
    @VisibleForTesting
240
    final Status error;
241

242
    public FailingClientCall(Status error) {
1✔
243
      this.error = error;
1✔
244
    }
1✔
245

246
    @Override
247
    public void start(ClientCall.Listener<RespT> listener, Metadata headers) {
248
      listener.onClose(error, new Metadata());
×
249
    }
×
250

251
    @Override
252
    public void request(int numMessages) {}
×
253

254
    @Override
255
    public void cancel(String message, Throwable cause) {}
×
256

257
    @Override
258
    public void halfClose() {}
×
259

260
    @Override
261
    public void sendMessage(ReqT message) {}
×
262
  }
263

264
  private static final class LruCache<K, V> {
265

266
    private Map<K, V> cache;
267
    private int maxSize;
268

269
    LruCache(int maxSize) {
1✔
270
      this.maxSize = maxSize;
1✔
271
      this.cache = createEvictingMap(maxSize);
1✔
272
    }
1✔
273

274
    V getOrInsert(K key, Function<K, V> create) {
275
      return cache.computeIfAbsent(key, create);
1✔
276
    }
277

278
    private void resizeCache(int newSize) {
279
      if (newSize >= maxSize) {
1✔
280
        maxSize = newSize;
1✔
281
        return;
1✔
282
      }
283
      Map<K, V> newCache = createEvictingMap(newSize);
1✔
284
      maxSize = newSize;
1✔
285
      newCache.putAll(cache);
1✔
286
      cache = newCache;
1✔
287
    }
1✔
288

289
    private Map<K, V> createEvictingMap(int size) {
290
      return new LinkedHashMap<K, V>(size, 0.75f, true) {
1✔
291
        @Override
292
        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
293
          return size() > LruCache.this.maxSize;
1✔
294
        }
295
      };
296
    }
297
  }
298

299
  static class AudienceMetadataParser implements MetadataValueParser {
1✔
300

301
    static final class AudienceWrapper {
302
      final String audience;
303

304
      AudienceWrapper(String audience) {
1✔
305
        this.audience = checkNotNull(audience);
1✔
306
      }
1✔
307
    }
308

309
    @Override
310
    public String getTypeUrl() {
311
      return "type.googleapis.com/envoy.extensions.filters.http.gcp_authn.v3.Audience";
1✔
312
    }
313

314
    @Override
315
    public AudienceWrapper parse(Any any) throws ResourceInvalidException {
316
      Audience audience;
317
      try {
318
        audience = any.unpack(Audience.class);
1✔
319
      } catch (InvalidProtocolBufferException ex) {
×
320
        throw new ResourceInvalidException("Invalid Resource in address proto", ex);
×
321
      }
1✔
322
      String url = audience.getUrl();
1✔
323
      if (url.isEmpty()) {
1✔
324
        throw new ResourceInvalidException(
×
325
            "Audience URL is empty. Metadata value must contain a valid URL.");
326
      }
327
      return new AudienceWrapper(url);
1✔
328
    }
329
  }
330
}
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