• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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