Skip to content

Commit 74ab524

Browse files
committed
feature: WIF Impersonation tests
1 parent 8cf6c5e commit 74ab524

File tree

1 file changed

+244
-1
lines changed

1 file changed

+244
-1
lines changed

tests/test_create_wif_attestation.cpp

Lines changed: 244 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,10 +210,12 @@ const std::string GCP_TEST_ISSUER = "https://accounts.google.com";
210210
const std::string GCP_TEST_SUBJECT = "107562638633288735786";
211211
const std::string GCP_TEST_AUDIENCE = "snowflakecomputing.com";
212212

213+
const std::string GCP_TEST_METADATA_ENDPOINT_HOST = "169.254.169.254";
214+
213215
FakeHttpClient 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+
287530
const std::string AZURE_TEST_ISSUER_ID = "123bdcc4-50e7-4fea-958d-32cdb3ad3aca";
288531
const std::string AZURE_TEST_SUBJECT = "f05bdcc4-50e7-4fea-958d-32cdb12b3aca";
289532

0 commit comments

Comments
 (0)