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

grpc / grpc-java / #19761

02 Apr 2025 10:59AM UTC coverage: 88.603% (+0.04%) from 88.566%
#19761

push

github

web-flow
xds: propagate audience from cluster resource in gcp auth filter (#11972)

34712 of 39177 relevant lines covered (88.6%)

0.89 hits per line

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

83.81
/../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

63
  final String filterInstanceName;
64

65
  GcpAuthenticationFilter(String name) {
1✔
66
    filterInstanceName = checkNotNull(name, "name");
1✔
67
  }
1✔
68

69

70
  static final class Provider implements Filter.Provider {
1✔
71
    @Override
72
    public String[] typeUrls() {
73
      return new String[]{TYPE_URL};
×
74
    }
75

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

81
    @Override
82
    public GcpAuthenticationFilter newInstance(String name) {
83
      return new GcpAuthenticationFilter(name);
×
84
    }
85

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

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

100
      long cacheSize = 10;
1✔
101
      // Validate cache_config
102
      if (gcpAuthnProto.hasCacheConfig()) {
1✔
103
        TokenCacheConfig cacheConfig = gcpAuthnProto.getCacheConfig();
1✔
104
        cacheSize = cacheConfig.getCacheSize().getValue();
1✔
105
        if (cacheSize == 0) {
1✔
106
          return ConfigOrError.fromError(
1✔
107
              "cache_config.cache_size must be greater than zero");
108
        }
109
        // LruCache's size is an int and briefly exceeds its maximum size before evicting entries
110
        cacheSize = UnsignedLongs.min(cacheSize, Integer.MAX_VALUE - 1);
1✔
111
      }
112

113
      GcpAuthenticationConfig config = new GcpAuthenticationConfig((int) cacheSize);
1✔
114
      return ConfigOrError.fromConfig(config);
1✔
115
    }
116

117
    @Override
118
    public ConfigOrError<GcpAuthenticationConfig> parseFilterConfigOverride(
119
        Message rawProtoMessage) {
120
      return parseFilterConfig(rawProtoMessage);
×
121
    }
122
  }
123

124
  @Nullable
125
  @Override
126
  public ClientInterceptor buildClientInterceptor(FilterConfig config,
127
      @Nullable FilterConfig overrideConfig, ScheduledExecutorService scheduler) {
128

129
    ComputeEngineCredentials credentials = ComputeEngineCredentials.create();
1✔
130
    LruCache<String, CallCredentials> callCredentialsCache =
1✔
131
        new LruCache<>(((GcpAuthenticationConfig) config).getCacheSize());
1✔
132
    return new ClientInterceptor() {
1✔
133
      @Override
134
      public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
135
          MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
136

137
        String clusterName = callOptions.getOption(CLUSTER_SELECTION_KEY);
1✔
138
        if (clusterName == null) {
1✔
139
          return new FailingClientCall<>(
1✔
140
              Status.UNAVAILABLE.withDescription(
1✔
141
                  String.format(
1✔
142
                      "GCP Authn for %s does not contain cluster resource", filterInstanceName)));
143
        }
144

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

194
  private CallCredentials getCallCredentials(LruCache<String, CallCredentials> cache,
195
      String audience, ComputeEngineCredentials credentials) {
196

197
    synchronized (cache) {
1✔
198
      return cache.getOrInsert(audience, key -> {
1✔
199
        IdTokenCredentials creds = IdTokenCredentials.newBuilder()
1✔
200
            .setIdTokenProvider(credentials)
1✔
201
            .setTargetAudience(audience)
1✔
202
            .build();
1✔
203
        return MoreCallCredentials.from(creds);
1✔
204
      });
205
    }
206
  }
207

208
  static final class GcpAuthenticationConfig implements FilterConfig {
209

210
    private final int cacheSize;
211

212
    public GcpAuthenticationConfig(int cacheSize) {
1✔
213
      this.cacheSize = cacheSize;
1✔
214
    }
1✔
215

216
    public int getCacheSize() {
217
      return cacheSize;
1✔
218
    }
219

220
    @Override
221
    public String typeUrl() {
222
      return GcpAuthenticationFilter.TYPE_URL;
×
223
    }
224
  }
225

226
  /** An implementation of {@link ClientCall} that fails when started. */
227
  @VisibleForTesting
228
  static final class FailingClientCall<ReqT, RespT> extends ClientCall<ReqT, RespT> {
229

230
    @VisibleForTesting
231
    final Status error;
232

233
    public FailingClientCall(Status error) {
1✔
234
      this.error = error;
1✔
235
    }
1✔
236

237
    @Override
238
    public void start(ClientCall.Listener<RespT> listener, Metadata headers) {
239
      listener.onClose(error, new Metadata());
×
240
    }
×
241

242
    @Override
243
    public void request(int numMessages) {}
×
244

245
    @Override
246
    public void cancel(String message, Throwable cause) {}
×
247

248
    @Override
249
    public void halfClose() {}
×
250

251
    @Override
252
    public void sendMessage(ReqT message) {}
×
253
  }
254

255
  private static final class LruCache<K, V> {
256

257
    private final Map<K, V> cache;
258

259
    LruCache(int maxSize) {
1✔
260
      this.cache = new LinkedHashMap<K, V>(
1✔
261
          maxSize,
262
          0.75f,
263
          true) {
1✔
264
        @Override
265
        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
266
          return size() > maxSize;
1✔
267
        }
268
      };
269
    }
1✔
270

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

276
  static class AudienceMetadataParser implements MetadataValueParser {
1✔
277

278
    static final class AudienceWrapper {
279
      final String audience;
280

281
      AudienceWrapper(String audience) {
1✔
282
        this.audience = checkNotNull(audience);
1✔
283
      }
1✔
284
    }
285

286
    @Override
287
    public String getTypeUrl() {
288
      return "type.googleapis.com/envoy.extensions.filters.http.gcp_authn.v3.Audience";
1✔
289
    }
290

291
    @Override
292
    public AudienceWrapper parse(Any any) throws ResourceInvalidException {
293
      Audience audience;
294
      try {
295
        audience = any.unpack(Audience.class);
1✔
296
      } catch (InvalidProtocolBufferException ex) {
×
297
        throw new ResourceInvalidException("Invalid Resource in address proto", ex);
×
298
      }
1✔
299
      String url = audience.getUrl();
1✔
300
      if (url.isEmpty()) {
1✔
301
        throw new ResourceInvalidException(
×
302
            "Audience URL is empty. Metadata value must contain a valid URL.");
303
      }
304
      return new AudienceWrapper(url);
1✔
305
    }
306
  }
307
}
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