Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
41599ae
refactor: split 'with_quota_project' into separate base class (#561)
busunkim96 Sep 2, 2020
d32f7df
fix: dummy commit to trigger a auto release (#597)
arithmetic1728 Sep 3, 2020
892dc37
chore: release 1.21.1 (#599)
release-please[bot] Sep 3, 2020
694d83f
fix: migrate signBlob to iamcredentials.googleapis.com (#600)
busunkim96 Sep 8, 2020
b921a0a
chore: release 1.21.2 (#601)
release-please[bot] Sep 16, 2020
d0e0aba
fix: fix expiry for `to_json()` (#589)
wescpy Sep 17, 2020
da3526f
chore: add default CODEOWNERS (#609)
busunkim96 Sep 22, 2020
cc91e75
chore: release 1.21.3 (#607)
release-please[bot] Sep 23, 2020
7e15258
feat: add asyncio based auth flow (#612)
crwilcox Sep 28, 2020
ee5617c
chore: release 1.22.0 (#615)
release-please[bot] Sep 28, 2020
a924011
fix: move aiohttp to extra as it is currently internal surface (#619)
crwilcox Oct 5, 2020
7f957ba
chore: release 1.22.1 (#620)
release-please[bot] Oct 5, 2020
6407258
fix: remove checks for ancient versions of Cryptography (#596)
akx Oct 8, 2020
3b3172e
tests: fix unit tests on python 3.6 / 3.7 (#630)
tseaver Oct 22, 2020
870e1a5
Merge remote-tracking branch 'upstream/byoid'
bojeil-google Oct 22, 2020
5906c85
Change metadata service helper to work with any query parameters (#588)
davidwtbuxton Oct 23, 2020
8c5ed6f
Merge branch 'master' of https://github.com/googleapis/google-auth-li…
bojeil-google Oct 23, 2020
7533d6b
feat: implements `get_project_id()` for google.auth.external_account.…
bojeil-google Oct 23, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions google/auth/compute_engine/_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,9 @@ def ping(request, timeout=_METADATA_DEFAULT_TIMEOUT, retry_count=3):
return False


def get(request, path, root=_METADATA_ROOT, recursive=False, retry_count=5):
def get(
request, path, root=_METADATA_ROOT, params=None, recursive=False, retry_count=5
):
"""Fetch a resource from the metadata server.

Args:
Expand All @@ -117,6 +119,8 @@ def get(request, path, root=_METADATA_ROOT, recursive=False, retry_count=5):
path (str): The resource to retrieve. For example,
``'instance/service-accounts/default'``.
root (str): The full path to the metadata server root.
params (Optional[Mapping[str, str]]): A mapping of query parameter
keys to values.
recursive (bool): Whether to do a recursive query of metadata. See
https://cloud.google.com/compute/docs/metadata#aggcontents for more
details.
Expand All @@ -133,7 +137,7 @@ def get(request, path, root=_METADATA_ROOT, recursive=False, retry_count=5):
retrieving metadata.
"""
base_url = urlparse.urljoin(root, path)
query_params = {}
query_params = {} if params is None else params

if recursive:
query_params["recursive"] = "true"
Expand Down Expand Up @@ -224,11 +228,10 @@ def get_service_account_info(request, service_account="default"):
google.auth.exceptions.TransportError: if an error occurred while
retrieving metadata.
"""
return get(
request,
"instance/service-accounts/{0}/".format(service_account),
recursive=True,
)
path = "instance/service-accounts/{0}/".format(service_account)
# See https://cloud.google.com/compute/docs/metadata#aggcontents
# for more on the use of 'recursive'.
return get(request, path, params={"recursive": "true"})


def get_service_account_token(request, service_account="default"):
Expand Down
9 changes: 3 additions & 6 deletions google/auth/compute_engine/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,12 +323,9 @@ def _call_metadata_identity_endpoint(self, request):
ValueError: If extracting expiry from the obtained ID token fails.
"""
try:
id_token = _metadata.get(
request,
"instance/service-accounts/default/identity?audience={}&format=full".format(
self._target_audience
),
)
path = "instance/service-accounts/default/identity"
params = {"audience": self._target_audience, "format": "full"}
id_token = _metadata.get(request, path, params=params)
except exceptions.TransportError as caught_exc:
new_exc = exceptions.RefreshError(caught_exc)
six.raise_from(new_exc, caught_exc)
Expand Down
61 changes: 61 additions & 0 deletions google/auth/external_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

import abc
import datetime
import json

import six

Expand All @@ -42,6 +43,8 @@
_STS_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"
# The token exchange requested_token_type. This is always an access_token.
_STS_REQUESTED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token"
# Cloud resource manager URL used to retrieve project information.
_CLOUD_RESOURCE_MANAGER = "https://cloudresourcemanager.googleapis.com/v1/projects/"


@six.add_metaclass(abc.ABCMeta)
Expand Down Expand Up @@ -107,6 +110,7 @@ def __init__(
self._impersonated_credentials = self._initialize_impersonated_credentials()
else:
self._impersonated_credentials = None
self._project_id = None

@property
def requires_scopes(self):
Expand All @@ -117,6 +121,20 @@ def requires_scopes(self):
"""
return True if not self._scopes else False

@property
def project_number(self):
"""Optional[str]: The project number corresponding to the workload identity pool."""

# STS audience pattern:
# //iam.googleapis.com/projects/$PROJECT_NUMBER/locations/...
components = self._audience.split("/")
try:
project_index = components.index("projects")
if project_index + 1 < len(components):
return components[project_index + 1] or None
except ValueError:
return None

@_helpers.copy_docstring(credentials.Scoped)
def with_scopes(self, scopes):
return self.__class__(
Expand Down Expand Up @@ -144,6 +162,49 @@ def retrieve_subject_token(self, request):
# (pylint doesn't recognize that this is abstract)
raise NotImplementedError("retrieve_subject_token must be implemented")

def get_project_id(self, request):
"""Retrieves the project ID corresponding to the workload identity pool.

When not determinable, None is returned.

This is introduced to support the current pattern of using the Auth library:

credentials, project_id = google.auth.default()

The resource may not have permission (resourcemanager.projects.get) to
call this API or the required scopes may not be selected:
https://cloud.google.com/resource-manager/reference/rest/v1/projects/get#authorization-scopes

Args:
request (google.auth.transport.Request): A callable used to make
HTTP requests.
Returns:
Optional[str]: The project ID corresponding to the workload identity pool
if determinable.
"""
if self._project_id:
# If already retrieved, return the cached project ID value.
return self._project_id
if self.project_number:
headers = {}
url = _CLOUD_RESOURCE_MANAGER + self.project_number
self.before_request(request, "GET", url, headers)
response = request(url=url, method="GET", headers=headers)

response_body = (
response.data.decode("utf-8")
if hasattr(response.data, "decode")
else response.data
)
response_data = json.loads(response_body)

if response.status == 200:
# Cache result as this field is immutable.
self._project_id = response_data.get("projectId")
return self._project_id

return None

@_helpers.copy_docstring(credentials.Credentials)
def refresh(self, request):
if self._impersonated_credentials:
Expand Down
1 change: 1 addition & 0 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ def docs(session):
@nox.session(python="pypy")
def pypy(session):
session.install(*TEST_DEPENDENCIES)
session.install(*ASYNC_DEPENDENCIES)
session.install(".")
session.run(
"pytest",
Expand Down
43 changes: 43 additions & 0 deletions tests/compute_engine/test__metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,49 @@ def test_get_success_text():
assert result == data


def test_get_success_params():
data = "foobar"
request = make_request(data, headers={"content-type": "text/plain"})
params = {"recursive": "true"}

result = _metadata.get(request, PATH, params=params)

request.assert_called_once_with(
method="GET",
url=_metadata._METADATA_ROOT + PATH + "?recursive=true",
headers=_metadata._METADATA_HEADERS,
)
assert result == data


def test_get_success_recursive_and_params():
data = "foobar"
request = make_request(data, headers={"content-type": "text/plain"})
params = {"recursive": "false"}
result = _metadata.get(request, PATH, recursive=True, params=params)

request.assert_called_once_with(
method="GET",
url=_metadata._METADATA_ROOT + PATH + "?recursive=true",
headers=_metadata._METADATA_HEADERS,
)
assert result == data


def test_get_success_recursive():
data = "foobar"
request = make_request(data, headers={"content-type": "text/plain"})

result = _metadata.get(request, PATH, recursive=True)

request.assert_called_once_with(
method="GET",
url=_metadata._METADATA_ROOT + PATH + "?recursive=true",
headers=_metadata._METADATA_HEADERS,
)
assert result == data


def test_get_success_custom_root_new_variable():
request = make_request("{}", headers={"content-type": "application/json"})

Expand Down
Loading