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

grpc / grpc-java / #20276

11 May 2026 08:24AM UTC coverage: 88.821% (-0.02%) from 88.838%
#20276

push

github

web-flow
Allow injecting bootstrap info into xDS Filter API for config parsing (#12724)

Extend the xDS Filter API to support injecting bootstrap information
into filters during configuration parsing. This allows filters to access
context information (e.g., allowed gRPC services) from the resource loading
layer during configuration validation and parsing.

- Update `Filter.Provider.parseFilterConfig` and
`parseFilterConfigOverride`
  to accept a `FilterContext` parameter.
- Introduce `BootstrapInfoGrpcServiceContextProvider` to encapsulate
  bootstrap info for context resolution.
- Update `XdsListenerResource` and `XdsRouteConfigureResource` to
  construct and pass `FilterContext` during configuration parsing.
- Update sub-filters (`FaultFilter`, `RbacFilter`,
`GcpAuthenticationFilter`,
  `RouterFilter`) to match the updated `FilterContext` signature.

Known Gaps & Limitations:
1. **MetricHolder**: Propagation of `MetricHolder` is not supported with
   this approach currently and is planned for support in a later phase.
2. **NameResolverRegistry**: Propagation is deferred for consistency.
While it could be passed from `XdsNameResolver` on the client side, there is
no equivalent mechanism on the server side. To ensure consistent
behavior, `DefaultRegistry` is used when validating schemes and creating channels.

36254 of 40817 relevant lines covered (88.82%)

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.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};
1✔
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(
90
        Message rawProtoMessage, FilterConfigParseContext context) {
91
      GcpAuthnFilterConfig gcpAuthnProto;
92
      if (!(rawProtoMessage instanceof Any)) {
1✔
93
        return ConfigOrError.fromError("Invalid config type: " + rawProtoMessage.getClass());
1✔
94
      }
95
      Any anyMessage = (Any) rawProtoMessage;
1✔
96

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

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

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

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

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

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

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

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

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

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

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

215
  static final class GcpAuthenticationConfig implements FilterConfig {
216

217
    private final int cacheSize;
218

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

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

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

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

237
    @VisibleForTesting
238
    final Status error;
239

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

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

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

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

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

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

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

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

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

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

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

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

297
  static class AudienceMetadataParser implements MetadataValueParser {
1✔
298

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

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

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

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