Skip to content

Commit 908c8d1

Browse files
authored
feat: adding meta header for trust boundary (#1334)
* feat: adding meta header for trust boundary * fixing lint * adding trust_boundary parameter for 3PI init * change inject header to kebab case and the value to a reasonable value
1 parent 17be0db commit 908c8d1

File tree

8 files changed

+82
-7
lines changed

8 files changed

+82
-7
lines changed

google/auth/credentials.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ def __init__(self):
5454
If this is None, the token is assumed to never expire."""
5555
self._quota_project_id = None
5656
"""Optional[str]: Project to use for quota and billing purposes."""
57+
self._trust_boundary = None
58+
"""Optional[str]: Encoded string representation of credentials trust
59+
boundary."""
5760

5861
@property
5962
def expired(self):
@@ -127,6 +130,8 @@ def apply(self, headers, token=None):
127130
headers["authorization"] = "Bearer {}".format(
128131
_helpers.from_bytes(token or self.token)
129132
)
133+
if self._trust_boundary is not None:
134+
headers["x-identity-trust-boundary"] = self._trust_boundary
130135
if self.quota_project_id:
131136
headers["x-goog-user-project"] = self.quota_project_id
132137

google/auth/external_account.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ def __init__(
8686
default_scopes=None,
8787
workforce_pool_user_project=None,
8888
universe_domain=_DEFAULT_UNIVERSE_DOMAIN,
89+
trust_boundary=None,
8990
):
9091
"""Instantiates an external account credentials object.
9192
@@ -111,6 +112,7 @@ def __init__(
111112
billing/quota.
112113
universe_domain (str): The universe domain. The default universe
113114
domain is googleapis.com.
115+
trust_boundary (str): String representation of trust boundary meta.
114116
Raises:
115117
google.auth.exceptions.RefreshError: If the generateAccessToken
116118
endpoint returned an error.
@@ -132,6 +134,7 @@ def __init__(
132134
self._default_scopes = default_scopes
133135
self._workforce_pool_user_project = workforce_pool_user_project
134136
self._universe_domain = universe_domain or _DEFAULT_UNIVERSE_DOMAIN
137+
self._trust_boundary = "0" # expose a placeholder trust boundary value.
135138

136139
if self._client_id:
137140
self._client_auth = utils.ClientAuthentication(

google/oauth2/credentials.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ def __init__(
8484
refresh_handler=None,
8585
enable_reauth_refresh=False,
8686
granted_scopes=None,
87+
trust_boundary=None,
8788
):
8889
"""
8990
Args:
@@ -141,6 +142,7 @@ def __init__(
141142
self._rapt_token = rapt_token
142143
self.refresh_handler = refresh_handler
143144
self._enable_reauth_refresh = enable_reauth_refresh
145+
self._trust_boundary = trust_boundary
144146

145147
def __getstate__(self):
146148
"""A __getstate__ method must exist for the __setstate__ to be called
@@ -171,6 +173,7 @@ def __setstate__(self, d):
171173
self._quota_project_id = d.get("_quota_project_id")
172174
self._rapt_token = d.get("_rapt_token")
173175
self._enable_reauth_refresh = d.get("_enable_reauth_refresh")
176+
self._trust_boundary = d.get("_trust_boundary")
174177
# The refresh_handler setter should be used to repopulate this.
175178
self._refresh_handler = None
176179

@@ -268,6 +271,7 @@ def with_quota_project(self, quota_project_id):
268271
quota_project_id=quota_project_id,
269272
rapt_token=self.rapt_token,
270273
enable_reauth_refresh=self._enable_reauth_refresh,
274+
trust_boundary=self._trust_boundary,
271275
)
272276

273277
@_helpers.copy_docstring(credentials.CredentialsWithTokenUri)
@@ -286,6 +290,7 @@ def with_token_uri(self, token_uri):
286290
quota_project_id=self.quota_project_id,
287291
rapt_token=self.rapt_token,
288292
enable_reauth_refresh=self._enable_reauth_refresh,
293+
trust_boundary=self._trust_boundary,
289294
)
290295

291296
def _metric_header_for_usage(self):
@@ -421,6 +426,7 @@ def from_authorized_user_info(cls, info, scopes=None):
421426
quota_project_id=info.get("quota_project_id"), # may not exist
422427
expiry=expiry,
423428
rapt_token=info.get("rapt_token"), # may not exist
429+
trust_boundary=info.get("trust_boundary"), # may not exist
424430
)
425431

426432
@classmethod

google/oauth2/service_account.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ def __init__(
140140
additional_claims=None,
141141
always_use_jwt_access=False,
142142
universe_domain=_DEFAULT_UNIVERSE_DOMAIN,
143+
trust_boundary=None,
143144
):
144145
"""
145146
Args:
@@ -163,6 +164,7 @@ def __init__(
163164
universe_domain (str): The universe domain. The default
164165
universe domain is googleapis.com. For default value self
165166
signed jwt is used for token refresh.
167+
trust_boundary (str): String representation of trust boundary meta.
166168
167169
.. note:: Typically one of the helper constructors
168170
:meth:`from_service_account_file` or
@@ -194,6 +196,7 @@ def __init__(
194196
self._additional_claims = additional_claims
195197
else:
196198
self._additional_claims = {}
199+
self._trust_boundary = "0"
197200

198201
@classmethod
199202
def _from_signer_and_info(cls, signer, info, **kwargs):
@@ -217,6 +220,7 @@ def _from_signer_and_info(cls, signer, info, **kwargs):
217220
token_uri=info["token_uri"],
218221
project_id=info.get("project_id"),
219222
universe_domain=info.get("universe_domain", _DEFAULT_UNIVERSE_DOMAIN),
223+
trust_boundary=info.get("trust_boundary"),
220224
**kwargs
221225
)
222226

tests/test_aws.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1969,6 +1969,7 @@ def test_refresh_success_with_impersonation_ignore_default_scopes(
19691969
"authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
19701970
"x-goog-user-project": QUOTA_PROJECT_ID,
19711971
"x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
1972+
"x-identity-trust-boundary": "0",
19721973
}
19731974
impersonation_request_data = {
19741975
"delegates": None,
@@ -2065,6 +2066,7 @@ def test_refresh_success_with_impersonation_use_default_scopes(
20652066
"authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
20662067
"x-goog-user-project": QUOTA_PROJECT_ID,
20672068
"x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
2069+
"x-identity-trust-boundary": "0",
20682070
}
20692071
impersonation_request_data = {
20702072
"delegates": None,

tests/test_credentials.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ def test_before_request():
8080
assert credentials.valid
8181
assert credentials.token == "token"
8282
assert headers["authorization"] == "Bearer token"
83+
assert "x-identity-trust-boundary" not in headers
8384

8485
request = "token2"
8586
headers = {}
@@ -89,6 +90,32 @@ def test_before_request():
8990
assert credentials.valid
9091
assert credentials.token == "token"
9192
assert headers["authorization"] == "Bearer token"
93+
assert "x-identity-trust-boundary" not in headers
94+
95+
96+
def test_before_request_with_trust_boundary():
97+
DUMMY_BOUNDARY = "00110101"
98+
credentials = CredentialsImpl()
99+
credentials._trust_boundary = DUMMY_BOUNDARY
100+
request = "token"
101+
headers = {}
102+
103+
# First call should call refresh, setting the token.
104+
credentials.before_request(request, "http://example.com", "GET", headers)
105+
assert credentials.valid
106+
assert credentials.token == "token"
107+
assert headers["authorization"] == "Bearer token"
108+
assert headers["x-identity-trust-boundary"] == DUMMY_BOUNDARY
109+
110+
request = "token2"
111+
headers = {}
112+
113+
# Second call shouldn't call refresh.
114+
credentials.before_request(request, "http://example.com", "GET", headers)
115+
assert credentials.valid
116+
assert credentials.token == "token"
117+
assert headers["authorization"] == "Bearer token"
118+
assert headers["x-identity-trust-boundary"] == DUMMY_BOUNDARY
92119

93120

94121
def test_before_request_metrics():

tests/test_external_account.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,7 @@ def test_refresh_impersonation_without_client_auth_success(
825825
"Content-Type": "application/json",
826826
"authorization": "Bearer {}".format(token_response["access_token"]),
827827
"x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
828+
"x-identity-trust-boundary": "0",
828829
}
829830
impersonation_request_data = {
830831
"delegates": None,
@@ -906,6 +907,7 @@ def test_refresh_workforce_impersonation_without_client_auth_success(
906907
"Content-Type": "application/json",
907908
"authorization": "Bearer {}".format(token_response["access_token"]),
908909
"x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
910+
"x-identity-trust-boundary": "0",
909911
}
910912
impersonation_request_data = {
911913
"delegates": None,
@@ -1124,6 +1126,7 @@ def test_refresh_impersonation_with_client_auth_success_ignore_default_scopes(
11241126
"Content-Type": "application/json",
11251127
"authorization": "Bearer {}".format(token_response["access_token"]),
11261128
"x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
1129+
"x-identity-trust-boundary": "0",
11271130
}
11281131
impersonation_request_data = {
11291132
"delegates": None,
@@ -1207,6 +1210,7 @@ def test_refresh_impersonation_with_client_auth_success_use_default_scopes(
12071210
"Content-Type": "application/json",
12081211
"authorization": "Bearer {}".format(token_response["access_token"]),
12091212
"x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
1213+
"x-identity-trust-boundary": "0",
12101214
}
12111215
impersonation_request_data = {
12121216
"delegates": None,
@@ -1261,7 +1265,8 @@ def test_apply_without_quota_project_id(self):
12611265
credentials.apply(headers)
12621266

12631267
assert headers == {
1264-
"authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"])
1268+
"authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
1269+
"x-identity-trust-boundary": "0",
12651270
}
12661271

12671272
def test_apply_workforce_without_quota_project_id(self):
@@ -1277,7 +1282,8 @@ def test_apply_workforce_without_quota_project_id(self):
12771282
credentials.apply(headers)
12781283

12791284
assert headers == {
1280-
"authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"])
1285+
"authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
1286+
"x-identity-trust-boundary": "0",
12811287
}
12821288

12831289
def test_apply_impersonation_without_quota_project_id(self):
@@ -1308,7 +1314,8 @@ def test_apply_impersonation_without_quota_project_id(self):
13081314
credentials.apply(headers)
13091315

13101316
assert headers == {
1311-
"authorization": "Bearer {}".format(impersonation_response["accessToken"])
1317+
"authorization": "Bearer {}".format(impersonation_response["accessToken"]),
1318+
"x-identity-trust-boundary": "0",
13121319
}
13131320

13141321
def test_apply_with_quota_project_id(self):
@@ -1325,6 +1332,7 @@ def test_apply_with_quota_project_id(self):
13251332
"other": "header-value",
13261333
"authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
13271334
"x-goog-user-project": self.QUOTA_PROJECT_ID,
1335+
"x-identity-trust-boundary": "0",
13281336
}
13291337

13301338
def test_apply_impersonation_with_quota_project_id(self):
@@ -1359,6 +1367,7 @@ def test_apply_impersonation_with_quota_project_id(self):
13591367
"other": "header-value",
13601368
"authorization": "Bearer {}".format(impersonation_response["accessToken"]),
13611369
"x-goog-user-project": self.QUOTA_PROJECT_ID,
1370+
"x-identity-trust-boundary": "0",
13621371
}
13631372

13641373
def test_before_request(self):
@@ -1374,6 +1383,7 @@ def test_before_request(self):
13741383
assert headers == {
13751384
"other": "header-value",
13761385
"authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
1386+
"x-identity-trust-boundary": "0",
13771387
}
13781388

13791389
# Second call shouldn't call refresh.
@@ -1382,6 +1392,7 @@ def test_before_request(self):
13821392
assert headers == {
13831393
"other": "header-value",
13841394
"authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
1395+
"x-identity-trust-boundary": "0",
13851396
}
13861397

13871398
def test_before_request_workforce(self):
@@ -1399,6 +1410,7 @@ def test_before_request_workforce(self):
13991410
assert headers == {
14001411
"other": "header-value",
14011412
"authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
1413+
"x-identity-trust-boundary": "0",
14021414
}
14031415

14041416
# Second call shouldn't call refresh.
@@ -1407,6 +1419,7 @@ def test_before_request_workforce(self):
14071419
assert headers == {
14081420
"other": "header-value",
14091421
"authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
1422+
"x-identity-trust-boundary": "0",
14101423
}
14111424

14121425
def test_before_request_impersonation(self):
@@ -1437,6 +1450,7 @@ def test_before_request_impersonation(self):
14371450
assert headers == {
14381451
"other": "header-value",
14391452
"authorization": "Bearer {}".format(impersonation_response["accessToken"]),
1453+
"x-identity-trust-boundary": "0",
14401454
}
14411455

14421456
# Second call shouldn't call refresh.
@@ -1445,6 +1459,7 @@ def test_before_request_impersonation(self):
14451459
assert headers == {
14461460
"other": "header-value",
14471461
"authorization": "Bearer {}".format(impersonation_response["accessToken"]),
1462+
"x-identity-trust-boundary": "0",
14481463
}
14491464

14501465
@mock.patch("google.auth._helpers.utcnow")
@@ -1470,7 +1485,10 @@ def test_before_request_expired(self, utcnow):
14701485
credentials.before_request(request, "POST", "https://example.com/api", headers)
14711486

14721487
# Cached token should be used.
1473-
assert headers == {"authorization": "Bearer token"}
1488+
assert headers == {
1489+
"authorization": "Bearer token",
1490+
"x-identity-trust-boundary": "0",
1491+
}
14741492

14751493
# Next call should simulate 1 second passed.
14761494
utcnow.return_value = datetime.datetime.min + datetime.timedelta(seconds=1)
@@ -1482,7 +1500,8 @@ def test_before_request_expired(self, utcnow):
14821500

14831501
# New token should be retrieved.
14841502
assert headers == {
1485-
"authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"])
1503+
"authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
1504+
"x-identity-trust-boundary": "0",
14861505
}
14871506

14881507
@mock.patch("google.auth._helpers.utcnow")
@@ -1523,7 +1542,10 @@ def test_before_request_impersonation_expired(self, utcnow):
15231542
credentials.before_request(request, "POST", "https://example.com/api", headers)
15241543

15251544
# Cached token should be used.
1526-
assert headers == {"authorization": "Bearer token"}
1545+
assert headers == {
1546+
"authorization": "Bearer token",
1547+
"x-identity-trust-boundary": "0",
1548+
}
15271549

15281550
# Next call should simulate 1 second passed. This will trigger the expiration
15291551
# threshold.
@@ -1536,7 +1558,8 @@ def test_before_request_impersonation_expired(self, utcnow):
15361558

15371559
# New token should be retrieved.
15381560
assert headers == {
1539-
"authorization": "Bearer {}".format(impersonation_response["accessToken"])
1561+
"authorization": "Bearer {}".format(impersonation_response["accessToken"]),
1562+
"x-identity-trust-boundary": "0",
15401563
}
15411564

15421565
@pytest.mark.parametrize(
@@ -1635,6 +1658,7 @@ def test_get_project_id_cloud_resource_manager_success(
16351658
"x-goog-user-project": self.QUOTA_PROJECT_ID,
16361659
"authorization": "Bearer {}".format(token_response["access_token"]),
16371660
"x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
1661+
"x-identity-trust-boundary": "0",
16381662
}
16391663
impersonation_request_data = {
16401664
"delegates": None,
@@ -1688,6 +1712,7 @@ def test_get_project_id_cloud_resource_manager_success(
16881712
"authorization": "Bearer {}".format(
16891713
impersonation_response["accessToken"]
16901714
),
1715+
"x-identity-trust-boundary": "0",
16911716
},
16921717
)
16931718

@@ -1759,6 +1784,7 @@ def test_workforce_pool_get_project_id_cloud_resource_manager_success(
17591784
"authorization": "Bearer {}".format(
17601785
self.SUCCESS_RESPONSE["access_token"]
17611786
),
1787+
"x-identity-trust-boundary": "0",
17621788
},
17631789
)
17641790

@@ -1808,6 +1834,7 @@ def test_refresh_impersonation_with_lifetime(
18081834
"Content-Type": "application/json",
18091835
"authorization": "Bearer {}".format(token_response["access_token"]),
18101836
"x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
1837+
"x-identity-trust-boundary": "0",
18111838
}
18121839
impersonation_request_data = {
18131840
"delegates": None,

tests/test_identity_pool.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,7 @@ def assert_underlying_credentials_refresh(
319319
"Content-Type": "application/json",
320320
"authorization": "Bearer {}".format(token_response["access_token"]),
321321
"x-goog-api-client": metrics_header_value,
322+
"x-identity-trust-boundary": "0",
322323
}
323324
impersonation_request_data = {
324325
"delegates": None,

0 commit comments

Comments
 (0)