1 //
2 // Copyright 2024 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 #include <grpc/support/string_util.h>
18
19 #include <string>
20 #include <vector>
21
22 #include "envoy/config/cluster/v3/cluster.pb.h"
23 #include "envoy/extensions/filters/http/gcp_authn/v3/gcp_authn.pb.h"
24 #include "envoy/extensions/filters/http/router/v3/router.pb.h"
25 #include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h"
26 #include "gmock/gmock.h"
27 #include "gtest/gtest.h"
28 #include "src/core/client_channel/backup_poller.h"
29 #include "src/core/config/config_vars.h"
30 #include "src/core/util/http_client/httpcli.h"
31 #include "test/core/test_util/scoped_env_var.h"
32 #include "test/core/test_util/test_config.h"
33 #include "test/cpp/end2end/xds/xds_end2end_test_lib.h"
34
35 namespace grpc {
36 namespace testing {
37 namespace {
38
39 using ::envoy::extensions::filters::http::gcp_authn::v3::Audience;
40 using ::envoy::extensions::filters::http::gcp_authn::v3::GcpAuthnFilterConfig;
41 using ::envoy::extensions::filters::network::http_connection_manager::v3::
42 HttpFilter;
43
44 constexpr absl::string_view kFilterInstanceName = "gcp_authn_instance";
45 constexpr absl::string_view kAudience = "audience";
46
47 class XdsGcpAuthnEnd2endTest : public XdsEnd2endTest {
48 public:
SetUp()49 void SetUp() override {
50 g_audience = "";
51 g_token = nullptr;
52 g_num_token_fetches = 0;
53 grpc_core::HttpRequest::SetOverride(HttpGetOverride, nullptr, nullptr);
54 InitClient(MakeBootstrapBuilder(), /*lb_expected_authority=*/"",
55 /*xds_resource_does_not_exist_timeout_ms=*/0,
56 /*balancer_authority_override=*/"", /*args=*/nullptr,
57 CreateTlsChannelCredentials());
58 }
59
TearDown()60 void TearDown() override {
61 XdsEnd2endTest::TearDown();
62 grpc_core::HttpRequest::SetOverride(nullptr, nullptr, nullptr);
63 }
64
ValidateHttpRequest(const grpc_http_request * request,const grpc_core::URI & uri)65 static void ValidateHttpRequest(const grpc_http_request* request,
66 const grpc_core::URI& uri) {
67 EXPECT_THAT(
68 uri.query_parameter_map(),
69 ::testing::ElementsAre(::testing::Pair("audience", g_audience)));
70 ASSERT_EQ(request->hdr_count, 1);
71 EXPECT_EQ(absl::string_view(request->hdrs[0].key), "Metadata-Flavor");
72 EXPECT_EQ(absl::string_view(request->hdrs[0].value), "Google");
73 }
74
HttpGetOverride(const grpc_http_request * request,const grpc_core::URI & uri,grpc_core::Timestamp,grpc_closure * on_done,grpc_http_response * response)75 static int HttpGetOverride(const grpc_http_request* request,
76 const grpc_core::URI& uri,
77 grpc_core::Timestamp /*deadline*/,
78 grpc_closure* on_done,
79 grpc_http_response* response) {
80 // Intercept only requests for GCP service account identity tokens.
81 if (uri.authority() != "metadata.google.internal." ||
82 uri.path() !=
83 "/computeMetadata/v1/instance/service-accounts/default/identity") {
84 return 0;
85 }
86 g_num_token_fetches.fetch_add(1);
87 // Validate request.
88 ValidateHttpRequest(request, uri);
89 // Generate response.
90 response->status = 200;
91 response->body = gpr_strdup(const_cast<char*>(g_token));
92 response->body_length = strlen(g_token);
93 grpc_core::ExecCtx::Run(DEBUG_LOCATION, on_done, absl::OkStatus());
94 return 1;
95 }
96
97 // Constructs a synthetic JWT token that's just valid enough for the
98 // call creds to extract the expiration date.
MakeToken(grpc_core::Timestamp expiration)99 static std::string MakeToken(grpc_core::Timestamp expiration) {
100 gpr_timespec ts = expiration.as_timespec(GPR_CLOCK_REALTIME);
101 std::string json = absl::StrCat("{\"exp\":", ts.tv_sec, "}");
102 return absl::StrCat("foo.", absl::WebSafeBase64Escape(json), ".bar");
103 }
104
BuildListenerWithGcpAuthnFilter(bool optional=false)105 Listener BuildListenerWithGcpAuthnFilter(bool optional = false) {
106 Listener listener = default_listener_;
107 HttpConnectionManager hcm = ClientHcmAccessor().Unpack(listener);
108 HttpFilter* filter0 = hcm.mutable_http_filters(0);
109 *hcm.add_http_filters() = *filter0;
110 filter0->set_name(kFilterInstanceName);
111 if (optional) filter0->set_is_optional(true);
112 filter0->mutable_typed_config()->PackFrom(GcpAuthnFilterConfig());
113 ClientHcmAccessor().Pack(hcm, &listener);
114 return listener;
115 }
116
BuildClusterWithAudience(absl::string_view audience)117 Cluster BuildClusterWithAudience(absl::string_view audience) {
118 Audience audience_proto;
119 audience_proto.set_url(audience);
120 Cluster cluster = default_cluster_;
121 auto& filter_map =
122 *cluster.mutable_metadata()->mutable_typed_filter_metadata();
123 auto& entry = filter_map[kFilterInstanceName];
124 entry.PackFrom(audience_proto);
125 return cluster;
126 }
127
128 static absl::string_view g_audience;
129 static const char* g_token;
130 static std::atomic<size_t> g_num_token_fetches;
131 };
132
133 absl::string_view XdsGcpAuthnEnd2endTest::g_audience;
134 const char* XdsGcpAuthnEnd2endTest::g_token;
135 std::atomic<size_t> XdsGcpAuthnEnd2endTest::g_num_token_fetches;
136
137 INSTANTIATE_TEST_SUITE_P(XdsTest, XdsGcpAuthnEnd2endTest,
138 ::testing::Values(XdsTestType()), &XdsTestType::Name);
139
TEST_P(XdsGcpAuthnEnd2endTest,Basic)140 TEST_P(XdsGcpAuthnEnd2endTest, Basic) {
141 grpc_core::testing::ScopedExperimentalEnvVar env(
142 "GRPC_EXPERIMENTAL_XDS_GCP_AUTHENTICATION_FILTER");
143 // Construct auth token.
144 g_audience = kAudience;
145 std::string token = MakeToken(grpc_core::Timestamp::InfFuture());
146 g_token = token.c_str();
147 // Set xDS resources.
148 CreateAndStartBackends(1, /*xds_enabled=*/false,
149 CreateTlsServerCredentials());
150 SetListenerAndRouteConfiguration(balancer_.get(),
151 BuildListenerWithGcpAuthnFilter(),
152 default_route_config_);
153 balancer_->ads_service()->SetCdsResource(BuildClusterWithAudience(kAudience));
154 EdsResourceArgs args({{"locality0", CreateEndpointsForBackends()}});
155 balancer_->ads_service()->SetEdsResource(BuildEdsResource(args));
156 // Send an RPC and check that it arrives with the right auth token.
157 std::multimap<std::string, std::string> server_initial_metadata;
158 Status status = SendRpc(RpcOptions().set_echo_metadata_initially(true),
159 /*response=*/nullptr, &server_initial_metadata);
160 EXPECT_TRUE(status.ok()) << "code=" << status.error_code()
161 << " message=" << status.error_message();
162 EXPECT_THAT(server_initial_metadata,
163 ::testing::Contains(::testing::Pair(
164 "authorization", absl::StrCat("Bearer ", g_token))));
165 EXPECT_EQ(g_num_token_fetches.load(), 1);
166 }
167
TEST_P(XdsGcpAuthnEnd2endTest,NoOpWhenClusterHasNoAudience)168 TEST_P(XdsGcpAuthnEnd2endTest, NoOpWhenClusterHasNoAudience) {
169 grpc_core::testing::ScopedExperimentalEnvVar env(
170 "GRPC_EXPERIMENTAL_XDS_GCP_AUTHENTICATION_FILTER");
171 // Set xDS resources.
172 CreateAndStartBackends(1, /*xds_enabled=*/false,
173 CreateTlsServerCredentials());
174 SetListenerAndRouteConfiguration(balancer_.get(),
175 BuildListenerWithGcpAuthnFilter(),
176 default_route_config_);
177 EdsResourceArgs args({{"locality0", CreateEndpointsForBackends()}});
178 balancer_->ads_service()->SetEdsResource(BuildEdsResource(args));
179 // Send an RPC and check that it does not have an auth token.
180 std::multimap<std::string, std::string> server_initial_metadata;
181 Status status = SendRpc(RpcOptions().set_echo_metadata_initially(true),
182 /*response=*/nullptr, &server_initial_metadata);
183 EXPECT_TRUE(status.ok()) << "code=" << status.error_code()
184 << " message=" << status.error_message();
185 EXPECT_THAT(
186 server_initial_metadata,
187 ::testing::Not(::testing::Contains(::testing::Key("authorization"))));
188 }
189
TEST_P(XdsGcpAuthnEnd2endTest,CacheRetainedAcrossXdsUpdates)190 TEST_P(XdsGcpAuthnEnd2endTest, CacheRetainedAcrossXdsUpdates) {
191 grpc_core::testing::ScopedExperimentalEnvVar env(
192 "GRPC_EXPERIMENTAL_XDS_GCP_AUTHENTICATION_FILTER");
193 // Construct auth token.
194 g_audience = kAudience;
195 std::string token = MakeToken(grpc_core::Timestamp::InfFuture());
196 g_token = token.c_str();
197 // Set xDS resources.
198 CreateAndStartBackends(1, /*xds_enabled=*/false,
199 CreateTlsServerCredentials());
200 SetListenerAndRouteConfiguration(balancer_.get(),
201 BuildListenerWithGcpAuthnFilter(),
202 default_route_config_);
203 balancer_->ads_service()->SetCdsResource(BuildClusterWithAudience(kAudience));
204 EdsResourceArgs args({{"locality0", {CreateEndpoint(0)}}});
205 balancer_->ads_service()->SetEdsResource(BuildEdsResource(args));
206 // Send an RPC and check that it arrives with the right auth token.
207 std::multimap<std::string, std::string> server_initial_metadata;
208 Status status = SendRpc(RpcOptions().set_echo_metadata_initially(true),
209 /*response=*/nullptr, &server_initial_metadata);
210 EXPECT_TRUE(status.ok()) << "code=" << status.error_code()
211 << " message=" << status.error_message();
212 EXPECT_THAT(server_initial_metadata,
213 ::testing::Contains(::testing::Pair(
214 "authorization", absl::StrCat("Bearer ", g_token))));
215 EXPECT_EQ(g_num_token_fetches.load(), 1);
216 // Trigger update that changes the route config, thus causing the
217 // dynamic filters to be recreated.
218 // We insert a route that matches requests with the header "foo" and
219 // has a non-forwarding action, which will cause the client to fail RPCs
220 // that hit this route.
221 RouteConfiguration route_config = default_route_config_;
222 *route_config.mutable_virtual_hosts(0)->add_routes() =
223 route_config.virtual_hosts(0).routes(0);
224 auto* header_matcher = route_config.mutable_virtual_hosts(0)
225 ->mutable_routes(0)
226 ->mutable_match()
227 ->add_headers();
228 header_matcher->set_name("foo");
229 header_matcher->set_present_match(true);
230 route_config.mutable_virtual_hosts(0)
231 ->mutable_routes(0)
232 ->mutable_non_forwarding_action();
233 SetListenerAndRouteConfiguration(
234 balancer_.get(), BuildListenerWithGcpAuthnFilter(), route_config);
235 // Send RPCs with the header "foo" and wait for them to start failing.
236 // When they do, we know that the client has seen the update.
237 SendRpcsUntilFailure(DEBUG_LOCATION, StatusCode::UNAVAILABLE,
238 "Matching route has inappropriate action",
239 /*timeout_ms=*/15000,
240 RpcOptions().set_metadata({{"foo", "bar"}}));
241 // Now send an RPC without the header, which will go through the new
242 // instance of the GCP auth filter.
243 CheckRpcSendOk(DEBUG_LOCATION);
244 // Make sure we didn't re-fetch the token.
245 EXPECT_EQ(g_num_token_fetches.load(), 1);
246 }
247
TEST_P(XdsGcpAuthnEnd2endTest,FilterIgnoredWhenEnvVarNotSet)248 TEST_P(XdsGcpAuthnEnd2endTest, FilterIgnoredWhenEnvVarNotSet) {
249 // Construct auth token.
250 g_audience = kAudience;
251 std::string token = MakeToken(grpc_core::Timestamp::InfFuture());
252 g_token = token.c_str();
253 // Set xDS resources.
254 CreateAndStartBackends(1, /*xds_enabled=*/false,
255 CreateTlsServerCredentials());
256 SetListenerAndRouteConfiguration(
257 balancer_.get(), BuildListenerWithGcpAuthnFilter(/*optional=*/true),
258 default_route_config_);
259 balancer_->ads_service()->SetCdsResource(BuildClusterWithAudience(kAudience));
260 EdsResourceArgs args({{"locality0", CreateEndpointsForBackends()}});
261 balancer_->ads_service()->SetEdsResource(BuildEdsResource(args));
262 // Send an RPC and check that it does not have an auth token.
263 std::multimap<std::string, std::string> server_initial_metadata;
264 Status status = SendRpc(RpcOptions().set_echo_metadata_initially(true),
265 /*response=*/nullptr, &server_initial_metadata);
266 EXPECT_TRUE(status.ok()) << "code=" << status.error_code()
267 << " message=" << status.error_message();
268 EXPECT_THAT(
269 server_initial_metadata,
270 ::testing::Not(::testing::Contains(::testing::Key("authorization"))));
271 }
272
273 } // namespace
274 } // namespace testing
275 } // namespace grpc
276
main(int argc,char ** argv)277 int main(int argc, char** argv) {
278 grpc::testing::TestEnvironment env(&argc, argv);
279 ::testing::InitGoogleTest(&argc, argv);
280 // Make the backup poller poll very frequently in order to pick up
281 // updates from all the subchannels's FDs.
282 grpc_core::ConfigVars::Overrides overrides;
283 overrides.client_channel_backup_poll_interval_ms = 1;
284 grpc_core::ConfigVars::SetOverrides(overrides);
285 #if TARGET_OS_IPHONE
286 // Workaround Apple CFStream bug
287 grpc_core::SetEnv("grpc_cfstream", "0");
288 #endif
289 grpc_init();
290 const auto result = RUN_ALL_TESTS();
291 grpc_shutdown();
292 return result;
293 }
294