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

grpc / grpc-java / #19624

07 Jan 2025 04:33AM CUT coverage: 88.565% (-0.003%) from 88.568%
#19624

push

github

web-flow
xds: Parsing xDS Cluster Metadata (#11741)

33683 of 38032 relevant lines covered (88.56%)

0.89 hits per line

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

71.43
/../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 com.google.auth.oauth2.ComputeEngineCredentials;
20
import com.google.auth.oauth2.IdTokenCredentials;
21
import com.google.common.primitives.UnsignedLongs;
22
import com.google.protobuf.Any;
23
import com.google.protobuf.InvalidProtocolBufferException;
24
import com.google.protobuf.Message;
25
import io.envoyproxy.envoy.extensions.filters.http.gcp_authn.v3.Audience;
26
import io.envoyproxy.envoy.extensions.filters.http.gcp_authn.v3.GcpAuthnFilterConfig;
27
import io.envoyproxy.envoy.extensions.filters.http.gcp_authn.v3.TokenCacheConfig;
28
import io.grpc.CallCredentials;
29
import io.grpc.CallOptions;
30
import io.grpc.Channel;
31
import io.grpc.ClientCall;
32
import io.grpc.ClientInterceptor;
33
import io.grpc.CompositeCallCredentials;
34
import io.grpc.LoadBalancer.PickSubchannelArgs;
35
import io.grpc.Metadata;
36
import io.grpc.MethodDescriptor;
37
import io.grpc.Status;
38
import io.grpc.auth.MoreCallCredentials;
39
import io.grpc.xds.Filter.ClientInterceptorBuilder;
40
import io.grpc.xds.MetadataRegistry.MetadataValueParser;
41
import java.util.LinkedHashMap;
42
import java.util.Map;
43
import java.util.concurrent.ScheduledExecutorService;
44
import java.util.function.Function;
45
import javax.annotation.Nullable;
46

47
/**
48
 * A {@link Filter} that injects a {@link CallCredentials} to handle
49
 * authentication for xDS credentials.
50
 */
51
final class GcpAuthenticationFilter implements Filter, ClientInterceptorBuilder {
1✔
52

53
  static final String TYPE_URL =
54
      "type.googleapis.com/envoy.extensions.filters.http.gcp_authn.v3.GcpAuthnFilterConfig";
55

56
  @Override
57
  public String[] typeUrls() {
58
    return new String[] { TYPE_URL };
×
59
  }
60

61
  @Override
62
  public ConfigOrError<? extends FilterConfig> parseFilterConfig(Message rawProtoMessage) {
63
    GcpAuthnFilterConfig gcpAuthnProto;
64
    if (!(rawProtoMessage instanceof Any)) {
1✔
65
      return ConfigOrError.fromError("Invalid config type: " + rawProtoMessage.getClass());
1✔
66
    }
67
    Any anyMessage = (Any) rawProtoMessage;
1✔
68

69
    try {
70
      gcpAuthnProto = anyMessage.unpack(GcpAuthnFilterConfig.class);
1✔
71
    } catch (InvalidProtocolBufferException e) {
×
72
      return ConfigOrError.fromError("Invalid proto: " + e);
×
73
    }
1✔
74

75
    long cacheSize = 10;
1✔
76
    // Validate cache_config
77
    if (gcpAuthnProto.hasCacheConfig()) {
1✔
78
      TokenCacheConfig cacheConfig = gcpAuthnProto.getCacheConfig();
1✔
79
      cacheSize = cacheConfig.getCacheSize().getValue();
1✔
80
      if (cacheSize == 0) {
1✔
81
        return ConfigOrError.fromError(
1✔
82
            "cache_config.cache_size must be greater than zero");
83
      }
84
      // LruCache's size is an int and briefly exceeds its maximum size before evicting entries
85
      cacheSize = UnsignedLongs.min(cacheSize, Integer.MAX_VALUE - 1);
1✔
86
    }
87

88
    GcpAuthenticationConfig config = new GcpAuthenticationConfig((int) cacheSize);
1✔
89
    return ConfigOrError.fromConfig(config);
1✔
90
  }
91

92
  @Override
93
  public ConfigOrError<? extends FilterConfig> parseFilterConfigOverride(Message rawProtoMessage) {
94
    return parseFilterConfig(rawProtoMessage);
×
95
  }
96

97
  @Nullable
98
  @Override
99
  public ClientInterceptor buildClientInterceptor(FilterConfig config,
100
      @Nullable FilterConfig overrideConfig, PickSubchannelArgs args,
101
      ScheduledExecutorService scheduler) {
102

103
    ComputeEngineCredentials credentials = ComputeEngineCredentials.create();
1✔
104
    LruCache<String, CallCredentials> callCredentialsCache =
1✔
105
        new LruCache<>(((GcpAuthenticationConfig) config).getCacheSize());
1✔
106
    return new ClientInterceptor() {
1✔
107
      @Override
108
      public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
109
          MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
110

111
        /*String clusterName = callOptions.getOption(XdsAttributes.ATTR_CLUSTER_NAME);
112
        if (clusterName == null) {
113
          return next.newCall(method, callOptions);
114
        }*/
115

116
        // TODO: Fetch the CDS resource for the cluster.
117
        // If the CDS resource is not available, fail the RPC with Status.UNAVAILABLE.
118

119
        // TODO: Extract the audience from the CDS resource metadata.
120
        // If the audience is not found or is in the wrong format, fail the RPC.
121
        String audience = "TEST_AUDIENCE";
1✔
122

123
        try {
124
          CallCredentials existingCallCredentials = callOptions.getCredentials();
1✔
125
          CallCredentials newCallCredentials =
1✔
126
              getCallCredentials(callCredentialsCache, audience, credentials);
1✔
127
          if (existingCallCredentials != null) {
1✔
128
            callOptions = callOptions.withCallCredentials(
×
129
                new CompositeCallCredentials(existingCallCredentials, newCallCredentials));
130
          } else {
131
            callOptions = callOptions.withCallCredentials(newCallCredentials);
1✔
132
          }
133
        }
134
        catch (Exception e) {
×
135
          // If we fail to attach CallCredentials due to any reason, return a FailingClientCall
136
          return new FailingClientCall<>(Status.UNAUTHENTICATED
×
137
              .withDescription("Failed to attach CallCredentials.")
×
138
              .withCause(e));
×
139
        }
1✔
140
        return next.newCall(method, callOptions);
1✔
141
      }
142
    };
143
  }
144

145
  private CallCredentials getCallCredentials(LruCache<String, CallCredentials> cache,
146
      String audience, ComputeEngineCredentials credentials) {
147

148
    synchronized (cache) {
1✔
149
      return cache.getOrInsert(audience, key -> {
1✔
150
        IdTokenCredentials creds = IdTokenCredentials.newBuilder()
1✔
151
            .setIdTokenProvider(credentials)
1✔
152
            .setTargetAudience(audience)
1✔
153
            .build();
1✔
154
        return MoreCallCredentials.from(creds);
1✔
155
      });
156
    }
157
  }
158

159
  static final class GcpAuthenticationConfig implements FilterConfig {
160

161
    private final int cacheSize;
162

163
    public GcpAuthenticationConfig(int cacheSize) {
1✔
164
      this.cacheSize = cacheSize;
1✔
165
    }
1✔
166

167
    public int getCacheSize() {
168
      return cacheSize;
1✔
169
    }
170

171
    @Override
172
    public String typeUrl() {
173
      return GcpAuthenticationFilter.TYPE_URL;
×
174
    }
175
  }
176

177
  /** An implementation of {@link ClientCall} that fails when started. */
178
  private static final class FailingClientCall<ReqT, RespT> extends ClientCall<ReqT, RespT> {
179

180
    private final Status error;
181

182
    public FailingClientCall(Status error) {
×
183
      this.error = error;
×
184
    }
×
185

186
    @Override
187
    public void start(ClientCall.Listener<RespT> listener, Metadata headers) {
188
      listener.onClose(error, new Metadata());
×
189
    }
×
190

191
    @Override
192
    public void request(int numMessages) {}
×
193

194
    @Override
195
    public void cancel(String message, Throwable cause) {}
×
196

197
    @Override
198
    public void halfClose() {}
×
199

200
    @Override
201
    public void sendMessage(ReqT message) {}
×
202
  }
203

204
  private static final class LruCache<K, V> {
205

206
    private final Map<K, V> cache;
207

208
    LruCache(int maxSize) {
1✔
209
      this.cache = new LinkedHashMap<K, V>(
1✔
210
          maxSize,
211
          0.75f,
212
          true) {
1✔
213
        @Override
214
        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
215
          return size() > maxSize;
1✔
216
        }
217
      };
218
    }
1✔
219

220
    V getOrInsert(K key, Function<K, V> create) {
221
      return cache.computeIfAbsent(key, create);
1✔
222
    }
223
  }
224

225
  static class AudienceMetadataParser implements MetadataValueParser {
1✔
226

227
    @Override
228
    public String getTypeUrl() {
229
      return "type.googleapis.com/envoy.extensions.filters.http.gcp_authn.v3.Audience";
1✔
230
    }
231

232
    @Override
233
    public String parse(Any any) throws InvalidProtocolBufferException {
234
      Audience audience = any.unpack(Audience.class);
1✔
235
      String url = audience.getUrl();
1✔
236
      if (url.isEmpty()) {
1✔
237
        throw new InvalidProtocolBufferException(
×
238
            "Audience URL is empty. Metadata value must contain a valid URL.");
239
      }
240
      return url;
1✔
241
    }
242
  }
243
}
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