From d994a5a87491b47f02c00f7d61b412bf8c62412f Mon Sep 17 00:00:00 2001 From: Jeffrey Rennie Date: Tue, 21 Sep 2021 12:36:08 -0700 Subject: [PATCH 1/5] chore: relocate owl bot post processor (#869) chore: relocate owl bot post processor --- .github/.OwlBot.lock.yaml | 4 ++-- .github/.OwlBot.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index c07f148f0..2567653c0 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -1,3 +1,3 @@ docker: - image: gcr.io/repo-automation-bots/owlbot-python:latest - digest: sha256:0ffe3bdd6c7159692df5f7744da74e5ef19966288a6bf76023e8e04e0c424d7d + image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest + digest: sha256:87eee22d276554e4e52863ec9b1cb6a7245815dfae20439712bf644348215a5a diff --git a/.github/.OwlBot.yaml b/.github/.OwlBot.yaml index 892fbc291..ed6155aab 100644 --- a/.github/.OwlBot.yaml +++ b/.github/.OwlBot.yaml @@ -13,6 +13,6 @@ # limitations under the License. docker: - image: gcr.io/repo-automation-bots/owlbot-python:latest + image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest begin-after-commit-hash: ee56c3493ec6aeb237ff515ecea949710944a20f From afd05a6629be4e4b7a0c079209db68e1ba53e0d6 Mon Sep 17 00:00:00 2001 From: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com> Date: Tue, 21 Sep 2021 14:42:13 -0600 Subject: [PATCH 2/5] chore: update token (#870) --- system_tests/secrets.tar.enc | Bin 10323 -> 10323 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc index a44427604eb1d47c629fc0eff4e5869f7b68e861..ed178d8bd92b0f5633c3447df729de58d65408a9 100644 GIT binary patch literal 10323 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 From 993bab2aaacf3034e09d9f0f25d36c0e815d3a29 Mon Sep 17 00:00:00 2001 From: bojeil-google Date: Tue, 21 Sep 2021 14:00:15 -0700 Subject: [PATCH 3/5] feat: add support for workforce pool credentials (#868) Workforce pools (external account credentials for non-Google users) are organization-level resources which means that issued workforce pool tokens will not have any client project ID on token exchange as currently designed. "To use a Google API, the client must identify the application to the server. If the API requires authentication, the client must also identify the principal running the application." The application here is the client project. The token will identify the user principal but not the application. This will result in APIs rejecting requests authenticated with these tokens. Note that passing a `x-goog-user-project` override header on API request is still not sufficient. The token is still expected to have a client project. As a result, we have extended the spec to support an additional `workforce_pool_user_project` for these credentials (workforce pools) which will be passed when exchanging an external token for a Google Access token. After the exchange, the issued access token will use the supplied project as the client project. The underlying principal must still have `serviceusage.services.use` IAM permission to use the project for billing/quota. This field is not needed for flows with basic client authentication (e.g. client ID is supplied). The client ID is sufficient to determine the client project and any additionally supplied `workforce_pool_user_project` value will be ignored. Note that this feature is not usable yet publicly. The additional field has been added to the abstract external account credentials `google.auth.external_account.Credentials` and the subclass `google.auth.identity_pool.Credentials`. --- google/auth/external_account.py | 67 ++++- google/auth/identity_pool.py | 8 + tests/test_external_account.py | 477 ++++++++++++++++++++++++++++++-- tests/test_identity_pool.py | 213 +++++++++++++- 4 files changed, 723 insertions(+), 42 deletions(-) diff --git a/google/auth/external_account.py b/google/auth/external_account.py index 24b93b423..f588981a0 100644 --- a/google/auth/external_account.py +++ b/google/auth/external_account.py @@ -73,6 +73,7 @@ def __init__( quota_project_id=None, scopes=None, default_scopes=None, + workforce_pool_user_project=None, ): """Instantiates an external account credentials object. @@ -90,6 +91,11 @@ def __init__( authorization grant. default_scopes (Optional[Sequence[str]]): Default scopes passed by a Google client library. Use 'scopes' for user-defined scopes. + workforce_pool_user_project (Optona[str]): The optional workforce pool user + project number when the credential corresponds to a workforce pool and not + a workload identity pool. The underlying principal must still have + serviceusage.services.use IAM permission to use the project for + billing/quota. Raises: google.auth.exceptions.RefreshError: If the generateAccessToken endpoint returned an error. @@ -105,6 +111,7 @@ def __init__( self._quota_project_id = quota_project_id self._scopes = scopes self._default_scopes = default_scopes + self._workforce_pool_user_project = workforce_pool_user_project if self._client_id: self._client_auth = utils.ClientAuthentication( @@ -120,6 +127,13 @@ def __init__( self._impersonated_credentials = None self._project_id = None + if not self.is_workforce_pool and self._workforce_pool_user_project: + # Workload identity pools do not support workforce pool user projects. + raise ValueError( + "workforce_pool_user_project should not be set for non-workforce pool " + "credentials" + ) + @property def info(self): """Generates the dictionary representation of the current credentials. @@ -140,6 +154,7 @@ def info(self): "quota_project_id": self._quota_project_id, "client_id": self._client_id, "client_secret": self._client_secret, + "workforce_pool_user_project": self._workforce_pool_user_project, } return {key: value for key, value in config_info.items() if value is not None} @@ -178,12 +193,23 @@ def is_user(self): # service account. if self._service_account_impersonation_url: return False + return self.is_workforce_pool + + @property + def is_workforce_pool(self): + """Returns whether the credentials represent a workforce pool (True) or + workload (False) based on the credentials' audience. + + This will also return True for impersonated workforce pool credentials. + + Returns: + bool: True if the credentials represent a workforce pool. False if they + represent a workload. + """ # Workforce pools representing users have the following audience format: # //iam.googleapis.com/locations/$location/workforcePools/$poolId/providers/$providerId p = re.compile(r"//iam\.googleapis\.com/locations/[^/]+/workforcePools/") - if p.match(self._audience): - return True - return False + return p.match(self._audience or "") is not None @property def requires_scopes(self): @@ -210,7 +236,7 @@ def project_number(self): @_helpers.copy_docstring(credentials.Scoped) def with_scopes(self, scopes, default_scopes=None): - return self.__class__( + d = dict( audience=self._audience, subject_token_type=self._subject_token_type, token_url=self._token_url, @@ -221,7 +247,11 @@ def with_scopes(self, scopes, default_scopes=None): quota_project_id=self._quota_project_id, scopes=scopes, default_scopes=default_scopes, + workforce_pool_user_project=self._workforce_pool_user_project, ) + if not self.is_workforce_pool: + d.pop("workforce_pool_user_project") + return self.__class__(**d) @abc.abstractmethod def retrieve_subject_token(self, request): @@ -238,7 +268,9 @@ def retrieve_subject_token(self, request): raise NotImplementedError("retrieve_subject_token must be implemented") def get_project_id(self, request): - """Retrieves the project ID corresponding to the workload identity pool. + """Retrieves the project ID corresponding to the workload identity or workforce pool. + For workforce pool credentials, it returns the project ID corresponding to + the workforce_pool_user_project. When not determinable, None is returned. @@ -255,16 +287,17 @@ def get_project_id(self, request): HTTP requests. Returns: Optional[str]: The project ID corresponding to the workload identity pool - if determinable. + or workforce pool if determinable. """ if self._project_id: # If already retrieved, return the cached project ID value. return self._project_id scopes = self._scopes if self._scopes is not None else self._default_scopes # Scopes are required in order to retrieve a valid access token. - if self.project_number and scopes: + project_number = self.project_number or self._workforce_pool_user_project + if project_number and scopes: headers = {} - url = _CLOUD_RESOURCE_MANAGER + self.project_number + url = _CLOUD_RESOURCE_MANAGER + project_number self.before_request(request, "GET", url, headers) response = request(url=url, method="GET", headers=headers) @@ -291,6 +324,11 @@ def refresh(self, request): self.expiry = self._impersonated_credentials.expiry else: now = _helpers.utcnow() + additional_options = None + # Do not pass workforce_pool_user_project when client authentication + # is used. The client ID is sufficient for determining the user project. + if self._workforce_pool_user_project and not self._client_id: + additional_options = {"userProject": self._workforce_pool_user_project} response_data = self._sts_client.exchange_token( request=request, grant_type=_STS_GRANT_TYPE, @@ -299,6 +337,7 @@ def refresh(self, request): audience=self._audience, scopes=scopes, requested_token_type=_STS_REQUESTED_TOKEN_TYPE, + additional_options=additional_options, ) self.token = response_data.get("access_token") lifetime = datetime.timedelta(seconds=response_data.get("expires_in")) @@ -307,7 +346,7 @@ def refresh(self, request): @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject) def with_quota_project(self, quota_project_id): # Return copy of instance with the provided quota project ID. - return self.__class__( + d = dict( audience=self._audience, subject_token_type=self._subject_token_type, token_url=self._token_url, @@ -318,7 +357,11 @@ def with_quota_project(self, quota_project_id): quota_project_id=quota_project_id, scopes=self._scopes, default_scopes=self._default_scopes, + workforce_pool_user_project=self._workforce_pool_user_project, ) + if not self.is_workforce_pool: + d.pop("workforce_pool_user_project") + return self.__class__(**d) def _initialize_impersonated_credentials(self): """Generates an impersonated credentials. @@ -336,7 +379,7 @@ def _initialize_impersonated_credentials(self): endpoint returned an error. """ # Return copy of instance with no service account impersonation. - source_credentials = self.__class__( + d = dict( audience=self._audience, subject_token_type=self._subject_token_type, token_url=self._token_url, @@ -347,7 +390,11 @@ def _initialize_impersonated_credentials(self): quota_project_id=self._quota_project_id, scopes=self._scopes, default_scopes=self._default_scopes, + workforce_pool_user_project=self._workforce_pool_user_project, ) + if not self.is_workforce_pool: + d.pop("workforce_pool_user_project") + source_credentials = self.__class__(**d) # Determine target_principal. target_principal = self.service_account_email diff --git a/google/auth/identity_pool.py b/google/auth/identity_pool.py index c331e0921..901fd62fb 100644 --- a/google/auth/identity_pool.py +++ b/google/auth/identity_pool.py @@ -58,6 +58,7 @@ def __init__( quota_project_id=None, scopes=None, default_scopes=None, + workforce_pool_user_project=None, ): """Instantiates an external account credentials object from a file/URL. @@ -95,6 +96,11 @@ def __init__( authorization grant. default_scopes (Optional[Sequence[str]]): Default scopes passed by a Google client library. Use 'scopes' for user-defined scopes. + workforce_pool_user_project (Optona[str]): The optional workforce pool user + project number when the credential corresponds to a workforce pool and not + a workload identity pool. The underlying principal must still have + serviceusage.services.use IAM permission to use the project for + billing/quota. Raises: google.auth.exceptions.RefreshError: If an error is encountered during @@ -117,6 +123,7 @@ def __init__( quota_project_id=quota_project_id, scopes=scopes, default_scopes=default_scopes, + workforce_pool_user_project=workforce_pool_user_project, ) if not isinstance(credential_source, Mapping): self._credential_source_file = None @@ -255,6 +262,7 @@ def from_info(cls, info, **kwargs): client_secret=info.get("client_secret"), credential_source=info.get("credential_source"), quota_project_id=info.get("quota_project_id"), + workforce_pool_user_project=info.get("workforce_pool_user_project"), **kwargs ) diff --git a/tests/test_external_account.py b/tests/test_external_account.py index df6174f17..97f1564ef 100644 --- a/tests/test_external_account.py +++ b/tests/test_external_account.py @@ -37,6 +37,33 @@ "//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id", "//iam.googleapis.com/locations/eu/workforcePools/workloadIdentityPools/providers/provider-id", ] +# Workload identity pool audiences or invalid workforce pool audiences. +TEST_NON_USER_AUDIENCES = [ + # Legacy K8s audience format. + "identitynamespace:1f12345:my_provider", + ( + "//iam.googleapis.com/projects/123456/locations/" + "global/workloadIdentityPools/pool-id/providers/" + "provider-id" + ), + ( + "//iam.googleapis.com/projects/123456/locations/" + "eu/workloadIdentityPools/pool-id/providers/" + "provider-id" + ), + # Pool ID with workforcePools string. + ( + "//iam.googleapis.com/projects/123456/locations/" + "global/workloadIdentityPools/workforcePools/providers/" + "provider-id" + ), + # Unrealistic / incorrect workforce pool audiences. + "//iamgoogleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id", + "//iam.googleapiscom/locations/eu/workforcePools/pool-id/providers/provider-id", + "//iam.googleapis.com/locations/workforcePools/pool-id/providers/provider-id", + "//iam.googleapis.com/locations/eu/workforcePool/pool-id/providers/provider-id", + "//iam.googleapis.com/locations//workforcePool/pool-id/providers/provider-id", +] class CredentialsImpl(external_account.Credentials): @@ -52,6 +79,7 @@ def __init__( quota_project_id=None, scopes=None, default_scopes=None, + workforce_pool_user_project=None, ): super(CredentialsImpl, self).__init__( audience=audience, @@ -64,6 +92,7 @@ def __init__( quota_project_id=quota_project_id, scopes=scopes, default_scopes=default_scopes, + workforce_pool_user_project=workforce_pool_user_project, ) self._counter = 0 @@ -83,7 +112,12 @@ class TestCredentials(object): "/locations/global/workloadIdentityPools/{}" "/providers/{}" ).format(PROJECT_NUMBER, POOL_ID, PROVIDER_ID) + WORKFORCE_AUDIENCE = ( + "//iam.googleapis.com/locations/global/workforcePools/{}/providers/{}" + ).format(POOL_ID, PROVIDER_ID) + WORKFORCE_POOL_USER_PROJECT = "WORKFORCE_POOL_USER_PROJECT_NUMBER" SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:jwt" + WORKFORCE_SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:id_token" CREDENTIAL_SOURCE = {"file": "/var/run/secrets/goog.id/token"} SUCCESS_RESPONSE = { "access_token": "ACCESS_TOKEN", @@ -146,6 +180,31 @@ def make_credentials( default_scopes=default_scopes, ) + @classmethod + def make_workforce_pool_credentials( + cls, + client_id=None, + client_secret=None, + quota_project_id=None, + scopes=None, + default_scopes=None, + service_account_impersonation_url=None, + workforce_pool_user_project=None, + ): + return CredentialsImpl( + audience=cls.WORKFORCE_AUDIENCE, + subject_token_type=cls.WORKFORCE_SUBJECT_TOKEN_TYPE, + token_url=cls.TOKEN_URL, + service_account_impersonation_url=service_account_impersonation_url, + credential_source=cls.CREDENTIAL_SOURCE, + client_id=client_id, + client_secret=client_secret, + quota_project_id=quota_project_id, + scopes=scopes, + default_scopes=default_scopes, + workforce_pool_user_project=workforce_pool_user_project, + ) + @classmethod def make_mock_request( cls, @@ -230,6 +289,21 @@ def test_default_state(self): assert credentials.requires_scopes assert not credentials.quota_project_id + def test_nonworkforce_with_workforce_pool_user_project(self): + with pytest.raises(ValueError) as excinfo: + CredentialsImpl( + audience=self.AUDIENCE, + subject_token_type=self.SUBJECT_TOKEN_TYPE, + token_url=self.TOKEN_URL, + credential_source=self.CREDENTIAL_SOURCE, + workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT, + ) + + assert excinfo.match( + "workforce_pool_user_project should not be set for non-workforce " + "pool credentials" + ) + def test_with_scopes(self): credentials = self.make_credentials() @@ -241,6 +315,23 @@ def test_with_scopes(self): assert scoped_credentials.has_scopes(["email"]) assert not scoped_credentials.requires_scopes + def test_with_scopes_workforce_pool(self): + credentials = self.make_workforce_pool_credentials( + workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT + ) + + assert not credentials.scopes + assert credentials.requires_scopes + + scoped_credentials = credentials.with_scopes(["email"]) + + assert scoped_credentials.has_scopes(["email"]) + assert not scoped_credentials.requires_scopes + assert ( + scoped_credentials.info.get("workforce_pool_user_project") + == self.WORKFORCE_POOL_USER_PROJECT + ) + def test_with_scopes_using_user_and_default_scopes(self): credentials = self.make_credentials() @@ -296,6 +387,7 @@ def test_with_scopes_full_options_propagated(self): quota_project_id=self.QUOTA_PROJECT_ID, scopes=["email"], default_scopes=["default2"], + workforce_pool_user_project=None, ) def test_with_quota_project(self): @@ -308,6 +400,22 @@ def test_with_quota_project(self): assert quota_project_creds.quota_project_id == "project-foo" + def test_with_quota_project_workforce_pool(self): + credentials = self.make_workforce_pool_credentials( + workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT + ) + + assert not credentials.scopes + assert not credentials.quota_project_id + + quota_project_creds = credentials.with_quota_project("project-foo") + + assert quota_project_creds.quota_project_id == "project-foo" + assert ( + quota_project_creds.info.get("workforce_pool_user_project") + == self.WORKFORCE_POOL_USER_PROJECT + ) + def test_with_quota_project_full_options_propagated(self): credentials = self.make_credentials( client_id=CLIENT_ID, @@ -336,6 +444,7 @@ def test_with_quota_project_full_options_propagated(self): quota_project_id="project-foo", scopes=self.SCOPES, default_scopes=["default1"], + workforce_pool_user_project=None, ) def test_with_invalid_impersonation_target_principal(self): @@ -359,6 +468,20 @@ def test_info(self): "credential_source": self.CREDENTIAL_SOURCE.copy(), } + def test_info_workforce_pool(self): + credentials = self.make_workforce_pool_credentials( + workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT + ) + + assert credentials.info == { + "type": "external_account", + "audience": self.WORKFORCE_AUDIENCE, + "subject_token_type": self.WORKFORCE_SUBJECT_TOKEN_TYPE, + "token_url": self.TOKEN_URL, + "credential_source": self.CREDENTIAL_SOURCE.copy(), + "workforce_pool_user_project": self.WORKFORCE_POOL_USER_PROJECT, + } + def test_info_with_full_options(self): credentials = self.make_credentials( client_id=CLIENT_ID, @@ -391,36 +514,7 @@ def test_service_account_email_with_impersonation(self): assert credentials.service_account_email == SERVICE_ACCOUNT_EMAIL - @pytest.mark.parametrize( - "audience", - # Workload identity pool audiences or invalid workforce pool audiences. - [ - # Legacy K8s audience format. - "identitynamespace:1f12345:my_provider", - ( - "//iam.googleapis.com/projects/123456/locations/" - "global/workloadIdentityPools/pool-id/providers/" - "provider-id" - ), - ( - "//iam.googleapis.com/projects/123456/locations/" - "eu/workloadIdentityPools/pool-id/providers/" - "provider-id" - ), - # Pool ID with workforcePools string. - ( - "//iam.googleapis.com/projects/123456/locations/" - "global/workloadIdentityPools/workforcePools/providers/" - "provider-id" - ), - # Unrealistic / incorrect workforce pool audiences. - "//iamgoogleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id", - "//iam.googleapiscom/locations/eu/workforcePools/pool-id/providers/provider-id", - "//iam.googleapis.com/locations/workforcePools/pool-id/providers/provider-id", - "//iam.googleapis.com/locations/eu/workforcePool/pool-id/providers/provider-id", - "//iam.googleapis.com/locations//workforcePool/pool-id/providers/provider-id", - ], - ) + @pytest.mark.parametrize("audience", TEST_NON_USER_AUDIENCES) def test_is_user_with_non_users(self, audience): credentials = CredentialsImpl( audience=audience, @@ -458,6 +552,43 @@ def test_is_user_with_users_and_impersonation(self, audience): # not a user. assert credentials.is_user is False + @pytest.mark.parametrize("audience", TEST_NON_USER_AUDIENCES) + def test_is_workforce_pool_with_non_users(self, audience): + credentials = CredentialsImpl( + audience=audience, + subject_token_type=self.SUBJECT_TOKEN_TYPE, + token_url=self.TOKEN_URL, + credential_source=self.CREDENTIAL_SOURCE, + ) + + assert credentials.is_workforce_pool is False + + @pytest.mark.parametrize("audience", TEST_USER_AUDIENCES) + def test_is_workforce_pool_with_users(self, audience): + credentials = CredentialsImpl( + audience=audience, + subject_token_type=self.SUBJECT_TOKEN_TYPE, + token_url=self.TOKEN_URL, + credential_source=self.CREDENTIAL_SOURCE, + ) + + assert credentials.is_workforce_pool is True + + @pytest.mark.parametrize("audience", TEST_USER_AUDIENCES) + def test_is_workforce_pool_with_users_and_impersonation(self, audience): + # Initialize the credentials with workforce audience and service account + # impersonation. + credentials = CredentialsImpl( + audience=audience, + subject_token_type=self.SUBJECT_TOKEN_TYPE, + token_url=self.TOKEN_URL, + credential_source=self.CREDENTIAL_SOURCE, + service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL, + ) + + # Even though impersonation is used, is_workforce_pool should still return True. + assert credentials.is_workforce_pool is True + @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) def test_refresh_without_client_auth_success(self, unused_utcnow): response = self.SUCCESS_RESPONSE.copy() @@ -485,6 +616,110 @@ def test_refresh_without_client_auth_success(self, unused_utcnow): assert not credentials.expired assert credentials.token == response["access_token"] + @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) + def test_refresh_workforce_without_client_auth_success(self, unused_utcnow): + response = self.SUCCESS_RESPONSE.copy() + # Test custom expiration to confirm expiry is set correctly. + response["expires_in"] = 2800 + expected_expiry = datetime.datetime.min + datetime.timedelta( + seconds=response["expires_in"] + ) + headers = {"Content-Type": "application/x-www-form-urlencoded"} + request_data = { + "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", + "audience": self.WORKFORCE_AUDIENCE, + "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", + "subject_token": "subject_token_0", + "subject_token_type": self.WORKFORCE_SUBJECT_TOKEN_TYPE, + "options": urllib.parse.quote( + json.dumps({"userProject": self.WORKFORCE_POOL_USER_PROJECT}) + ), + } + request = self.make_mock_request(status=http.client.OK, data=response) + credentials = self.make_workforce_pool_credentials( + workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT + ) + + credentials.refresh(request) + + self.assert_token_request_kwargs(request.call_args[1], headers, request_data) + assert credentials.valid + assert credentials.expiry == expected_expiry + assert not credentials.expired + assert credentials.token == response["access_token"] + + @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) + def test_refresh_workforce_with_client_auth_success(self, unused_utcnow): + response = self.SUCCESS_RESPONSE.copy() + # Test custom expiration to confirm expiry is set correctly. + response["expires_in"] = 2800 + expected_expiry = datetime.datetime.min + datetime.timedelta( + seconds=response["expires_in"] + ) + headers = { + "Content-Type": "application/x-www-form-urlencoded", + "Authorization": "Basic {}".format(BASIC_AUTH_ENCODING), + } + request_data = { + "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", + "audience": self.WORKFORCE_AUDIENCE, + "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", + "subject_token": "subject_token_0", + "subject_token_type": self.WORKFORCE_SUBJECT_TOKEN_TYPE, + } + request = self.make_mock_request(status=http.client.OK, data=response) + # Client Auth will have higher priority over workforce_pool_user_project. + credentials = self.make_workforce_pool_credentials( + client_id=CLIENT_ID, + client_secret=CLIENT_SECRET, + workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT, + ) + + credentials.refresh(request) + + self.assert_token_request_kwargs(request.call_args[1], headers, request_data) + assert credentials.valid + assert credentials.expiry == expected_expiry + assert not credentials.expired + assert credentials.token == response["access_token"] + + @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) + def test_refresh_workforce_with_client_auth_and_no_workforce_project_success( + self, unused_utcnow + ): + response = self.SUCCESS_RESPONSE.copy() + # Test custom expiration to confirm expiry is set correctly. + response["expires_in"] = 2800 + expected_expiry = datetime.datetime.min + datetime.timedelta( + seconds=response["expires_in"] + ) + headers = { + "Content-Type": "application/x-www-form-urlencoded", + "Authorization": "Basic {}".format(BASIC_AUTH_ENCODING), + } + request_data = { + "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", + "audience": self.WORKFORCE_AUDIENCE, + "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", + "subject_token": "subject_token_0", + "subject_token_type": self.WORKFORCE_SUBJECT_TOKEN_TYPE, + } + request = self.make_mock_request(status=http.client.OK, data=response) + # Client Auth will be sufficient for user project determination. + credentials = self.make_workforce_pool_credentials( + client_id=CLIENT_ID, + client_secret=CLIENT_SECRET, + workforce_pool_user_project=None, + ) + + credentials.refresh(request) + + self.assert_token_request_kwargs(request.call_args[1], headers, request_data) + assert credentials.valid + assert credentials.expiry == expected_expiry + assert not credentials.expired + assert credentials.token == response["access_token"] + def test_refresh_impersonation_without_client_auth_success(self): # Simulate service account access token expires in 2800 seconds. expire_time = ( @@ -549,6 +784,74 @@ def test_refresh_impersonation_without_client_auth_success(self): assert not credentials.expired assert credentials.token == impersonation_response["accessToken"] + def test_refresh_workforce_impersonation_without_client_auth_success(self): + # Simulate service account access token expires in 2800 seconds. + expire_time = ( + _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=2800) + ).isoformat("T") + "Z" + expected_expiry = datetime.datetime.strptime(expire_time, "%Y-%m-%dT%H:%M:%SZ") + # STS token exchange request/response. + token_response = self.SUCCESS_RESPONSE.copy() + token_headers = {"Content-Type": "application/x-www-form-urlencoded"} + token_request_data = { + "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", + "audience": self.WORKFORCE_AUDIENCE, + "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", + "subject_token": "subject_token_0", + "subject_token_type": self.WORKFORCE_SUBJECT_TOKEN_TYPE, + "scope": "https://www.googleapis.com/auth/iam", + "options": urllib.parse.quote( + json.dumps({"userProject": self.WORKFORCE_POOL_USER_PROJECT}) + ), + } + # Service account impersonation request/response. + impersonation_response = { + "accessToken": "SA_ACCESS_TOKEN", + "expireTime": expire_time, + } + impersonation_headers = { + "Content-Type": "application/json", + "authorization": "Bearer {}".format(token_response["access_token"]), + } + impersonation_request_data = { + "delegates": None, + "scope": self.SCOPES, + "lifetime": "3600s", + } + # Initialize mock request to handle token exchange and service account + # impersonation request. + request = self.make_mock_request( + status=http.client.OK, + data=token_response, + impersonation_status=http.client.OK, + impersonation_data=impersonation_response, + ) + # Initialize credentials with service account impersonation. + credentials = self.make_workforce_pool_credentials( + service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL, + scopes=self.SCOPES, + workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT, + ) + + credentials.refresh(request) + + # Only 2 requests should be processed. + assert len(request.call_args_list) == 2 + # Verify token exchange request parameters. + self.assert_token_request_kwargs( + request.call_args_list[0][1], token_headers, token_request_data + ) + # Verify service account impersonation request parameters. + self.assert_impersonation_request_kwargs( + request.call_args_list[1][1], + impersonation_headers, + impersonation_request_data, + ) + assert credentials.valid + assert credentials.expiry == expected_expiry + assert not credentials.expired + assert credentials.token == impersonation_response["accessToken"] + def test_refresh_without_client_auth_success_explicit_user_scopes_ignore_default_scopes( self, ): @@ -822,6 +1125,22 @@ def test_apply_without_quota_project_id(self): "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]) } + def test_apply_workforce_without_quota_project_id(self): + headers = {} + request = self.make_mock_request( + status=http.client.OK, data=self.SUCCESS_RESPONSE + ) + credentials = self.make_workforce_pool_credentials( + workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT + ) + + credentials.refresh(request) + credentials.apply(headers) + + assert headers == { + "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]) + } + def test_apply_impersonation_without_quota_project_id(self): expire_time = ( _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=3600) @@ -926,6 +1245,31 @@ def test_before_request(self): "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]), } + def test_before_request_workforce(self): + headers = {"other": "header-value"} + request = self.make_mock_request( + status=http.client.OK, data=self.SUCCESS_RESPONSE + ) + credentials = self.make_workforce_pool_credentials( + workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT + ) + + # First call should call refresh, setting the token. + credentials.before_request(request, "POST", "https://example.com/api", headers) + + assert headers == { + "other": "header-value", + "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]), + } + + # Second call shouldn't call refresh. + credentials.before_request(request, "POST", "https://example.com/api", headers) + + assert headers == { + "other": "header-value", + "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]), + } + def test_before_request_impersonation(self): expire_time = ( _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=3600) @@ -1091,6 +1435,17 @@ def test_project_number_determinable(self): assert credentials.project_number == self.PROJECT_NUMBER + def test_project_number_workforce(self): + credentials = CredentialsImpl( + audience=self.WORKFORCE_AUDIENCE, + subject_token_type=self.WORKFORCE_SUBJECT_TOKEN_TYPE, + token_url=self.TOKEN_URL, + credential_source=self.CREDENTIAL_SOURCE, + workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT, + ) + + assert credentials.project_number is None + def test_project_id_without_scopes(self): # Initialize credentials with no scopes. credentials = CredentialsImpl( @@ -1190,6 +1545,68 @@ def test_get_project_id_cloud_resource_manager_success(self): # No additional requests. assert len(request.call_args_list) == 3 + def test_workforce_pool_get_project_id_cloud_resource_manager_success(self): + # STS token exchange request/response. + token_headers = {"Content-Type": "application/x-www-form-urlencoded"} + token_request_data = { + "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", + "audience": self.WORKFORCE_AUDIENCE, + "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", + "subject_token": "subject_token_0", + "subject_token_type": self.WORKFORCE_SUBJECT_TOKEN_TYPE, + "scope": "scope1 scope2", + "options": urllib.parse.quote( + json.dumps({"userProject": self.WORKFORCE_POOL_USER_PROJECT}) + ), + } + # Initialize mock request to handle token exchange and cloud resource + # manager request. + request = self.make_mock_request( + status=http.client.OK, + data=self.SUCCESS_RESPONSE.copy(), + cloud_resource_manager_status=http.client.OK, + cloud_resource_manager_data=self.CLOUD_RESOURCE_MANAGER_SUCCESS_RESPONSE, + ) + credentials = self.make_workforce_pool_credentials( + scopes=self.SCOPES, + quota_project_id=self.QUOTA_PROJECT_ID, + workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT, + ) + + # Expected project ID from cloud resource manager response should be returned. + project_id = credentials.get_project_id(request) + + assert project_id == self.PROJECT_ID + # 2 requests should be processed. + assert len(request.call_args_list) == 2 + # Verify token exchange request parameters. + self.assert_token_request_kwargs( + request.call_args_list[0][1], token_headers, token_request_data + ) + # In the process of getting project ID, an access token should be + # retrieved. + assert credentials.valid + assert not credentials.expired + assert credentials.token == self.SUCCESS_RESPONSE["access_token"] + # Verify cloud resource manager request parameters. + self.assert_resource_manager_request_kwargs( + request.call_args_list[1][1], + self.WORKFORCE_POOL_USER_PROJECT, + { + "x-goog-user-project": self.QUOTA_PROJECT_ID, + "authorization": "Bearer {}".format( + self.SUCCESS_RESPONSE["access_token"] + ), + }, + ) + + # Calling get_project_id again should return the cached project_id. + project_id = credentials.get_project_id(request) + + assert project_id == self.PROJECT_ID + # No additional requests. + assert len(request.call_args_list) == 2 + def test_get_project_id_cloud_resource_manager_error(self): # Simulate resource doesn't have sufficient permissions to access # cloud resource manager. diff --git a/tests/test_identity_pool.py b/tests/test_identity_pool.py index efe11b082..e90e2880d 100644 --- a/tests/test_identity_pool.py +++ b/tests/test_identity_pool.py @@ -53,6 +53,11 @@ TOKEN_URL = "https://sts.googleapis.com/v1/token" SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:jwt" AUDIENCE = "//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID" +WORKFORCE_AUDIENCE = ( + "//iam.googleapis.com/locations/global/workforcePools/POOL_ID/providers/PROVIDER_ID" +) +WORKFORCE_SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:id_token" +WORKFORCE_POOL_USER_PROJECT = "WORKFORCE_POOL_USER_PROJECT_NUMBER" class TestCredentials(object): @@ -158,6 +163,7 @@ def assert_underlying_credentials_refresh( credential_data=None, scopes=None, default_scopes=None, + workforce_pool_user_project=None, ): """Utility to assert that a credentials are initialized with the expected attributes by calling refresh functionality and confirming response matches @@ -183,6 +189,10 @@ def assert_underlying_credentials_refresh( "subject_token": subject_token, "subject_token_type": subject_token_type, } + if workforce_pool_user_project: + token_request_data["options"] = urllib.parse.quote( + json.dumps({"userProject": workforce_pool_user_project}) + ) if service_account_impersonation_url: # Service account impersonation request/response. @@ -250,6 +260,8 @@ def assert_underlying_credentials_refresh( @classmethod def make_credentials( cls, + audience=AUDIENCE, + subject_token_type=SUBJECT_TOKEN_TYPE, client_id=None, client_secret=None, quota_project_id=None, @@ -257,10 +269,11 @@ def make_credentials( default_scopes=None, service_account_impersonation_url=None, credential_source=None, + workforce_pool_user_project=None, ): return identity_pool.Credentials( - audience=AUDIENCE, - subject_token_type=SUBJECT_TOKEN_TYPE, + audience=audience, + subject_token_type=subject_token_type, token_url=TOKEN_URL, service_account_impersonation_url=service_account_impersonation_url, credential_source=credential_source, @@ -269,6 +282,7 @@ def make_credentials( quota_project_id=quota_project_id, scopes=scopes, default_scopes=default_scopes, + workforce_pool_user_project=workforce_pool_user_project, ) @mock.patch.object(identity_pool.Credentials, "__init__", return_value=None) @@ -297,6 +311,7 @@ def test_from_info_full_options(self, mock_init): client_secret=CLIENT_SECRET, credential_source=self.CREDENTIAL_SOURCE_TEXT, quota_project_id=QUOTA_PROJECT_ID, + workforce_pool_user_project=None, ) @mock.patch.object(identity_pool.Credentials, "__init__", return_value=None) @@ -321,6 +336,33 @@ def test_from_info_required_options_only(self, mock_init): client_secret=None, credential_source=self.CREDENTIAL_SOURCE_TEXT, quota_project_id=None, + workforce_pool_user_project=None, + ) + + @mock.patch.object(identity_pool.Credentials, "__init__", return_value=None) + def test_from_info_workforce_pool(self, mock_init): + credentials = identity_pool.Credentials.from_info( + { + "audience": WORKFORCE_AUDIENCE, + "subject_token_type": WORKFORCE_SUBJECT_TOKEN_TYPE, + "token_url": TOKEN_URL, + "credential_source": self.CREDENTIAL_SOURCE_TEXT, + "workforce_pool_user_project": WORKFORCE_POOL_USER_PROJECT, + } + ) + + # Confirm identity_pool.Credentials instantiated with expected attributes. + assert isinstance(credentials, identity_pool.Credentials) + mock_init.assert_called_once_with( + audience=WORKFORCE_AUDIENCE, + subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE, + token_url=TOKEN_URL, + service_account_impersonation_url=None, + client_id=None, + client_secret=None, + credential_source=self.CREDENTIAL_SOURCE_TEXT, + quota_project_id=None, + workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT, ) @mock.patch.object(identity_pool.Credentials, "__init__", return_value=None) @@ -350,6 +392,7 @@ def test_from_file_full_options(self, mock_init, tmpdir): client_secret=CLIENT_SECRET, credential_source=self.CREDENTIAL_SOURCE_TEXT, quota_project_id=QUOTA_PROJECT_ID, + workforce_pool_user_project=None, ) @mock.patch.object(identity_pool.Credentials, "__init__", return_value=None) @@ -375,6 +418,46 @@ def test_from_file_required_options_only(self, mock_init, tmpdir): client_secret=None, credential_source=self.CREDENTIAL_SOURCE_TEXT, quota_project_id=None, + workforce_pool_user_project=None, + ) + + @mock.patch.object(identity_pool.Credentials, "__init__", return_value=None) + def test_from_file_workforce_pool(self, mock_init, tmpdir): + info = { + "audience": WORKFORCE_AUDIENCE, + "subject_token_type": WORKFORCE_SUBJECT_TOKEN_TYPE, + "token_url": TOKEN_URL, + "credential_source": self.CREDENTIAL_SOURCE_TEXT, + "workforce_pool_user_project": WORKFORCE_POOL_USER_PROJECT, + } + config_file = tmpdir.join("config.json") + config_file.write(json.dumps(info)) + credentials = identity_pool.Credentials.from_file(str(config_file)) + + # Confirm identity_pool.Credentials instantiated with expected attributes. + assert isinstance(credentials, identity_pool.Credentials) + mock_init.assert_called_once_with( + audience=WORKFORCE_AUDIENCE, + subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE, + token_url=TOKEN_URL, + service_account_impersonation_url=None, + client_id=None, + client_secret=None, + credential_source=self.CREDENTIAL_SOURCE_TEXT, + quota_project_id=None, + workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT, + ) + + def test_constructor_nonworkforce_with_workforce_pool_user_project(self): + with pytest.raises(ValueError) as excinfo: + self.make_credentials( + audience=AUDIENCE, + workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT, + ) + + assert excinfo.match( + "workforce_pool_user_project should not be set for non-workforce " + "pool credentials" ) def test_constructor_invalid_options(self): @@ -430,6 +513,23 @@ def test_constructor_missing_subject_token_field_name(self): r"Missing subject_token_field_name for JSON credential_source format" ) + def test_info_with_workforce_pool_user_project(self): + credentials = self.make_credentials( + audience=WORKFORCE_AUDIENCE, + subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE, + credential_source=self.CREDENTIAL_SOURCE_TEXT_URL.copy(), + workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT, + ) + + assert credentials.info == { + "type": "external_account", + "audience": WORKFORCE_AUDIENCE, + "subject_token_type": WORKFORCE_SUBJECT_TOKEN_TYPE, + "token_url": TOKEN_URL, + "credential_source": self.CREDENTIAL_SOURCE_TEXT_URL, + "workforce_pool_user_project": WORKFORCE_POOL_USER_PROJECT, + } + def test_info_with_file_credential_source(self): credentials = self.make_credentials( credential_source=self.CREDENTIAL_SOURCE_TEXT_URL.copy() @@ -557,6 +657,115 @@ def test_refresh_text_file_success_without_impersonation_ignore_default_scopes( default_scopes=["ignored"], ) + def test_refresh_workforce_success_with_client_auth_without_impersonation(self): + credentials = self.make_credentials( + audience=WORKFORCE_AUDIENCE, + subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE, + client_id=CLIENT_ID, + client_secret=CLIENT_SECRET, + # Test with text format type. + credential_source=self.CREDENTIAL_SOURCE_TEXT, + scopes=SCOPES, + # This will be ignored in favor of client auth. + workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT, + ) + + self.assert_underlying_credentials_refresh( + credentials=credentials, + audience=WORKFORCE_AUDIENCE, + subject_token=TEXT_FILE_SUBJECT_TOKEN, + subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE, + token_url=TOKEN_URL, + service_account_impersonation_url=None, + basic_auth_encoding=BASIC_AUTH_ENCODING, + quota_project_id=None, + used_scopes=SCOPES, + scopes=SCOPES, + workforce_pool_user_project=None, + ) + + def test_refresh_workforce_success_with_client_auth_and_no_workforce_project(self): + credentials = self.make_credentials( + audience=WORKFORCE_AUDIENCE, + subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE, + client_id=CLIENT_ID, + client_secret=CLIENT_SECRET, + # Test with text format type. + credential_source=self.CREDENTIAL_SOURCE_TEXT, + scopes=SCOPES, + # This is not needed when client Auth is used. + workforce_pool_user_project=None, + ) + + self.assert_underlying_credentials_refresh( + credentials=credentials, + audience=WORKFORCE_AUDIENCE, + subject_token=TEXT_FILE_SUBJECT_TOKEN, + subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE, + token_url=TOKEN_URL, + service_account_impersonation_url=None, + basic_auth_encoding=BASIC_AUTH_ENCODING, + quota_project_id=None, + used_scopes=SCOPES, + scopes=SCOPES, + workforce_pool_user_project=None, + ) + + def test_refresh_workforce_success_without_client_auth_without_impersonation(self): + credentials = self.make_credentials( + audience=WORKFORCE_AUDIENCE, + subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE, + client_id=None, + client_secret=None, + # Test with text format type. + credential_source=self.CREDENTIAL_SOURCE_TEXT, + scopes=SCOPES, + # This will not be ignored as client auth is not used. + workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT, + ) + + self.assert_underlying_credentials_refresh( + credentials=credentials, + audience=WORKFORCE_AUDIENCE, + subject_token=TEXT_FILE_SUBJECT_TOKEN, + subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE, + token_url=TOKEN_URL, + service_account_impersonation_url=None, + basic_auth_encoding=None, + quota_project_id=None, + used_scopes=SCOPES, + scopes=SCOPES, + workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT, + ) + + def test_refresh_workforce_success_without_client_auth_with_impersonation(self): + credentials = self.make_credentials( + audience=WORKFORCE_AUDIENCE, + subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE, + client_id=None, + client_secret=None, + service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, + # Test with text format type. + credential_source=self.CREDENTIAL_SOURCE_TEXT, + scopes=SCOPES, + # This will not be ignored as client auth is not used. + workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT, + ) + + self.assert_underlying_credentials_refresh( + credentials=credentials, + audience=WORKFORCE_AUDIENCE, + subject_token=TEXT_FILE_SUBJECT_TOKEN, + subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE, + token_url=TOKEN_URL, + service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, + basic_auth_encoding=None, + quota_project_id=None, + used_scopes=SCOPES, + scopes=SCOPES, + workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT, + ) + def test_refresh_text_file_success_without_impersonation_use_default_scopes(self): credentials = self.make_credentials( client_id=CLIENT_ID, From 435be09af46fb70c3654d6b410b823c85de7dd70 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 21 Sep 2021 17:14:29 -0400 Subject: [PATCH 4/5] chore: remove 'six' (#871) --- tests/test_downscoped.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/test_downscoped.py b/tests/test_downscoped.py index 9ca95f5aa..a686391c0 100644 --- a/tests/test_downscoped.py +++ b/tests/test_downscoped.py @@ -13,12 +13,12 @@ # limitations under the License. import datetime +import http.client import json +import urllib import mock import pytest -from six.moves import http_client -from six.moves import urllib from google.auth import _helpers from google.auth import credentials @@ -461,7 +461,7 @@ def make_credentials(source_credentials=SourceCredentials(), quota_project_id=No ) @staticmethod - def make_mock_request(data, status=http_client.OK): + def make_mock_request(data, status=http.client.OK): response = mock.create_autospec(transport.Response, instance=True) response.status = status response.data = json.dumps(data).encode("utf-8") @@ -521,7 +521,7 @@ def test_refresh(self, unused_utcnow): "requested_token_type": REQUESTED_TOKEN_TYPE, "options": urllib.parse.quote(json.dumps(CREDENTIAL_ACCESS_BOUNDARY_JSON)), } - request = self.make_mock_request(status=http_client.OK, data=response) + request = self.make_mock_request(status=http.client.OK, data=response) source_credentials = SourceCredentials() credentials = self.make_credentials(source_credentials=source_credentials) @@ -563,7 +563,7 @@ def test_refresh_without_response_expires_in(self, unused_utcnow): "requested_token_type": REQUESTED_TOKEN_TYPE, "options": urllib.parse.quote(json.dumps(CREDENTIAL_ACCESS_BOUNDARY_JSON)), } - request = self.make_mock_request(status=http_client.OK, data=response) + request = self.make_mock_request(status=http.client.OK, data=response) credentials = self.make_credentials(source_credentials=source_credentials) # Spy on calls to source credentials refresh to confirm the expected request @@ -583,7 +583,7 @@ def test_refresh_without_response_expires_in(self, unused_utcnow): def test_refresh_token_exchange_error(self): request = self.make_mock_request( - status=http_client.BAD_REQUEST, data=ERROR_RESPONSE + status=http.client.BAD_REQUEST, data=ERROR_RESPONSE ) credentials = self.make_credentials() @@ -612,7 +612,7 @@ def test_refresh_source_credentials_refresh_error(self): def test_apply_without_quota_project_id(self): headers = {} - request = self.make_mock_request(status=http_client.OK, data=SUCCESS_RESPONSE) + request = self.make_mock_request(status=http.client.OK, data=SUCCESS_RESPONSE) credentials = self.make_credentials() credentials.refresh(request) @@ -624,7 +624,7 @@ def test_apply_without_quota_project_id(self): def test_apply_with_quota_project_id(self): headers = {"other": "header-value"} - request = self.make_mock_request(status=http_client.OK, data=SUCCESS_RESPONSE) + request = self.make_mock_request(status=http.client.OK, data=SUCCESS_RESPONSE) credentials = self.make_credentials(quota_project_id=QUOTA_PROJECT_ID) credentials.refresh(request) @@ -638,7 +638,7 @@ def test_apply_with_quota_project_id(self): def test_before_request(self): headers = {"other": "header-value"} - request = self.make_mock_request(status=http_client.OK, data=SUCCESS_RESPONSE) + request = self.make_mock_request(status=http.client.OK, data=SUCCESS_RESPONSE) credentials = self.make_credentials() # First call should call refresh, setting the token. @@ -662,7 +662,7 @@ def test_before_request(self): @mock.patch("google.auth._helpers.utcnow") def test_before_request_expired(self, utcnow): headers = {} - request = self.make_mock_request(status=http_client.OK, data=SUCCESS_RESPONSE) + request = self.make_mock_request(status=http.client.OK, data=SUCCESS_RESPONSE) credentials = self.make_credentials() credentials.token = "token" utcnow.return_value = datetime.datetime.min From a53bd0cf4d30e738dd6bf17067a0759eedce093f Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 27 Sep 2021 16:02:19 +0000 Subject: [PATCH 5/5] chore: release 2.2.0 (#872) :robot: I have created a release \*beep\* \*boop\* --- ## [2.2.0](https://www.github.com/googleapis/google-auth-library-python/compare/v2.1.0...v2.2.0) (2021-09-21) ### Features * add support for workforce pool credentials ([#868](https://www.github.com/googleapis/google-auth-library-python/issues/868)) ([993bab2](https://www.github.com/googleapis/google-auth-library-python/commit/993bab2aaacf3034e09d9f0f25d36c0e815d3a29)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- CHANGELOG.md | 7 +++++++ google/auth/version.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 056aeac6e..3e5b77b0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://pypi.org/project/google-auth/#history +## [2.2.0](https://www.github.com/googleapis/google-auth-library-python/compare/v2.1.0...v2.2.0) (2021-09-21) + + +### Features + +* add support for workforce pool credentials ([#868](https://www.github.com/googleapis/google-auth-library-python/issues/868)) ([993bab2](https://www.github.com/googleapis/google-auth-library-python/commit/993bab2aaacf3034e09d9f0f25d36c0e815d3a29)) + ## [2.1.0](https://www.github.com/googleapis/google-auth-library-python/compare/v2.0.2...v2.1.0) (2021-09-10) diff --git a/google/auth/version.py b/google/auth/version.py index a02dc8aec..b423306c0 100644 --- a/google/auth/version.py +++ b/google/auth/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2.1.0" +__version__ = "2.2.0"