Skip to content

Commit e383636

Browse files
author
Ryan Kohler
authored
test: Create AWS-based external account integration tests (#731)
1 parent 5832fc1 commit e383636

File tree

3 files changed

+122
-3
lines changed

3 files changed

+122
-3
lines changed

scripts/setup_external_accounts.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,4 @@ gcloud iam service-accounts add-iam-policy-binding $service_account_email \
110110

111111
echo "OIDC audience: "$oidc_aud
112112
echo "AWS audience: "$aws_aud
113+
echo "AWS role: arn:aws:iam::$aws_account_id:role/$aws_role_name"

system_tests/system_tests_sync/conftest.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,40 @@ def authorized_user_file():
5454

5555

5656
@pytest.fixture(params=["urllib3", "requests"])
57-
def http_request(request):
57+
def request_type(request):
58+
yield request.param
59+
60+
61+
@pytest.fixture
62+
def http_request(request_type):
5863
"""A transport.request object."""
59-
if request.param == "urllib3":
64+
if request_type == "urllib3":
6065
yield google.auth.transport.urllib3.Request(URLLIB3_HTTP)
61-
elif request.param == "requests":
66+
elif request_type == "requests":
6267
yield google.auth.transport.requests.Request(REQUESTS_SESSION)
6368

6469

70+
@pytest.fixture
71+
def authenticated_request(request_type):
72+
"""A transport.request object that takes credentials"""
73+
if request_type == "urllib3":
74+
75+
def wrapper(credentials):
76+
return google.auth.transport.urllib3.AuthorizedHttp(
77+
credentials, http=URLLIB3_HTTP
78+
).request
79+
80+
yield wrapper
81+
elif request_type == "requests":
82+
83+
def wrapper(credentials):
84+
session = google.auth.transport.requests.AuthorizedSession(credentials)
85+
session.verify = False
86+
return google.auth.transport.requests.Request(session)
87+
88+
yield wrapper
89+
90+
6591
@pytest.fixture
6692
def token_info(http_request):
6793
"""Returns a function that obtains OAuth2 token info."""

system_tests/system_tests_sync/test_external_accounts.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848

4949
# Populate values from the output of scripts/setup_external_accounts.sh.
5050
_AUDIENCE_OIDC = "//iam.googleapis.com/projects/79992041559/locations/global/workloadIdentityPools/pool-73wslmxn/providers/oidc-73wslmxn"
51+
_AUDIENCE_AWS = "//iam.googleapis.com/projects/79992041559/locations/global/workloadIdentityPools/pool-73wslmxn/providers/aws-73wslmxn"
52+
_ROLE_AWS = "arn:aws:iam::077071391996:role/ci-python-test"
5153

5254

5355
def dns_access_direct(request, project_id):
@@ -100,6 +102,27 @@ def service_account_info(service_account_file):
100102
yield json.load(f)
101103

102104

105+
@pytest.fixture
106+
def aws_oidc_credentials(
107+
service_account_file, service_account_info, authenticated_request
108+
):
109+
credentials = service_account.Credentials.from_service_account_file(
110+
service_account_file, scopes=["https://www.googleapis.com/auth/cloud-platform"]
111+
)
112+
result = authenticated_request(credentials)(
113+
url="https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{}:generateIdToken".format(
114+
service_account_info["client_email"]
115+
),
116+
method="POST",
117+
body=json.dumps(
118+
{"audience": service_account_info["client_id"], "includeEmail": True}
119+
),
120+
)
121+
assert result.status == 200
122+
123+
yield json.loads(result.data)["token"]
124+
125+
103126
# Our external accounts tests involve setting up some preconditions, setting a
104127
# credential file, and then making sure that our client libraries can work with
105128
# the set credentials.
@@ -115,6 +138,14 @@ def get_project_dns(dns_access, credential_data):
115138
return dns_access()
116139

117140

141+
def get_xml_value_by_tagname(data, tagname):
142+
startIndex = data.index("<{}>".format(tagname))
143+
if startIndex >= 0:
144+
endIndex = data.index("</{}>".format(tagname), startIndex)
145+
if endIndex > startIndex:
146+
return data[startIndex + len(tagname) + 2 : endIndex]
147+
148+
118149
# This test makes sure that setting an accesible credential file
119150
# works to allow access to Google resources.
120151
def test_file_based_external_account(
@@ -211,3 +242,64 @@ def __enter__(self):
211242
},
212243
},
213244
)
245+
246+
247+
# AWS provider tests for AWS credentials
248+
# The test suite will also run tests for AWS credentials. This works as
249+
# follows. (Note prequisite setup is needed. This is documented in
250+
# setup_external_accounts.sh).
251+
# - iamcredentials:generateIdToken is used to generate a Google ID token using
252+
# the service account access token. The service account client_id is used as
253+
# audience.
254+
# - AWS STS AssumeRoleWithWebIdentity API is used to exchange this token for
255+
# temporary AWS security credentials for a specified AWS ARN role.
256+
# - AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN
257+
# environment variables are set using these credentials before the test is
258+
# run simulating an AWS VM.
259+
# - The test can now be run.
260+
def test_aws_based_external_account(
261+
aws_oidc_credentials, service_account_info, dns_access, http_request
262+
):
263+
264+
response = http_request(
265+
url=(
266+
"https://sts.amazonaws.com/"
267+
"?Action=AssumeRoleWithWebIdentity"
268+
"&Version=2011-06-15"
269+
"&DurationSeconds=3600"
270+
"&RoleSessionName=python-test"
271+
"&RoleArn={}"
272+
"&WebIdentityToken={}"
273+
).format(_ROLE_AWS, aws_oidc_credentials)
274+
)
275+
assert response.status == 200
276+
277+
# The returned data is in XML, but loading an XML parser would be overkill.
278+
# Searching the return text manually for the start and finish tag.
279+
data = response.data.decode("utf-8")
280+
281+
with patch.dict(
282+
os.environ,
283+
{
284+
"AWS_REGION": "us-east-2",
285+
"AWS_ACCESS_KEY_ID": get_xml_value_by_tagname(data, "AccessKeyId"),
286+
"AWS_SECRET_ACCESS_KEY": get_xml_value_by_tagname(data, "SecretAccessKey"),
287+
"AWS_SESSION_TOKEN": get_xml_value_by_tagname(data, "SessionToken"),
288+
},
289+
):
290+
assert get_project_dns(
291+
dns_access,
292+
{
293+
"type": "external_account",
294+
"audience": _AUDIENCE_AWS,
295+
"subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
296+
"token_url": "https://sts.googleapis.com/v1/token",
297+
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{}:generateAccessToken".format(
298+
service_account_info["client_email"]
299+
),
300+
"credential_source": {
301+
"environment_id": "aws1",
302+
"regional_cred_verification_url": "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
303+
},
304+
},
305+
)

0 commit comments

Comments
 (0)