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