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

grpc / grpc-java / #19826

23 May 2025 10:25AM UTC coverage: 88.641% (+0.005%) from 88.636%
#19826

push

github

web-flow
xds: Add GcpAuthenticationFilter to FilterRegistry (#12075) (#12097)

34686 of 39131 relevant lines covered (88.64%)

0.89 hits per line

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

85.0
/../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.FilterRegistry.isEnabledGcpAuthnFilter;
21
import static io.grpc.xds.XdsNameResolver.CLUSTER_SELECTION_KEY;
22
import static io.grpc.xds.XdsNameResolver.XDS_CONFIG_CALL_OPTION_KEY;
23

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

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

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

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

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

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

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

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

89
    @Override
90
    public ConfigOrError<GcpAuthenticationConfig> parseFilterConfig(Message rawProtoMessage) {
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) {
126
      return parseFilterConfig(rawProtoMessage);
×
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
        if (!isEnabledGcpAuthnFilter) {
1✔
317
          throw new InvalidProtocolBufferException("Environment variable for GCP Authentication "
×
318
              + "Filter is Not Set");
319
        }
320
        audience = any.unpack(Audience.class);
1✔
321
      } catch (InvalidProtocolBufferException ex) {
×
322
        throw new ResourceInvalidException("Invalid Resource in address proto", ex);
×
323
      }
1✔
324
      String url = audience.getUrl();
1✔
325
      if (url.isEmpty()) {
1✔
326
        throw new ResourceInvalidException(
×
327
            "Audience URL is empty. Metadata value must contain a valid URL.");
328
      }
329
      return new AudienceWrapper(url);
1✔
330
    }
331
  }
332
}
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