Skip to content

Commit 1beee8f

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

File tree

1 file changed

+258
-1
lines changed

1 file changed

+258
-1
lines changed

tests/test_create_wif_attestation.cpp

Lines changed: 258 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,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+
287544
const std::string AZURE_TEST_ISSUER_ID = "123bdcc4-50e7-4fea-958d-32cdb3ad3aca";
288545
const std::string AZURE_TEST_SUBJECT = "f05bdcc4-50e7-4fea-958d-32cdb12b3aca";
289546

0 commit comments

Comments
 (0)