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

grpc / grpc-java / #19824

23 May 2025 09:18AM UTC coverage: 88.636% (-0.007%) from 88.643%
#19824

push

github

web-flow
xds: float LRU cache across interceptors (#11992) (#12096)

34679 of 39125 relevant lines covered (88.64%)

0.89 hits per line

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

85.59
/../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.GcpAuthenticationFilter.AudienceMetadataParser.AudienceWrapper;
45
import io.grpc.xds.MetadataRegistry.MetadataValueParser;
46
import io.grpc.xds.XdsConfig.XdsClusterConfig;
47
import io.grpc.xds.client.XdsResourceType.ResourceInvalidException;
48
import java.util.LinkedHashMap;
49
import java.util.Map;
50
import java.util.concurrent.ScheduledExecutorService;
51
import java.util.function.Function;
52
import javax.annotation.Nullable;
53

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

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

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

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

73
    @Override
74
    public String[] typeUrls() {
75
      return new String[]{TYPE_URL};
×
76
    }
77

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

83
    @Override
84
    public GcpAuthenticationFilter newInstance(String name) {
85
      return new GcpAuthenticationFilter(name, cacheSize);
×
86
    }
87

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

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

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

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

118
      GcpAuthenticationConfig config = new GcpAuthenticationConfig((int) cacheSize);
1✔
119
      return ConfigOrError.fromConfig(config);
1✔
120
    }
121

122
    @Override
123
    public ConfigOrError<GcpAuthenticationConfig> parseFilterConfigOverride(
124
        Message rawProtoMessage) {
125
      return parseFilterConfig(rawProtoMessage);
×
126
    }
127
  }
128

129
  @Nullable
130
  @Override
131
  public ClientInterceptor buildClientInterceptor(FilterConfig config,
132
      @Nullable FilterConfig overrideConfig, ScheduledExecutorService scheduler) {
133

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

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

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

200
  private CallCredentials getCallCredentials(LruCache<String, CallCredentials> cache,
201
      String audience, ComputeEngineCredentials credentials) {
202

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

214
  static final class GcpAuthenticationConfig implements FilterConfig {
215

216
    private final int cacheSize;
217

218
    public GcpAuthenticationConfig(int cacheSize) {
1✔
219
      this.cacheSize = cacheSize;
1✔
220
    }
1✔
221

222
    public int getCacheSize() {
223
      return cacheSize;
1✔
224
    }
225

226
    @Override
227
    public String typeUrl() {
228
      return GcpAuthenticationFilter.TYPE_URL;
×
229
    }
230
  }
231

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

236
    @VisibleForTesting
237
    final Status error;
238

239
    public FailingClientCall(Status error) {
1✔
240
      this.error = error;
1✔
241
    }
1✔
242

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

248
    @Override
249
    public void request(int numMessages) {}
×
250

251
    @Override
252
    public void cancel(String message, Throwable cause) {}
×
253

254
    @Override
255
    public void halfClose() {}
×
256

257
    @Override
258
    public void sendMessage(ReqT message) {}
×
259
  }
260

261
  private static final class LruCache<K, V> {
262

263
    private Map<K, V> cache;
264
    private int maxSize;
265

266
    LruCache(int maxSize) {
1✔
267
      this.maxSize = maxSize;
1✔
268
      this.cache = createEvictingMap(maxSize);
1✔
269
    }
1✔
270

271
    V getOrInsert(K key, Function<K, V> create) {
272
      return cache.computeIfAbsent(key, create);
1✔
273
    }
274

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

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

296
  static class AudienceMetadataParser implements MetadataValueParser {
1✔
297

298
    static final class AudienceWrapper {
299
      final String audience;
300

301
      AudienceWrapper(String audience) {
1✔
302
        this.audience = checkNotNull(audience);
1✔
303
      }
1✔
304
    }
305

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

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

© 2025 Coveralls, Inc