diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 12edee776..fccaa8e84 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,4 +13,4 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:452901c74a22f9b9a3bd02bce780b8e8805c97270d424684bff809ce5be8c2a2 + digest: sha256:3bf87e47c2173d7eed42714589dc4da2c07c3268610f1e47f8e1a30decbfc7f1 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b76778cd1..4ddae6fb2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,10 +4,27 @@ # For syntax help see: # https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax -# The @googleapis/yoshi-python is the default owner for changes in this repo -* @arithmetic1728 @sai-sunder-s @googleapis/googleapis-auth @googleapis/yoshi-python +# The @googleapis/googleapis-auth and @googleapis/yoshi-python is the default owner for changes in this repo +* @googleapis/googleapis-auth @googleapis/yoshi-python +google/auth/_default.py @googleapis/googleapis-auth @googleapis/aion-sdk +google/auth/aws.py @googleapis/googleapis-auth @googleapis/aion-sdk +google/auth/credentials.py @googleapis/googleapis-auth @googleapis/aion-sdk +google/auth/downscoped.py @googleapis/googleapis-auth @googleapis/aion-sdk +google/auth/external_account.py @googleapis/googleapis-auth @googleapis/aion-sdk +google/auth/external_account_authorized_user.py @googleapis/googleapis-auth @googleapis/aion-sdk +google/auth/identity_pool.py @googleapis/googleapis-auth @googleapis/aion-sdk +google/auth/pluggable.py @googleapis/googleapis-auth @googleapis/aion-sdk +google/auth/sts.py @googleapis/googleapis-auth @googleapis/aion-sdk +google/auth/impersonated_credentials.py @googleapis/googleapis-auth @googleapis/aion-sdk +tests/test__default.py @googleapis/googleapis-auth @googleapis/aion-sdk +tests/test_aws.py @googleapis/googleapis-auth @googleapis/aion-sdk +tests/test_credentials.py @googleapis/googleapis-auth @googleapis/aion-sdk +tests/test_downscoped.py @googleapis/googleapis-auth @googleapis/aion-sdk +tests/test_external_account.py @googleapis/googleapis-auth @googleapis/aion-sdk +tests/test_external_account_authorized_user.py @googleapis/googleapis-auth @googleapis/aion-sdk +tests/test_identity_pool.py @googleapis/googleapis-auth @googleapis/aion-sdk +tests/test_pluggable.py @googleapis/googleapis-auth @googleapis/aion-sdk +tests/test_sts.py @googleapis/googleapis-auth @googleapis/aion-sdk +tests/test_impersonated_credentials.py @googleapis/googleapis-auth @googleapis/aion-sdk +/samples/ @googleapis/python-samples-owners system_tests/secrets.tar.enc # Remove noise from test creds. - - -# The python-samples-reviewers team is the default owner for samples changes -/samples/ @googleapis/python-samples-owners diff --git a/.kokoro/docker/docs/Dockerfile b/.kokoro/docker/docs/Dockerfile index 238b87b9d..f8137d0ae 100644 --- a/.kokoro/docker/docs/Dockerfile +++ b/.kokoro/docker/docs/Dockerfile @@ -60,16 +60,16 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* \ && rm -f /var/cache/apt/archives/*.deb -###################### Install python 3.8.11 +###################### Install python 3.9.13 -# Download python 3.8.11 -RUN wget https://www.python.org/ftp/python/3.8.11/Python-3.8.11.tgz +# Download python 3.9.13 +RUN wget https://www.python.org/ftp/python/3.9.13/Python-3.9.13.tgz # Extract files -RUN tar -xvf Python-3.8.11.tgz +RUN tar -xvf Python-3.9.13.tgz -# Install python 3.8.11 -RUN ./Python-3.8.11/configure --enable-optimizations +# Install python 3.9.13 +RUN ./Python-3.9.13/configure --enable-optimizations RUN make altinstall ###################### Install pip diff --git a/.kokoro/requirements.in b/.kokoro/requirements.in index 7718391a3..cbd7e77f4 100644 --- a/.kokoro/requirements.in +++ b/.kokoro/requirements.in @@ -5,4 +5,6 @@ typing-extensions twine wheel setuptools -nox \ No newline at end of file +nox +charset-normalizer<3 +click<8.1.0 diff --git a/.kokoro/requirements.txt b/.kokoro/requirements.txt index 31425f164..05dc4672e 100644 --- a/.kokoro/requirements.txt +++ b/.kokoro/requirements.txt @@ -20,9 +20,9 @@ cachetools==5.2.0 \ --hash=sha256:6a94c6402995a99c3970cc7e4884bb60b4a8639938157eeed436098bf9831757 \ --hash=sha256:f9f17d2aec496a9aa6b76f53e3b614c965223c061982d434d160f930c698a9db # via google-auth -certifi==2022.9.24 \ - --hash=sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14 \ - --hash=sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382 +certifi==2022.12.7 \ + --hash=sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3 \ + --hash=sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18 # via requests cffi==1.15.1 \ --hash=sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5 \ @@ -93,11 +93,14 @@ cffi==1.15.1 \ charset-normalizer==2.1.1 \ --hash=sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845 \ --hash=sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f - # via requests + # via + # -r requirements.in + # requests click==8.0.4 \ --hash=sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1 \ --hash=sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb # via + # -r requirements.in # gcp-docuploader # gcp-releasetool colorlog==6.7.0 \ @@ -156,9 +159,9 @@ gcp-docuploader==0.6.4 \ --hash=sha256:01486419e24633af78fd0167db74a2763974765ee8078ca6eb6964d0ebd388af \ --hash=sha256:70861190c123d907b3b067da896265ead2eeb9263969d6955c9e0bb091b5ccbf # via -r requirements.in -gcp-releasetool==1.9.1 \ - --hash=sha256:952f4055d5d986b070ae2a71c4410b250000f9cc5a1e26398fcd55a5bbc5a15f \ - --hash=sha256:d0d3c814a97c1a237517e837d8cfa668ced8df4b882452578ecef4a4e79c583b +gcp-releasetool==1.10.0 \ + --hash=sha256:72a38ca91b59c24f7e699e9227c90cbe4dd71b789383cb0164b088abae294c83 \ + --hash=sha256:8c7c99320208383d4bb2b808c6880eb7a81424afe7cdba3c8d84b25f4f0e097d # via -r requirements.in google-api-core==2.10.2 \ --hash=sha256:10c06f7739fe57781f87523375e8e1a3a4674bf6392cd6131a3222182b971320 \ @@ -166,9 +169,9 @@ google-api-core==2.10.2 \ # via # google-cloud-core # google-cloud-storage -google-auth==2.14.0 \ - --hash=sha256:1ad5b0e6eba5f69645971abb3d2c197537d5914070a8c6d30299dfdb07c5c700 \ - --hash=sha256:cf24817855d874ede2efd071aa22125445f555de1685b739a9782fcf408c2a3d +google-auth==2.14.1 \ + --hash=sha256:ccaa901f31ad5cbb562615eb8b664b3dd0bf5404a67618e642307f00613eda4d \ + --hash=sha256:f5d8701633bebc12e0deea4df8abd8aff31c28b355360597f7f2ee60f2e4d016 # via # gcp-releasetool # google-api-core @@ -178,9 +181,9 @@ google-cloud-core==2.3.2 \ --hash=sha256:8417acf6466be2fa85123441696c4badda48db314c607cf1e5d543fa8bdc22fe \ --hash=sha256:b9529ee7047fd8d4bf4a2182de619154240df17fbe60ead399078c1ae152af9a # via google-cloud-storage -google-cloud-storage==2.5.0 \ - --hash=sha256:19a26c66c317ce542cea0830b7e787e8dac2588b6bfa4d3fd3b871ba16305ab0 \ - --hash=sha256:382f34b91de2212e3c2e7b40ec079d27ee2e3dbbae99b75b1bcd8c63063ce235 +google-cloud-storage==2.6.0 \ + --hash=sha256:104ca28ae61243b637f2f01455cc8a05e8f15a2a18ced96cb587241cdd3820f5 \ + --hash=sha256:4ad0415ff61abdd8bb2ae81c1f8f7ec7d91a1011613f2db87c614c550f97bfe9 # via gcp-docuploader google-crc32c==1.5.0 \ --hash=sha256:024894d9d3cfbc5943f8f230e23950cd4906b2fe004c72e29b209420a1e6b05a \ @@ -256,9 +259,9 @@ google-resumable-media==2.4.0 \ --hash=sha256:2aa004c16d295c8f6c33b2b4788ba59d366677c0a25ae7382436cb30f776deaa \ --hash=sha256:8d5518502f92b9ecc84ac46779bd4f09694ecb3ba38a3e7ca737a86d15cbca1f # via google-cloud-storage -googleapis-common-protos==1.56.4 \ - --hash=sha256:8eb2cbc91b69feaf23e32452a7ae60e791e09967d81d4fcc7fc388182d1bd394 \ - --hash=sha256:c25873c47279387cfdcbdafa36149887901d36202cb645a0e4f29686bf6e4417 +googleapis-common-protos==1.57.0 \ + --hash=sha256:27a849d6205838fb6cc3c1c21cb9800707a661bb21c6ce7fb13e99eb1f8a0c46 \ + --hash=sha256:a9f4a1d7f6d9809657b7f1316a1aa527f6664891531bcfcc13b6696e685f443c # via google-api-core idna==3.4 \ --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \ @@ -269,6 +272,7 @@ importlib-metadata==5.0.0 \ --hash=sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43 # via # -r requirements.in + # keyring # twine jaraco-classes==3.2.3 \ --hash=sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158 \ @@ -284,9 +288,9 @@ jinja2==3.1.2 \ --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \ --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61 # via gcp-releasetool -keyring==23.9.3 \ - --hash=sha256:69732a15cb1433bdfbc3b980a8a36a04878a6cfd7cb99f497b573f31618001c0 \ - --hash=sha256:69b01dd83c42f590250fe7a1f503fc229b14de83857314b1933a3ddbf595c4a5 +keyring==23.11.0 \ + --hash=sha256:3dd30011d555f1345dec2c262f0153f2f0ca6bca041fb1dc4588349bb4c0ac1e \ + --hash=sha256:ad192263e2cdd5f12875dedc2da13534359a7e760e77f8d04b50968a821c2361 # via # gcp-releasetool # twine @@ -350,9 +354,9 @@ pkginfo==1.8.3 \ --hash=sha256:848865108ec99d4901b2f7e84058b6e7660aae8ae10164e015a6dcf5b242a594 \ --hash=sha256:a84da4318dd86f870a9447a8c98340aa06216bfc6f2b7bdc4b8766984ae1867c # via twine -platformdirs==2.5.2 \ - --hash=sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788 \ - --hash=sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19 +platformdirs==2.5.4 \ + --hash=sha256:1006647646d80f16130f052404c6b901e80ee4ed6bef6792e1f238a8969106f7 \ + --hash=sha256:af0276409f9a02373d540bf8480021a048711d572745aef4b7842dad245eba10 # via virtualenv protobuf==3.20.3 \ --hash=sha256:03038ac1cfbc41aa21f6afcbcd357281d7521b4157926f30ebecc8d4ea59dcb7 \ @@ -381,7 +385,6 @@ protobuf==3.20.3 \ # gcp-docuploader # gcp-releasetool # google-api-core - # googleapis-common-protos py==1.11.0 \ --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \ --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 @@ -476,17 +479,17 @@ urllib3==1.26.12 \ # via # requests # twine -virtualenv==20.16.6 \ - --hash=sha256:186ca84254abcbde98180fd17092f9628c5fe742273c02724972a1d8a2035108 \ - --hash=sha256:530b850b523c6449406dfba859d6345e48ef19b8439606c5d74d7d3c9e14d76e +virtualenv==20.16.7 \ + --hash=sha256:8691e3ff9387f743e00f6bb20f70121f5e4f596cae754531f2b3b3a1b1ac696e \ + --hash=sha256:efd66b00386fdb7dbe4822d172303f40cd05e50e01740b19ea42425cbe653e29 # via nox webencodings==0.5.1 \ --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \ --hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923 # via bleach -wheel==0.37.1 \ - --hash=sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a \ - --hash=sha256:e9a504e793efbca1b8e0e9cb979a249cf4a0a7b5b8c9e8b65a5e39d49529c1c4 +wheel==0.38.4 \ + --hash=sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac \ + --hash=sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8 # via -r requirements.in zipp==3.10.0 \ --hash=sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1 \ @@ -494,7 +497,7 @@ zipp==3.10.0 \ # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: -setuptools==65.5.0 \ - --hash=sha256:512e5536220e38146176efb833d4a62aa726b7bbff82cfbc8ba9eaa3996e0b17 \ - --hash=sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356 +setuptools==65.5.1 \ + --hash=sha256:d0b9a8433464d5800cbe05094acf5c6d52a91bfac9b52bcfc4d41382be5d5d31 \ + --hash=sha256:e197a19aa8ec9722928f2206f8de752def0e4c9fc6953527360d1c36d94ddb2f # via -r requirements.in diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a3cc94cf..b3c2aee94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,20 @@ [1]: https://pypi.org/project/google-auth/#history +## [2.16.0](https://github.com/googleapis/google-auth-library-python/compare/v2.15.0...v2.16.0) (2023-01-09) + + +### Features + +* AwsCredentials should not call metadata server if security creds and region are retrievable through the environment variables ([#1195](https://github.com/googleapis/google-auth-library-python/issues/1195)) ([5e27c8f](https://github.com/googleapis/google-auth-library-python/commit/5e27c8f213b2e19ec504a04e1f95fc1333ea9e1e)) +* Wrap all python built-in exceptions into library excpetions ([#1191](https://github.com/googleapis/google-auth-library-python/issues/1191)) ([a83af39](https://github.com/googleapis/google-auth-library-python/commit/a83af399fe98764ee851997bf3078ec45a9b51c9)) + + +### Bug Fixes + +* Allow get_project_id to take a request ([#1203](https://github.com/googleapis/google-auth-library-python/issues/1203)) ([9a4d23a](https://github.com/googleapis/google-auth-library-python/commit/9a4d23a28eb4b9aa9e457ad053c087a0450eb298)) +* Make OAUTH2.0 client resistant to string type 'expires_in' responses from non-compliant services ([#1208](https://github.com/googleapis/google-auth-library-python/issues/1208)) ([9fc7b1c](https://github.com/googleapis/google-auth-library-python/commit/9fc7b1c5613366cc1ad7186f894cec26a5f2231e)) + ## [2.15.0](https://github.com/googleapis/google-auth-library-python/compare/v2.14.1...v2.15.0) (2022-12-01) diff --git a/google/auth/_default.py b/google/auth/_default.py index 0860c67fe..195388c9d 100644 --- a/google/auth/_default.py +++ b/google/auth/_default.py @@ -434,7 +434,7 @@ def _get_impersonated_service_account_credentials(filename, info, scopes): filename, source_credentials_info ) else: - raise ValueError( + raise exceptions.InvalidType( "source credential of type {} is not supported.".format( source_credentials_type ) @@ -443,7 +443,7 @@ def _get_impersonated_service_account_credentials(filename, info, scopes): start_index = impersonation_url.rfind("/") end_index = impersonation_url.find(":generateAccessToken") if start_index == -1 or end_index == -1 or start_index > end_index: - raise ValueError( + raise exceptions.InvalidValue( "Cannot extract target principal from {}".format(impersonation_url) ) target_principal = impersonation_url[start_index + 1 : end_index] diff --git a/google/auth/_helpers.py b/google/auth/_helpers.py index 1b08ab87f..30fbafb64 100644 --- a/google/auth/_helpers.py +++ b/google/auth/_helpers.py @@ -22,6 +22,7 @@ import six from six.moves import urllib +from google.auth import exceptions # Token server doesn't provide a new a token when doing refresh unless the # token is expiring within 30 seconds, so refresh threshold should not be @@ -51,10 +52,10 @@ def decorator(method): Callable: the same method passed in with an updated docstring. Raises: - ValueError: if the method already has a docstring. + google.auth.exceptions.InvalidOperation: if the method already has a docstring. """ if method.__doc__: - raise ValueError("Method already has a docstring.") + raise exceptions.InvalidOperation("Method already has a docstring.") source_method = getattr(source_class, method.__name__) method.__doc__ = source_method.__doc__ @@ -101,13 +102,15 @@ def to_bytes(value, encoding="utf-8"): passed in if it started out as bytes. Raises: - ValueError: If the value could not be converted to bytes. + google.auth.exceptions.InvalidValue: If the value could not be converted to bytes. """ result = value.encode(encoding) if isinstance(value, six.text_type) else value if isinstance(result, six.binary_type): return result else: - raise ValueError("{0!r} could not be converted to bytes".format(value)) + raise exceptions.InvalidValue( + "{0!r} could not be converted to bytes".format(value) + ) def from_bytes(value): @@ -121,13 +124,15 @@ def from_bytes(value): if it started out as unicode. Raises: - ValueError: If the value could not be converted to unicode. + google.auth.exceptions.InvalidValue: If the value could not be converted to unicode. """ result = value.decode("utf-8") if isinstance(value, six.binary_type) else value if isinstance(result, six.text_type): return result else: - raise ValueError("{0!r} could not be converted to unicode".format(value)) + raise exceptions.InvalidValue( + "{0!r} could not be converted to unicode".format(value) + ) def update_query(url, params, remove=None): diff --git a/google/auth/_service_account_info.py b/google/auth/_service_account_info.py index 157099273..b17f34f5c 100644 --- a/google/auth/_service_account_info.py +++ b/google/auth/_service_account_info.py @@ -20,6 +20,7 @@ import six from google.auth import crypt +from google.auth import exceptions def from_dict(data, require=None, use_rsa_signer=True): @@ -40,7 +41,7 @@ def from_dict(data, require=None, use_rsa_signer=True): service account file. Raises: - ValueError: if the data was in the wrong format, or if one of the + MalformedError: if the data was in the wrong format, or if one of the required keys is missing. """ keys_needed = set(require if require is not None else []) @@ -48,7 +49,7 @@ def from_dict(data, require=None, use_rsa_signer=True): missing = keys_needed.difference(six.iterkeys(data)) if missing: - raise ValueError( + raise exceptions.MalformedError( "Service account info was not in the expected format, missing " "fields {}.".format(", ".join(missing)) ) diff --git a/google/auth/api_key.py b/google/auth/api_key.py index 49c6ffd2d..4fdf7f276 100644 --- a/google/auth/api_key.py +++ b/google/auth/api_key.py @@ -20,6 +20,7 @@ from google.auth import _helpers from google.auth import credentials +from google.auth import exceptions class Credentials(credentials.Credentials): @@ -36,7 +37,7 @@ def __init__(self, token): """ super(Credentials, self).__init__() if not token: - raise ValueError("Token must be a non-empty API key string") + raise exceptions.InvalidValue("Token must be a non-empty API key string") self.token = token @property diff --git a/google/auth/app_engine.py b/google/auth/app_engine.py index 1460a7d1a..7083ee614 100644 --- a/google/auth/app_engine.py +++ b/google/auth/app_engine.py @@ -27,6 +27,7 @@ from google.auth import _helpers from google.auth import credentials from google.auth import crypt +from google.auth import exceptions # pytype: disable=import-error try: @@ -67,13 +68,13 @@ def get_project_id(): str: The project ID Raises: - EnvironmentError: If the App Engine APIs are unavailable. + google.auth.exceptions.OSError: If the App Engine APIs are unavailable. """ # pylint: disable=missing-raises-doc - # Pylint rightfully thinks EnvironmentError is OSError, but doesn't + # Pylint rightfully thinks google.auth.exceptions.OSError is OSError, but doesn't # realize it's a valid alias. if app_identity is None: - raise EnvironmentError("The App Engine APIs are not available.") + raise exceptions.OSError("The App Engine APIs are not available.") return app_identity.get_application_id() @@ -107,13 +108,13 @@ def __init__( and billing. Raises: - EnvironmentError: If the App Engine APIs are unavailable. + google.auth.exceptions.OSError: If the App Engine APIs are unavailable. """ # pylint: disable=missing-raises-doc - # Pylint rightfully thinks EnvironmentError is OSError, but doesn't + # Pylint rightfully thinks google.auth.exceptions.OSError is OSError, but doesn't # realize it's a valid alias. if app_identity is None: - raise EnvironmentError("The App Engine APIs are not available.") + raise exceptions.OSError("The App Engine APIs are not available.") super(Credentials, self).__init__() self._scopes = scopes diff --git a/google/auth/aws.py b/google/auth/aws.py index 04c5e7c5d..f651433f0 100644 --- a/google/auth/aws.py +++ b/google/auth/aws.py @@ -123,7 +123,7 @@ def get_request_options( ) # Validate provided URL. if not uri.hostname or uri.scheme != "https": - raise ValueError("Invalid AWS service URL") + raise exceptions.InvalidResource("Invalid AWS service URL") header_map = _generate_authentication_header_map( host=uri.hostname, @@ -408,9 +408,11 @@ def __init__( env_id, env_version = (None, None) if env_id != "aws" or self._cred_verification_url is None: - raise ValueError("No valid AWS 'credential_source' provided") + raise exceptions.InvalidResource( + "No valid AWS 'credential_source' provided" + ) elif int(env_version or "") != 1: - raise ValueError( + raise exceptions.InvalidValue( "aws version '{}' is not supported in the current build.".format( env_version ) @@ -428,7 +430,7 @@ def validate_metadata_server_url_if_any(url_string, name_of_data): if url_string: url = urlparse(url_string) if url.hostname != "169.254.169.254" and url.hostname != "fd00:ec2::254": - raise ValueError( + raise exceptions.InvalidResource( "Invalid hostname '{}' for '{}'".format(url.hostname, name_of_data) ) @@ -464,8 +466,12 @@ def retrieve_subject_token(self, request): Returns: str: The retrieved subject token. """ - # Fetch the session token required to make meta data endpoint calls to aws - if request is not None and self._imdsv2_session_token_url is not None: + # Fetch the session token required to make meta data endpoint calls to aws. + if ( + request is not None + and self._imdsv2_session_token_url is not None + and self._should_use_metadata_server() + ): headers = {"X-aws-ec2-metadata-token-ttl-seconds": "300"} imdsv2_session_token_response = request( @@ -736,6 +742,25 @@ def _get_metadata_role_name(self, request, imdsv2_session_token): return response_body + def _should_use_metadata_server(self): + # The AWS region can be provided through AWS_REGION or AWS_DEFAULT_REGION. + # The metadata server should be used if it cannot be retrieved from one of + # these environment variables. + if not os.environ.get(environment_vars.AWS_REGION) and not os.environ.get( + environment_vars.AWS_DEFAULT_REGION + ): + return True + + # AWS security credentials can be retrieved from the AWS_ACCESS_KEY_ID + # and AWS_SECRET_ACCESS_KEY environment variables. The metadata server + # should be used if either of these are not available. + if not os.environ.get(environment_vars.AWS_ACCESS_KEY_ID) or not os.environ.get( + environment_vars.AWS_SECRET_ACCESS_KEY + ): + return True + + return False + @classmethod def from_info(cls, info, **kwargs): """Creates an AWS Credentials instance from parsed external account info. diff --git a/google/auth/compute_engine/credentials.py b/google/auth/compute_engine/credentials.py index e97fabea9..618fa5a2d 100644 --- a/google/auth/compute_engine/credentials.py +++ b/google/auth/compute_engine/credentials.py @@ -221,7 +221,7 @@ def __init__( if use_metadata_identity_endpoint: if token_uri or additional_claims or service_account_email or signer: - raise ValueError( + raise exceptions.MalformedError( "If use_metadata_identity_endpoint is set, token_uri, " "additional_claims, service_account_email, signer arguments" " must not be set" @@ -312,7 +312,7 @@ def with_token_uri(self, token_uri): # since the signer is already instantiated, # the request is not needed if self._use_metadata_identity_endpoint: - raise ValueError( + raise exceptions.MalformedError( "If use_metadata_identity_endpoint is set, token_uri" " must not be set" ) else: @@ -423,7 +423,7 @@ def sign_bytes(self, message): Signer is not available if metadata identity endpoint is used. """ if self._use_metadata_identity_endpoint: - raise ValueError( + raise exceptions.InvalidOperation( "Signer is not available if metadata identity endpoint is used" ) return self._signer.sign(message) diff --git a/google/auth/credentials.py b/google/auth/credentials.py index ca1032a14..4c0af7a6b 100644 --- a/google/auth/credentials.py +++ b/google/auth/credentials.py @@ -21,6 +21,7 @@ import six from google.auth import _helpers, environment_vars +from google.auth import exceptions @six.add_metaclass(abc.ABCMeta) @@ -190,9 +191,9 @@ def valid(self): return True def refresh(self, request): - """Raises :class:`ValueError``, anonymous credentials cannot be + """Raises :class:``InvalidOperation``, anonymous credentials cannot be refreshed.""" - raise ValueError("Anonymous credentials cannot be refreshed.") + raise exceptions.InvalidOperation("Anonymous credentials cannot be refreshed.") def apply(self, headers, token=None): """Anonymous credentials do nothing to the request. @@ -200,10 +201,10 @@ def apply(self, headers, token=None): The optional ``token`` argument is not supported. Raises: - ValueError: If a token was specified. + google.auth.exceptions.InvalidValue: If a token was specified. """ if token is not None: - raise ValueError("Anonymous credentials don't support tokens.") + raise exceptions.InvalidValue("Anonymous credentials don't support tokens.") def before_request(self, request, method, url, headers): """Anonymous credentials do nothing to the request.""" diff --git a/google/auth/crypt/_python_rsa.py b/google/auth/crypt/_python_rsa.py index 797a2592b..e8595440c 100644 --- a/google/auth/crypt/_python_rsa.py +++ b/google/auth/crypt/_python_rsa.py @@ -29,6 +29,7 @@ import six from google.auth import _helpers +from google.auth import exceptions from google.auth.crypt import base _POW2 = (128, 64, 32, 16, 8, 4, 2, 1) @@ -101,7 +102,7 @@ def from_string(cls, public_key): der = rsa.pem.load_pem(public_key, "CERTIFICATE") asn1_cert, remaining = decoder.decode(der, asn1Spec=Certificate()) if remaining != b"": - raise ValueError("Unused bytes", remaining) + raise exceptions.InvalidValue("Unused bytes", remaining) cert_info = asn1_cert["tbsCertificate"]["subjectPublicKeyInfo"] key_bytes = _bit_list_to_bytes(cert_info["subjectPublicKey"]) @@ -162,12 +163,12 @@ def from_string(cls, key, key_id=None): elif marker_id == 1: key_info, remaining = decoder.decode(key_bytes, asn1Spec=_PKCS8_SPEC) if remaining != b"": - raise ValueError("Unused bytes", remaining) + raise exceptions.InvalidValue("Unused bytes", remaining) private_key_info = key_info.getComponentByName("privateKey") private_key = rsa.key.PrivateKey.load_pkcs1( private_key_info.asOctets(), format="DER" ) else: - raise ValueError("No key could be detected.") + raise exceptions.MalformedError("No key could be detected.") return cls(private_key, key_id=key_id) diff --git a/google/auth/crypt/base.py b/google/auth/crypt/base.py index c98d5bf64..573211d7c 100644 --- a/google/auth/crypt/base.py +++ b/google/auth/crypt/base.py @@ -20,6 +20,7 @@ import six +from google.auth import exceptions _JSON_FILE_PRIVATE_KEY = "private_key" _JSON_FILE_PRIVATE_KEY_ID = "private_key_id" @@ -106,7 +107,7 @@ def from_service_account_info(cls, info): ValueError: If the info is not in the expected format. """ if _JSON_FILE_PRIVATE_KEY not in info: - raise ValueError( + raise exceptions.MalformedError( "The private_key field was not found in the service account " "info." ) diff --git a/google/auth/downscoped.py b/google/auth/downscoped.py index a1d7b6e46..a84ac4af6 100644 --- a/google/auth/downscoped.py +++ b/google/auth/downscoped.py @@ -54,6 +54,7 @@ from google.auth import _helpers from google.auth import credentials +from google.auth import exceptions from google.oauth2 import sts # The maximum number of access boundary rules a Credential Access Boundary can @@ -86,8 +87,8 @@ def __init__(self, rules=[]): access boundary rules limiting the access that a downscoped credential will have. Raises: - TypeError: If any of the rules are not a valid type. - ValueError: If the provided rules exceed the maximum allowed. + InvalidType: If any of the rules are not a valid type. + InvalidValue: If the provided rules exceed the maximum allowed. """ self.rules = rules @@ -113,18 +114,18 @@ def rules(self, value): access boundary rules limiting the access that a downscoped credential will have. Raises: - TypeError: If any of the rules are not a valid type. - ValueError: If the provided rules exceed the maximum allowed. + InvalidType: If any of the rules are not a valid type. + InvalidValue: If the provided rules exceed the maximum allowed. """ if len(value) > _MAX_ACCESS_BOUNDARY_RULES_COUNT: - raise ValueError( + raise exceptions.InvalidValue( "Credential access boundary rules can have a maximum of {} rules.".format( _MAX_ACCESS_BOUNDARY_RULES_COUNT ) ) for access_boundary_rule in value: if not isinstance(access_boundary_rule, AccessBoundaryRule): - raise TypeError( + raise exceptions.InvalidType( "List of rules provided do not contain a valid 'google.auth.downscoped.AccessBoundaryRule'." ) # Make a copy of the original list. @@ -138,17 +139,17 @@ def add_rule(self, rule): limiting the access that a downscoped credential will have, to be added to the existing rules. Raises: - TypeError: If any of the rules are not a valid type. - ValueError: If the provided rules exceed the maximum allowed. + InvalidType: If any of the rules are not a valid type. + InvalidValue: If the provided rules exceed the maximum allowed. """ if len(self.rules) == _MAX_ACCESS_BOUNDARY_RULES_COUNT: - raise ValueError( + raise exceptions.InvalidValue( "Credential access boundary rules can have a maximum of {} rules.".format( _MAX_ACCESS_BOUNDARY_RULES_COUNT ) ) if not isinstance(rule, AccessBoundaryRule): - raise TypeError( + raise exceptions.InvalidType( "The provided rule does not contain a valid 'google.auth.downscoped.AccessBoundaryRule'." ) self._rules.append(rule) @@ -197,8 +198,8 @@ def __init__( specific Cloud Storage objects. Raises: - TypeError: If any of the parameters are not of the expected types. - ValueError: If any of the parameters are not of the expected values. + InvalidType: If any of the parameters are not of the expected types. + InvalidValue: If any of the parameters are not of the expected values. """ self.available_resource = available_resource self.available_permissions = available_permissions @@ -221,10 +222,12 @@ def available_resource(self, value): value (str): The updated value of the available resource. Raises: - TypeError: If the value is not a string. + google.auth.exceptions.InvalidType: If the value is not a string. """ if not isinstance(value, six.string_types): - raise TypeError("The provided available_resource is not a string.") + raise exceptions.InvalidType( + "The provided available_resource is not a string." + ) self._available_resource = value @property @@ -245,16 +248,16 @@ def available_permissions(self, value): value (Sequence[str]): The updated value of the available permissions. Raises: - TypeError: If the value is not a list of strings. - ValueError: If the value is not valid. + InvalidType: If the value is not a list of strings. + InvalidValue: If the value is not valid. """ for available_permission in value: if not isinstance(available_permission, six.string_types): - raise TypeError( + raise exceptions.InvalidType( "Provided available_permissions are not a list of strings." ) if available_permission.find("inRole:") != 0: - raise ValueError( + raise exceptions.InvalidValue( "available_permissions must be prefixed with 'inRole:'." ) # Make a copy of the original list. @@ -279,11 +282,11 @@ def availability_condition(self, value): value of the availability condition. Raises: - TypeError: If the value is not of type google.auth.downscoped.AvailabilityCondition + google.auth.exceptions.InvalidType: If the value is not of type google.auth.downscoped.AvailabilityCondition or None. """ if not isinstance(value, AvailabilityCondition) and value is not None: - raise TypeError( + raise exceptions.InvalidType( "The provided availability_condition is not a 'google.auth.downscoped.AvailabilityCondition' or None." ) self._availability_condition = value @@ -326,8 +329,8 @@ def __init__(self, expression, title=None, description=None): description (Optional[str]): Optional details about the purpose of the condition. Raises: - TypeError: If any of the parameters are not of the expected types. - ValueError: If any of the parameters are not of the expected values. + InvalidType: If any of the parameters are not of the expected types. + InvalidValue: If any of the parameters are not of the expected values. """ self.expression = expression self.title = title @@ -350,10 +353,10 @@ def expression(self, value): value (str): The updated value of the condition expression. Raises: - TypeError: If the value is not of type string. + google.auth.exceptions.InvalidType: If the value is not of type string. """ if not isinstance(value, six.string_types): - raise TypeError("The provided expression is not a string.") + raise exceptions.InvalidType("The provided expression is not a string.") self._expression = value @property @@ -373,10 +376,10 @@ def title(self, value): value (Optional[str]): The updated value of the title. Raises: - TypeError: If the value is not of type string or None. + google.auth.exceptions.InvalidType: If the value is not of type string or None. """ if not isinstance(value, six.string_types) and value is not None: - raise TypeError("The provided title is not a string or None.") + raise exceptions.InvalidType("The provided title is not a string or None.") self._title = value @property @@ -396,10 +399,12 @@ def description(self, value): value (Optional[str]): The updated value of the description. Raises: - TypeError: If the value is not of type string or None. + google.auth.exceptions.InvalidType: If the value is not of type string or None. """ if not isinstance(value, six.string_types) and value is not None: - raise TypeError("The provided description is not a string or None.") + raise exceptions.InvalidType( + "The provided description is not a string or None." + ) self._description = value def to_json(self): diff --git a/google/auth/exceptions.py b/google/auth/exceptions.py index 7760c87b8..fcbe61b74 100644 --- a/google/auth/exceptions.py +++ b/google/auth/exceptions.py @@ -74,3 +74,27 @@ def __init__(self, message=None, **kwargs): class ReauthSamlChallengeFailError(ReauthFailError): """An exception for SAML reauth challenge failures.""" + + +class MalformedError(DefaultCredentialsError, ValueError): + """An exception for malformed data.""" + + +class InvalidResource(DefaultCredentialsError, ValueError): + """An exception for URL error.""" + + +class InvalidOperation(DefaultCredentialsError, ValueError): + """An exception for invalid operation.""" + + +class InvalidValue(DefaultCredentialsError, ValueError): + """Used to wrap general ValueError of python.""" + + +class InvalidType(DefaultCredentialsError, TypeError): + """Used to wrap general TypeError of python.""" + + +class OSError(DefaultCredentialsError, EnvironmentError): + """Used to wrap EnvironmentError(OSError after python3.3).""" diff --git a/google/auth/external_account.py b/google/auth/external_account.py index 4249529e8..d24b22837 100644 --- a/google/auth/external_account.py +++ b/google/auth/external_account.py @@ -151,7 +151,7 @@ def __init__( if not self.is_workforce_pool and self._workforce_pool_user_project: # Workload identity pools do not support workforce pool user projects. - raise ValueError( + raise exceptions.InvalidValue( "workforce_pool_user_project should not be set for non-workforce pool " "credentials" ) @@ -445,7 +445,9 @@ def validate_token_url(token_url, url_type="token"): ] if not Credentials.is_valid_url(_TOKEN_URL_PATTERNS, token_url): - raise ValueError("The provided {} URL is invalid.".format(url_type)) + raise exceptions.InvalidResource( + "The provided {} URL is invalid.".format(url_type) + ) @staticmethod def validate_service_account_impersonation_url(url): @@ -460,7 +462,7 @@ def validate_service_account_impersonation_url(url): if not Credentials.is_valid_url( _SERVICE_ACCOUNT_IMPERSONATION_URL_PATTERNS, url ): - raise ValueError( + raise exceptions.InvalidResource( "The provided service account impersonation URL is invalid." ) @@ -498,7 +500,7 @@ def from_info(cls, info, **kwargs): credentials. Raises: - ValueError: For invalid parameters. + InvalidValue: For invalid parameters. """ return cls( audience=info.get("audience"), diff --git a/google/auth/external_account_authorized_user.py b/google/auth/external_account_authorized_user.py index 51e7f2058..a2d4edf6f 100644 --- a/google/auth/external_account_authorized_user.py +++ b/google/auth/external_account_authorized_user.py @@ -118,7 +118,7 @@ def __init__( self._scopes = scopes if not self.valid and not self.can_refresh: - raise ValueError( + raise exceptions.InvalidOperation( "Token should be created with fields to make it valid (`token` and " "`expiry`), or fields to allow it to refresh (`refresh_token`, " "`token_url`, `client_id`, `client_secret`)." @@ -222,12 +222,19 @@ def can_refresh(self): (self._refresh_token, self._token_url, self._client_id, self._client_secret) ) - def get_project_id(self): + def get_project_id(self, request=None): """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. + + Args: + request (google.auth.transport.requests.Request): Request object. + Unused here, but passed from _default.default(). + + Return: + str: project ID is not determinable for this credential type so it returns None """ return None diff --git a/google/auth/identity_pool.py b/google/auth/identity_pool.py index 5fa9faef9..ebe50883c 100644 --- a/google/auth/identity_pool.py +++ b/google/auth/identity_pool.py @@ -122,11 +122,11 @@ def __init__( # environment_id is only supported in AWS or dedicated future external # account credentials. if "environment_id" in credential_source: - raise ValueError( + raise exceptions.MalformedError( "Invalid Identity Pool credential_source field 'environment_id'" ) if self._credential_source_format_type not in ["text", "json"]: - raise ValueError( + raise exceptions.MalformedError( "Invalid credential_source format '{}'".format( self._credential_source_format_type ) @@ -137,18 +137,18 @@ def __init__( "subject_token_field_name" ) if self._credential_source_field_name is None: - raise ValueError( + raise exceptions.MalformedError( "Missing subject_token_field_name for JSON credential_source format" ) else: self._credential_source_field_name = None if self._credential_source_file and self._credential_source_url: - raise ValueError( + raise exceptions.MalformedError( "Ambiguous credential_source. 'file' is mutually exclusive with 'url'." ) if not self._credential_source_file and not self._credential_source_url: - raise ValueError( + raise exceptions.MalformedError( "Missing credential_source. A 'file' or 'url' must be provided." ) diff --git a/google/auth/jwt.py b/google/auth/jwt.py index 21de8fe95..390368958 100644 --- a/google/auth/jwt.py +++ b/google/auth/jwt.py @@ -122,7 +122,9 @@ def _decode_jwt_segment(encoded_section): try: return json.loads(section_bytes.decode("utf-8")) except ValueError as caught_exc: - new_exc = ValueError("Can't parse segment: {0}".format(section_bytes)) + new_exc = exceptions.MalformedError( + "Can't parse segment: {0}".format(section_bytes) + ) six.raise_from(new_exc, caught_exc) @@ -137,13 +139,14 @@ def _unverified_decode(token): signature. Raises: - ValueError: if there are an incorrect amount of segments in the token or - segments of the wrong type. + google.auth.exceptions.MalformedError: if there are an incorrect amount of segments in the token or segments of the wrong type. """ token = _helpers.to_bytes(token) if token.count(b".") != 2: - raise ValueError("Wrong number of segments in token: {0}".format(token)) + raise exceptions.MalformedError( + "Wrong number of segments in token: {0}".format(token) + ) encoded_header, encoded_payload, signature = token.split(b".") signed_section = encoded_header + b"." + encoded_payload @@ -154,12 +157,12 @@ def _unverified_decode(token): payload = _decode_jwt_segment(encoded_payload) if not isinstance(header, Mapping): - raise ValueError( + raise exceptions.MalformedError( "Header segment should be a JSON object: {0}".format(encoded_header) ) if not isinstance(payload, Mapping): - raise ValueError( + raise exceptions.MalformedError( "Payload segment should be a JSON object: {0}".format(encoded_payload) ) @@ -193,14 +196,17 @@ def _verify_iat_and_exp(payload, clock_skew_in_seconds=0): validation. Raises: - ValueError: if any checks failed. + google.auth.exceptions.InvalidValue: if value validation failed. + google.auth.exceptions.MalformedError: if schema validation failed. """ now = _helpers.datetime_to_secs(_helpers.utcnow()) # Make sure the iat and exp claims are present. for key in ("iat", "exp"): if key not in payload: - raise ValueError("Token does not contain required claim {}".format(key)) + raise exceptions.MalformedError( + "Token does not contain required claim {}".format(key) + ) # Make sure the token wasn't issued in the future. iat = payload["iat"] @@ -208,7 +214,7 @@ def _verify_iat_and_exp(payload, clock_skew_in_seconds=0): # for clock skew. earliest = iat - clock_skew_in_seconds if now < earliest: - raise ValueError( + raise exceptions.InvalidValue( "Token used too early, {} < {}. Check that your computer's clock is set correctly.".format( now, iat ) @@ -220,7 +226,7 @@ def _verify_iat_and_exp(payload, clock_skew_in_seconds=0): # to account for clow skew. latest = exp + clock_skew_in_seconds if latest < now: - raise ValueError("Token expired, {} < {}".format(latest, now)) + raise exceptions.InvalidValue("Token expired, {} < {}".format(latest, now)) def decode(token, certs=None, verify=True, audience=None, clock_skew_in_seconds=0): @@ -246,7 +252,8 @@ def decode(token, certs=None, verify=True, audience=None, clock_skew_in_seconds= Mapping[str, str]: The deserialized JSON payload in the JWT. Raises: - ValueError: if any verification checks failed. + google.auth.exceptions.InvalidValue: if value validation failed. + google.auth.exceptions.MalformedError: if schema validation failed. """ header, payload, signed_section, signature = _unverified_decode(token) @@ -263,7 +270,7 @@ def decode(token, certs=None, verify=True, audience=None, clock_skew_in_seconds= except KeyError as exc: if key_alg in _CRYPTOGRAPHY_BASED_ALGORITHMS: six.raise_from( - ValueError( + exceptions.InvalidValue( "The key algorithm {} requires the cryptography package " "to be installed.".format(key_alg) ), @@ -271,7 +278,10 @@ def decode(token, certs=None, verify=True, audience=None, clock_skew_in_seconds= ) else: six.raise_from( - ValueError("Unsupported signature algorithm {}".format(key_alg)), exc + exceptions.InvalidValue( + "Unsupported signature algorithm {}".format(key_alg) + ), + exc, ) # If certs is specified as a dictionary of key IDs to certificates, then @@ -279,7 +289,9 @@ def decode(token, certs=None, verify=True, audience=None, clock_skew_in_seconds= if isinstance(certs, Mapping): if key_id: if key_id not in certs: - raise ValueError("Certificate for key id {} not found.".format(key_id)) + raise exceptions.MalformedError( + "Certificate for key id {} not found.".format(key_id) + ) certs_to_check = [certs[key_id]] # If there's no key id in the header, check against all of the certs. else: @@ -291,7 +303,7 @@ def decode(token, certs=None, verify=True, audience=None, clock_skew_in_seconds= if not crypt.verify_signature( signed_section, signature, certs_to_check, verifier_cls ): - raise ValueError("Could not verify token signature.") + raise exceptions.MalformedError("Could not verify token signature.") # Verify the issued at and created times in the payload. _verify_iat_and_exp(payload, clock_skew_in_seconds) @@ -302,7 +314,7 @@ def decode(token, certs=None, verify=True, audience=None, clock_skew_in_seconds= if isinstance(audience, str): audience = [audience] if claim_audience not in audience: - raise ValueError( + raise exceptions.InvalidValue( "Token has wrong audience {}, expected one of {}".format( claim_audience, audience ) @@ -414,7 +426,7 @@ def _from_signer_and_info(cls, signer, info, **kwargs): google.auth.jwt.Credentials: The constructed credentials. Raises: - ValueError: If the info is not in the expected format. + google.auth.exceptions.MalformedError: If the info is not in the expected format. """ kwargs.setdefault("subject", info["client_email"]) kwargs.setdefault("issuer", info["client_email"]) @@ -433,7 +445,7 @@ def from_service_account_info(cls, info, **kwargs): google.auth.jwt.Credentials: The constructed credentials. Raises: - ValueError: If the info is not in the expected format. + google.auth.exceptions.MalformedError: If the info is not in the expected format. """ signer = _service_account_info.from_dict(info, require=["client_email"]) return cls._from_signer_and_info(signer, info, **kwargs) @@ -651,7 +663,7 @@ def _from_signer_and_info(cls, signer, info, **kwargs): google.auth.jwt.OnDemandCredentials: The constructed credentials. Raises: - ValueError: If the info is not in the expected format. + google.auth.exceptions.MalformedError: If the info is not in the expected format. """ kwargs.setdefault("subject", info["client_email"]) kwargs.setdefault("issuer", info["client_email"]) @@ -670,7 +682,7 @@ def from_service_account_info(cls, info, **kwargs): google.auth.jwt.OnDemandCredentials: The constructed credentials. Raises: - ValueError: If the info is not in the expected format. + google.auth.exceptions.MalformedError: If the info is not in the expected format. """ signer = _service_account_info.from_dict(info, require=["client_email"]) return cls._from_signer_and_info(signer, info, **kwargs) diff --git a/google/auth/pluggable.py b/google/auth/pluggable.py index b4fa448b8..7fef36112 100644 --- a/google/auth/pluggable.py +++ b/google/auth/pluggable.py @@ -93,7 +93,8 @@ def __init__( Raises: google.auth.exceptions.RefreshError: If an error is encountered during access token retrieval logic. - ValueError: For invalid parameters. + google.auth.exceptions.InvalidValue: For invalid parameters. + google.auth.exceptions.MalformedError: For invalid parameters. .. note:: Typically one of the helper constructors :meth:`from_file` or @@ -111,12 +112,12 @@ def __init__( ) if not isinstance(credential_source, Mapping): self._credential_source_executable = None - raise ValueError( + raise exceptions.MalformedError( "Missing credential_source. The credential_source is not a dict." ) self._credential_source_executable = credential_source.get("executable") if not self._credential_source_executable: - raise ValueError( + raise exceptions.MalformedError( "Missing credential_source. An 'executable' must be provided." ) self._credential_source_executable_command = self._credential_source_executable.get( @@ -136,7 +137,7 @@ def __init__( self._tokeninfo_username = "" if not self._credential_source_executable_command: - raise ValueError( + raise exceptions.MalformedError( "Missing command field. Executable command must be provided." ) if not self._credential_source_executable_timeout_millis: @@ -149,7 +150,7 @@ def __init__( or self._credential_source_executable_timeout_millis > EXECUTABLE_TIMEOUT_MILLIS_UPPER_BOUND ): - raise ValueError("Timeout must be between 5 and 120 seconds.") + raise exceptions.InvalidValue("Timeout must be between 5 and 120 seconds.") if self._credential_source_executable_interactive_timeout_millis: if ( @@ -158,7 +159,7 @@ def __init__( or self._credential_source_executable_interactive_timeout_millis > EXECUTABLE_INTERACTIVE_TIMEOUT_MILLIS_UPPER_BOUND ): - raise ValueError( + raise exceptions.InvalidValue( "Interactive timeout must be between 30 seconds and 30 minutes." ) @@ -183,7 +184,7 @@ def retrieve_subject_token(self, request): "expiration_time" not in response ): # Always treat missing expiration_time as expired and proceed to executable run. raise exceptions.RefreshError - except ValueError: + except (exceptions.MalformedError, exceptions.InvalidValue): raise except exceptions.RefreshError: pass @@ -247,7 +248,9 @@ def revoke(self, request): """ if not self.interactive: - raise ValueError("Revoke is only enabled under interactive mode.") + raise exceptions.InvalidValue( + "Revoke is only enabled under interactive mode." + ) self._validate_running_mode() if not _helpers.is_python_3(): @@ -307,7 +310,8 @@ def from_info(cls, info, **kwargs): credentials. Raises: - ValueError: For invalid parameters. + google.auth.exceptions.InvalidValue: For invalid parameters. + google.auth.exceptions.MalformedError: For invalid parameters. """ return super(Credentials, cls).from_info(info, **kwargs) @@ -344,7 +348,7 @@ def _parse_subject_token(self, response): self._validate_response_schema(response) if not response["success"]: if "code" not in response or "message" not in response: - raise ValueError( + raise exceptions.MalformedError( "Error code and message fields are required in the response." ) raise exceptions.RefreshError( @@ -357,7 +361,9 @@ def _parse_subject_token(self, response): "The token returned by the executable is expired." ) if "token_type" not in response: - raise ValueError("The executable response is missing the token_type field.") + raise exceptions.MalformedError( + "The executable response is missing the token_type field." + ) if ( response["token_type"] == "urn:ietf:params:oauth:token-type:jwt" or response["token_type"] == "urn:ietf:params:oauth:token-type:id_token" @@ -375,7 +381,9 @@ def _validate_revoke_response(self, response): def _validate_response_schema(self, response): if "version" not in response: - raise ValueError("The executable response is missing the version field.") + raise exceptions.MalformedError( + "The executable response is missing the version field." + ) if response["version"] > EXECUTABLE_SUPPORTED_MAX_VERSION: raise exceptions.RefreshError( "Executable returned unsupported version {}.".format( @@ -384,19 +392,21 @@ def _validate_response_schema(self, response): ) if "success" not in response: - raise ValueError("The executable response is missing the success field.") + raise exceptions.MalformedError( + "The executable response is missing the success field." + ) def _validate_running_mode(self): env_allow_executables = os.environ.get( "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES" ) if env_allow_executables != "1": - raise ValueError( + raise exceptions.MalformedError( "Executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') to run." ) if self.interactive and not self._credential_source_executable_output_file: - raise ValueError( + raise exceptions.MalformedError( "An output_file must be specified in the credential configuration for interactive mode." ) @@ -404,9 +414,11 @@ def _validate_running_mode(self): self.interactive and not self._credential_source_executable_interactive_timeout_millis ): - raise ValueError( + raise exceptions.InvalidOperation( "Interactive mode cannot run without an interactive timeout." ) if self.interactive and not self.is_workforce_pool: - raise ValueError("Interactive mode is only enabled for workforce pool.") + raise exceptions.InvalidValue( + "Interactive mode is only enabled for workforce pool." + ) diff --git a/google/auth/transport/_aiohttp_requests.py b/google/auth/transport/_aiohttp_requests.py index 179cadbdf..364570e31 100644 --- a/google/auth/transport/_aiohttp_requests.py +++ b/google/auth/transport/_aiohttp_requests.py @@ -140,7 +140,7 @@ class Request(transport.Request): def __init__(self, session=None): # TODO: Use auto_decompress property for aiohttp 3.7+ if session is not None and session._auto_decompress: - raise ValueError( + raise exceptions.InvalidOperation( "Client sessions with auto_decompress=True are not supported." ) self.session = session diff --git a/google/auth/transport/grpc.py b/google/auth/transport/grpc.py index 87fa5042f..55764b6f6 100644 --- a/google/auth/transport/grpc.py +++ b/google/auth/transport/grpc.py @@ -255,7 +255,7 @@ def my_client_cert_callback(): google_auth_credentials = grpc.metadata_call_credentials(metadata_plugin) if ssl_credentials and client_cert_callback: - raise ValueError( + raise exceptions.MalformedError( "Received both ssl_credentials and client_cert_callback; " "these are mutually exclusive." ) diff --git a/google/auth/version.py b/google/auth/version.py index f0ecd5d63..6ab5ecc4c 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.15.0" +__version__ = "2.16.0" diff --git a/google/oauth2/_client.py b/google/oauth2/_client.py index 7f866d446..c2eb6443f 100644 --- a/google/oauth2/_client.py +++ b/google/oauth2/_client.py @@ -120,6 +120,11 @@ def _parse_expiry(response_data): expires_in = response_data.get("expires_in", None) if expires_in is not None: + # Some services do not respect the OAUTH2.0 RFC and send expires_in as a + # JSON String. + if isinstance(expires_in, str): + expires_in = int(expires_in) + return _helpers.utcnow() + datetime.timedelta(seconds=expires_in) else: return None diff --git a/setup.py b/setup.py index 79d31b87c..c89b05d1d 100644 --- a/setup.py +++ b/setup.py @@ -37,6 +37,7 @@ "requests >= 2.20.0, < 3.0.0dev", ], "pyopenssl": ["pyopenssl>=20.0.0", "cryptography>=38.0.3"], + "requests": "requests >= 2.20.0, < 3.0.0dev", "reauth": "pyu2f>=0.1.5", # Enterprise cert only works for OpenSSL 1.1.1. Newer versions of these # dependencies are built with OpenSSL 3.0 so we need to fix the version. diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc index 6ad7ea014..7323421d0 100644 Binary files a/system_tests/secrets.tar.enc and b/system_tests/secrets.tar.enc differ diff --git a/testing/requirements.txt b/testing/requirements.txt index 299c8f2ef..27a0b3cb7 100644 --- a/testing/requirements.txt +++ b/testing/requirements.txt @@ -10,7 +10,7 @@ pytest-localserver pyu2f requests urllib3 -cryptography +cryptography < 39.0.0 responses grpcio # Async Dependencies diff --git a/tests/oauth2/test__client.py b/tests/oauth2/test__client.py index 13c42dc52..b322eefed 100644 --- a/tests/oauth2/test__client.py +++ b/tests/oauth2/test__client.py @@ -99,9 +99,10 @@ def test__can_retry_no_retry_message(response_data): assert not _client._can_retry(http_client.OK, response_data) +@pytest.mark.parametrize("mock_expires_in", [500, "500"]) @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) -def test__parse_expiry(unused_utcnow): - result = _client._parse_expiry({"expires_in": 500}) +def test__parse_expiry(unused_utcnow, mock_expires_in): + result = _client._parse_expiry({"expires_in": mock_expires_in}) assert result == datetime.datetime.min + datetime.timedelta(seconds=500) diff --git a/tests/oauth2/test_service_account.py b/tests/oauth2/test_service_account.py index 4bd194b35..ed281fcfa 100644 --- a/tests/oauth2/test_service_account.py +++ b/tests/oauth2/test_service_account.py @@ -373,7 +373,7 @@ def test_refresh_with_jwt_credentials(self, make_jwt): assert credentials.valid # Assert make_jwt was called - assert make_jwt.called_once() + assert make_jwt.call_count == 1 assert credentials.token == token assert credentials.expiry == expiry diff --git a/tests/test_aws.py b/tests/test_aws.py index d059487f4..400412660 100644 --- a/tests/test_aws.py +++ b/tests/test_aws.py @@ -14,6 +14,7 @@ import datetime import json +import os import mock import pytest # type: ignore @@ -1224,6 +1225,7 @@ def test_retrieve_subject_token_success_temp_creds_no_environment_vars( ) @mock.patch("google.auth._helpers.utcnow") + @mock.patch.dict(os.environ, {}) def test_retrieve_subject_token_success_temp_creds_no_environment_vars_idmsv2( self, utcnow ): @@ -1300,7 +1302,7 @@ def test_retrieve_subject_token_success_temp_creds_no_environment_vars_idmsv2( # Only 3 requests should be sent as the region is cached. assert len(new_request.call_args_list) == 3 - # Assert session token request + # Assert session token request. self.assert_aws_metadata_request_kwargs( request.call_args_list[0][1], IMDSV2_SESSION_TOKEN_URL, @@ -1323,6 +1325,210 @@ def test_retrieve_subject_token_success_temp_creds_no_environment_vars_idmsv2( }, ) + @mock.patch("google.auth._helpers.utcnow") + @mock.patch.dict( + os.environ, + { + environment_vars.AWS_REGION: AWS_REGION, + environment_vars.AWS_ACCESS_KEY_ID: ACCESS_KEY_ID, + }, + ) + def test_retrieve_subject_token_success_temp_creds_environment_vars_missing_secret_access_key_idmsv2( + self, utcnow + ): + utcnow.return_value = datetime.datetime.strptime( + self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ" + ) + request = self.make_mock_request( + role_status=http_client.OK, + role_name=self.AWS_ROLE, + security_credentials_status=http_client.OK, + security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE, + imdsv2_session_token_status=http_client.OK, + imdsv2_session_token_data=self.AWS_IMDSV2_SESSION_TOKEN, + ) + credential_source_token_url = self.CREDENTIAL_SOURCE.copy() + credential_source_token_url[ + "imdsv2_session_token_url" + ] = IMDSV2_SESSION_TOKEN_URL + credentials = self.make_credentials( + credential_source=credential_source_token_url + ) + + subject_token = credentials.retrieve_subject_token(request) + assert subject_token == self.make_serialized_aws_signed_request( + { + "access_key_id": ACCESS_KEY_ID, + "secret_access_key": SECRET_ACCESS_KEY, + "security_token": TOKEN, + } + ) + # Assert session token request. + self.assert_aws_metadata_request_kwargs( + request.call_args_list[0][1], + IMDSV2_SESSION_TOKEN_URL, + {"X-aws-ec2-metadata-token-ttl-seconds": "300"}, + "PUT", + ) + # Assert role request. + self.assert_aws_metadata_request_kwargs( + request.call_args_list[1][1], + SECURITY_CREDS_URL, + {"X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN}, + ) + # Assert security credentials request. + self.assert_aws_metadata_request_kwargs( + request.call_args_list[2][1], + "{}/{}".format(SECURITY_CREDS_URL, self.AWS_ROLE), + { + "Content-Type": "application/json", + "X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN, + }, + ) + + @mock.patch("google.auth._helpers.utcnow") + @mock.patch.dict( + os.environ, + { + environment_vars.AWS_REGION: AWS_REGION, + environment_vars.AWS_SECRET_ACCESS_KEY: SECRET_ACCESS_KEY, + }, + ) + def test_retrieve_subject_token_success_temp_creds_environment_vars_missing_access_key_id_idmsv2( + self, utcnow + ): + utcnow.return_value = datetime.datetime.strptime( + self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ" + ) + request = self.make_mock_request( + role_status=http_client.OK, + role_name=self.AWS_ROLE, + security_credentials_status=http_client.OK, + security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE, + imdsv2_session_token_status=http_client.OK, + imdsv2_session_token_data=self.AWS_IMDSV2_SESSION_TOKEN, + ) + credential_source_token_url = self.CREDENTIAL_SOURCE.copy() + credential_source_token_url[ + "imdsv2_session_token_url" + ] = IMDSV2_SESSION_TOKEN_URL + credentials = self.make_credentials( + credential_source=credential_source_token_url + ) + + subject_token = credentials.retrieve_subject_token(request) + assert subject_token == self.make_serialized_aws_signed_request( + { + "access_key_id": ACCESS_KEY_ID, + "secret_access_key": SECRET_ACCESS_KEY, + "security_token": TOKEN, + } + ) + # Assert session token request. + self.assert_aws_metadata_request_kwargs( + request.call_args_list[0][1], + IMDSV2_SESSION_TOKEN_URL, + {"X-aws-ec2-metadata-token-ttl-seconds": "300"}, + "PUT", + ) + # Assert role request. + self.assert_aws_metadata_request_kwargs( + request.call_args_list[1][1], + SECURITY_CREDS_URL, + {"X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN}, + ) + # Assert security credentials request. + self.assert_aws_metadata_request_kwargs( + request.call_args_list[2][1], + "{}/{}".format(SECURITY_CREDS_URL, self.AWS_ROLE), + { + "Content-Type": "application/json", + "X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN, + }, + ) + + @mock.patch("google.auth._helpers.utcnow") + @mock.patch.dict(os.environ, {environment_vars.AWS_REGION: AWS_REGION}) + def test_retrieve_subject_token_success_temp_creds_environment_vars_missing_creds_idmsv2( + self, utcnow + ): + utcnow.return_value = datetime.datetime.strptime( + self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ" + ) + request = self.make_mock_request( + role_status=http_client.OK, + role_name=self.AWS_ROLE, + security_credentials_status=http_client.OK, + security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE, + imdsv2_session_token_status=http_client.OK, + imdsv2_session_token_data=self.AWS_IMDSV2_SESSION_TOKEN, + ) + credential_source_token_url = self.CREDENTIAL_SOURCE.copy() + credential_source_token_url[ + "imdsv2_session_token_url" + ] = IMDSV2_SESSION_TOKEN_URL + credentials = self.make_credentials( + credential_source=credential_source_token_url + ) + + subject_token = credentials.retrieve_subject_token(request) + assert subject_token == self.make_serialized_aws_signed_request( + { + "access_key_id": ACCESS_KEY_ID, + "secret_access_key": SECRET_ACCESS_KEY, + "security_token": TOKEN, + } + ) + # Assert session token request. + self.assert_aws_metadata_request_kwargs( + request.call_args_list[0][1], + IMDSV2_SESSION_TOKEN_URL, + {"X-aws-ec2-metadata-token-ttl-seconds": "300"}, + "PUT", + ) + # Assert role request. + self.assert_aws_metadata_request_kwargs( + request.call_args_list[1][1], + SECURITY_CREDS_URL, + {"X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN}, + ) + # Assert security credentials request. + self.assert_aws_metadata_request_kwargs( + request.call_args_list[2][1], + "{}/{}".format(SECURITY_CREDS_URL, self.AWS_ROLE), + { + "Content-Type": "application/json", + "X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN, + }, + ) + + @mock.patch("google.auth._helpers.utcnow") + @mock.patch.dict( + os.environ, + { + environment_vars.AWS_REGION: AWS_REGION, + environment_vars.AWS_ACCESS_KEY_ID: ACCESS_KEY_ID, + environment_vars.AWS_SECRET_ACCESS_KEY: SECRET_ACCESS_KEY, + }, + ) + def test_retrieve_subject_token_success_temp_creds_idmsv2(self, utcnow): + utcnow.return_value = datetime.datetime.strptime( + self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ" + ) + request = self.make_mock_request( + role_status=http_client.OK, role_name=self.AWS_ROLE + ) + credential_source_token_url = self.CREDENTIAL_SOURCE.copy() + credential_source_token_url[ + "imdsv2_session_token_url" + ] = IMDSV2_SESSION_TOKEN_URL + credentials = self.make_credentials( + credential_source=credential_source_token_url + ) + + credentials.retrieve_subject_token(request) + assert not request.called + def test_validate_metadata_server_url_if_any(self): aws.Credentials.validate_metadata_server_url_if_any( "http://[fd00:ec2::254]/latest/meta-data/placement/availability-zone", "url" diff --git a/tests/test_external_account_authorized_user.py b/tests/test_external_account_authorized_user.py index c97d087b3..db18450a8 100644 --- a/tests/test_external_account_authorized_user.py +++ b/tests/test_external_account_authorized_user.py @@ -424,7 +424,10 @@ def test_to_json_full_with_strip(self): def test_get_project_id(self): creds = self.make_credentials() - assert creds.get_project_id() is None + request = mock.create_autospec(transport.Request) + + assert creds.get_project_id(request) is None + request.assert_not_called() def test_with_quota_project(self): creds = self.make_credentials(