@@ -210,10 +210,12 @@ const std::string GCP_TEST_ISSUER = "https://accounts.google.com";
210210const std::string GCP_TEST_SUBJECT = " 107562638633288735786" ;
211211const std::string GCP_TEST_AUDIENCE = " snowflakecomputing.com" ;
212212
213+ const std::string GCP_TEST_METADATA_ENDPOINT_HOST = " 169.254.169.254" ;
214+
213215FakeHttpClient makeSuccessfulGCPHttpClient (const std::vector<char > &token) {
214216 return FakeHttpClient ([=](Snowflake::Client::HttpRequest req) {
215217 assert_true ((*req.url .params ().find (" audience" )).value == GCP_TEST_AUDIENCE);
216- assert_true (req.url .host () == " 169.254.169.254 " );
218+ assert_true (req.url .host () == GCP_TEST_METADATA_ENDPOINT_HOST );
217219 assert_true (req.url .scheme () == " http" );
218220 HttpResponse response;
219221 response.code = 200 ;
@@ -284,6 +286,261 @@ void test_unit_gcp_attestation_bad_request(void **) {
284286 assert_true (!attestationOpt);
285287}
286288
289+ const std::string GCP_TEST_SUBJECT_ACCESS = " 107562638633288735787" ;
290+
291+ const std::string GCP_TEST_IAM_ENDPOINT_HOST = " iamcredentials.googleapis.com" ;
292+
293+ // Multi-path fake HTTP client for GCP service account impersonation
294+ enum class AcceptedHosts {
295+ Metadata,
296+ Iam,
297+ Other
298+ };
299+
300+ auto getHost (const std::string& host) -> AcceptedHosts {
301+ if (host == GCP_TEST_METADATA_ENDPOINT_HOST) return AcceptedHosts::Metadata;
302+ if (host == GCP_TEST_IAM_ENDPOINT_HOST) return AcceptedHosts::Iam;
303+ return AcceptedHosts::Other;
304+ }
305+
306+ FakeHttpClient makeSuccessfulGCPImpersonationHttpClient (
307+ const std::vector<char >& accessToken,
308+ const std::vector<char >& idToken,
309+ const std::vector<std::string>& expectedDelegates,
310+ const std::string& expectedTargetServiceAccount) {
311+ return FakeHttpClient ([=](Snowflake::Client::HttpRequest req) {
312+ HttpResponse response;
313+ response.code = 200 ;
314+
315+ switch (getHost (req.url .host ())) {
316+ case AcceptedHosts::Metadata: {
317+ if (req.url .encoded_path () == " /computeMetadata/v1/instance/service-accounts/default/token" ) {
318+ assert_true (req.headers .find (" Metadata-Flavor" )->second == " Google" );
319+ response.buffer = accessToken;
320+ }
321+ break ;
322+ }
323+ case AcceptedHosts::Iam: {
324+ std::string expectedPath = " /v1/projects/-/serviceAccounts/" +
325+ expectedTargetServiceAccount + " :generateIdToken" ;
326+ assert_true (req.url .encoded_path () == expectedPath);
327+ assert_true (req.method == HttpRequest::Method::POST);
328+ const auto accessTokenStr = std::string (accessToken.data (), accessToken.size ());
329+ assert_true (req.headers .find (" Authorization" )->second == " Bearer " + accessTokenStr);
330+ assert_true (req.headers .find (" Content-Type" )->second == " application/json" );
331+
332+ picojson::value bodyJson;
333+ std::string err = picojson::parse (bodyJson, req.body );
334+ assert_true (err.empty ());
335+ assert_true (bodyJson.is <picojson::object>());
336+
337+ auto bodyObj = bodyJson.get <picojson::object>();
338+ assert_true (bodyObj[" audience" ].get <std::string>() == GCP_TEST_AUDIENCE);
339+ assert_true (bodyObj[" includeEmail" ].get <bool >() == true );
340+
341+ if (!expectedDelegates.empty ()) {
342+ assert_true (bodyObj.find (" delegates" ) != bodyObj.end ());
343+ auto delegates = bodyObj[" delegates" ].get <picojson::array>();
344+ assert_true (delegates.size () == expectedDelegates.size ());
345+ for (size_t i = 0 ; i < expectedDelegates.size (); ++i) {
346+ std::string expected = " projects/-/serviceAccounts/" + expectedDelegates[i];
347+ assert_true (delegates[i].get <std::string>() == expected);
348+ }
349+ }
350+
351+ response.buffer = idToken;
352+ break ;
353+ }
354+ case AcceptedHosts::Other: {
355+ // Leave response as default.
356+ break ;
357+ }
358+ }
359+
360+ return response;
361+ });
362+ }
363+
364+ void test_unit_gcp_impersonation_single_account_success (void **) {
365+ const auto accessToken = makeGCPToken (GCP_TEST_ISSUER, GCP_TEST_SUBJECT_ACCESS);
366+ const auto idToken = makeGCPToken (GCP_TEST_ISSUER, GCP_TEST_SUBJECT);
367+ const std::string targetServiceAccount =
" [email protected] " ;
368+
369+ auto fakeHttpClient = makeSuccessfulGCPImpersonationHttpClient (
370+ accessToken,
371+ idToken,
372+ {},
373+ targetServiceAccount);
374+
375+ AttestationConfig config;
376+ config.type = AttestationType::GCP;
377+ config.httpClient = &fakeHttpClient;
378+ config.workloadIdentityImpersonationPath = targetServiceAccount;
379+
380+ const auto attestationOpt = createAttestation (config);
381+ assert_true (attestationOpt.has_value ());
382+ const auto &[type, credential, issuer, subject] = attestationOpt.get ();
383+ assert_true (type == AttestationType::GCP);
384+ assert_true (credential == std::string (idToken.data (), idToken.size ()));
385+ assert_true (subject == GCP_TEST_SUBJECT);
386+ assert_true (issuer == GCP_TEST_ISSUER);
387+ }
388+
389+ void test_unit_gcp_impersonation_chain_success (void **) {
390+ const auto accessToken = makeGCPToken (GCP_TEST_ISSUER, GCP_TEST_SUBJECT_ACCESS);
391+ const auto idToken = makeGCPToken (GCP_TEST_ISSUER, GCP_TEST_SUBJECT);
392+ const std::vector<std::string> delegates = {
393+ 394+ 395+ };
396+ const std::string targetServiceAccount =
" [email protected] " ;
397+
398+ auto fakeHttpClient = makeSuccessfulGCPImpersonationHttpClient (
399+ accessToken,
400+ idToken,
401+ delegates,
402+ targetServiceAccount);
403+
404+ AttestationConfig config;
405+ config.type = AttestationType::GCP;
406+ config.httpClient = &fakeHttpClient;
407+
408+ std::string workloadIdentityImpersonationPath;
409+ for (const auto &delegate: delegates) {
410+ workloadIdentityImpersonationPath += delegate + " ," ;
411+ }
412+ workloadIdentityImpersonationPath += targetServiceAccount;
413+ config.workloadIdentityImpersonationPath = workloadIdentityImpersonationPath;
414+
415+ const auto attestationOpt = createAttestation (config);
416+ assert_true (attestationOpt.has_value ());
417+ const auto &[type, credential, issuer, subject] = attestationOpt.get ();
418+ assert_true (type == AttestationType::GCP);
419+ assert_true (credential == std::string (idToken.data (), idToken.size ()));
420+ assert_true (subject == GCP_TEST_SUBJECT);
421+ }
422+
423+ void test_unit_gcp_impersonation_whitespace_in_path (void **) {
424+ const auto accessToken = makeGCPToken (GCP_TEST_ISSUER, GCP_TEST_SUBJECT_ACCESS);
425+ const auto idToken = makeGCPToken (GCP_TEST_ISSUER, GCP_TEST_SUBJECT);
426+ const std::vector<std::string> delegates = {
427+ 428+ 429+ };
430+ const std::string targetServiceAccount =
" [email protected] " ;
431+
432+ auto fakeHttpClient = makeSuccessfulGCPImpersonationHttpClient (
433+ accessToken,
434+ idToken,
435+ delegates,
436+ targetServiceAccount);
437+
438+ AttestationConfig config;
439+ config.type = AttestationType::GCP;
440+ config.httpClient = &fakeHttpClient;
441+
442+ std::string workloadIdentityImpersonationPath = " " ;
443+ for (const auto &delegate: delegates) {
444+ workloadIdentityImpersonationPath += " " + delegate + " , " ;
445+ }
446+ workloadIdentityImpersonationPath += targetServiceAccount + " " ;
447+ config.workloadIdentityImpersonationPath = workloadIdentityImpersonationPath;
448+
449+ const auto attestationOpt = createAttestation (config);
450+ assert_true (attestationOpt.has_value ());
451+ }
452+
453+ void test_unit_gcp_impersonation_access_token_failed (void **) {
454+ auto fakeHttpClient = FakeHttpClient ([](const HttpRequest &req) {
455+ if (req.url .host () == GCP_TEST_METADATA_ENDPOINT_HOST) {
456+ HttpResponse response;
457+ response.code = 404 ;
458+ return boost::optional<HttpResponse>(response);
459+ }
460+ return boost::optional<HttpResponse>(boost::none);
461+ });
462+
463+ AttestationConfig config;
464+ config.type = AttestationType::GCP;
465+ config.httpClient = &fakeHttpClient;
466+ config.
workloadIdentityImpersonationPath =
" [email protected] " ;
467+
468+ const auto attestationOpt = createAttestation (config);
469+ assert_false (attestationOpt.has_value ());
470+ }
471+
472+ void test_unit_gcp_impersonation_id_token_failed (void **) {
473+ const auto accessToken = makeGCPToken (GCP_TEST_ISSUER, GCP_TEST_SUBJECT_ACCESS);
474+
475+ auto fakeHttpClient = FakeHttpClient ([=](const HttpRequest &req) {
476+ if (req.url .host () == GCP_TEST_METADATA_ENDPOINT_HOST) {
477+ HttpResponse response;
478+ response.code = 200 ;
479+ response.buffer = accessToken;
480+ return boost::optional<HttpResponse>(response);
481+ }
482+ if (req.url .host () == GCP_TEST_IAM_ENDPOINT_HOST) {
483+ HttpResponse response;
484+ response.code = 403 ;
485+ const std::string error = " Forbidden" ;
486+ response.buffer = std::vector<char >(error.begin (), error.end ());
487+ return boost::optional<HttpResponse>(response);
488+ }
489+ return boost::optional<HttpResponse>(boost::none);
490+ });
491+
492+ AttestationConfig config;
493+ config.type = AttestationType::GCP;
494+ config.httpClient = &fakeHttpClient;
495+ config.
workloadIdentityImpersonationPath =
" [email protected] " ;
496+
497+ const auto attestationOpt = createAttestation (config);
498+ assert_false (attestationOpt.has_value ());
499+ }
500+
501+ void test_unit_gcp_impersonation_empty_path (void **) {
502+ const auto idToken = makeGCPToken (GCP_TEST_ISSUER, GCP_TEST_SUBJECT);
503+ auto fakeHttpClient = makeSuccessfulGCPHttpClient (idToken);
504+
505+ AttestationConfig config;
506+ config.type = AttestationType::GCP;
507+ config.httpClient = &fakeHttpClient;
508+ // Empty path should use direct flow
509+ config.workloadIdentityImpersonationPath = " " ;
510+
511+ const auto attestationOpt = createAttestation (config);
512+ assert_true (attestationOpt.has_value ());
513+ }
514+
515+ void test_unit_gcp_impersonation_missing_token_in_response (void **) {
516+ const auto accessToken = makeGCPToken (GCP_TEST_ISSUER, GCP_TEST_SUBJECT_ACCESS);
517+
518+ auto fakeHttpClient = FakeHttpClient ([=](const HttpRequest &req) {
519+ if (req.url .host () == GCP_TEST_METADATA_ENDPOINT_HOST) {
520+ HttpResponse response;
521+ response.code = 200 ;
522+ response.buffer = accessToken;
523+ return boost::optional<HttpResponse>(response);
524+ }
525+ if (req.url .host () == GCP_TEST_IAM_ENDPOINT_HOST) {
526+ HttpResponse response;
527+ response.code = 200 ;
528+ const std::string body = " {\" invalid_field\" : \" value\" }" ;
529+ response.buffer = std::vector<char >(body.begin (), body.end ());
530+ return boost::optional<HttpResponse>(response);
531+ }
532+ return boost::optional<HttpResponse>(boost::none);
533+ });
534+
535+ AttestationConfig config;
536+ config.type = AttestationType::GCP;
537+ config.httpClient = &fakeHttpClient;
538+ config.
workloadIdentityImpersonationPath =
" [email protected] " ;
539+
540+ const auto attestationOpt = createAttestation (config);
541+ assert_false (attestationOpt.has_value ());
542+ }
543+
287544const std::string AZURE_TEST_ISSUER_ID = " 123bdcc4-50e7-4fea-958d-32cdb3ad3aca" ;
288545const std::string AZURE_TEST_SUBJECT = " f05bdcc4-50e7-4fea-958d-32cdb12b3aca" ;
289546
0 commit comments