From a65c1e482a5974cd39da2afc4b91ef1b49dc538c Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Tue, 30 Aug 2022 19:34:02 +0000 Subject: [PATCH 01/36] feat: implementation of pluggable auth interactive mode --- google/auth/pluggable.py | 103 ++++++++++++++++++++++++++++++--------- 1 file changed, 79 insertions(+), 24 deletions(-) diff --git a/google/auth/pluggable.py b/google/auth/pluggable.py index 42f6bcd81..3baff06bf 100644 --- a/google/auth/pluggable.py +++ b/google/auth/pluggable.py @@ -38,6 +38,7 @@ import json import os import subprocess +import sys import time from google.auth import _helpers @@ -105,6 +106,7 @@ def __init__( raise ValueError( "Missing credential_source. The credential_source is not a dict." ) + self._interactive = os.environ.get("GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE") == "1" self._credential_source_executable = credential_source.get("executable") if not self._credential_source_executable: raise ValueError( @@ -116,6 +118,9 @@ def __init__( self._credential_source_executable_timeout_millis = self._credential_source_executable.get( "timeout_millis" ) + self._credential_source_executable_interactive_timeout_millis = self._credential_source_executable.get( + "interactive_timeout_millis" + ) self._credential_source_executable_output_file = self._credential_source_executable.get( "output_file" ) @@ -132,6 +137,20 @@ def __init__( ): raise ValueError("Timeout must be between 5 and 120 seconds.") + if not self._credential_source_executable_interactive_timeout_millis: + self._credential_source_executable_interactive_timeout_millis = ( + 5 * 60 * 1000 + ) + elif ( + self._credential_source_executable_interactive_timeout_millis + < 5 * 60 * 1000 + or self._credential_source_executable_interactive_timeout_millis + > 30 * 60 * 1000 + ): + raise ValueError("Interactive timeout must be between 5 and 30 minutes.") + if self._interactive and not self._credential_source_executable_output_file: + raise ValueError("Output file must be specified in interactive mode") + @_helpers.copy_docstring(external_account.Credentials) def retrieve_subject_token(self, request): env_allow_executables = os.environ.get( @@ -155,6 +174,10 @@ def retrieve_subject_token(self, request): try: # If the cached response is expired, _parse_subject_token will raise an error which will be ignored and we will call the executable again. subject_token = self._parse_subject_token(response) + if ( + self._interactive and "expiration_time" not in response + ): # Always treat missing expiration_time as expired + raise exceptions.RefreshError except ValueError: raise except exceptions.RefreshError: @@ -171,9 +194,10 @@ def retrieve_subject_token(self, request): env = os.environ.copy() env["GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE"] = self._audience env["GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE"] = self._subject_token_type - env[ - "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE" - ] = "0" # Always set to 0 until interactive mode is implemented. + env["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = ( + "1" if self._interactive else "0" + ) # if variable not set, backfill to "0" + if self._service_account_impersonation_url is not None: env[ "GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL" @@ -183,31 +207,61 @@ def retrieve_subject_token(self, request): "GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE" ] = self._credential_source_executable_output_file - try: - result = subprocess.run( - self._credential_source_executable_command.split(), - timeout=self._credential_source_executable_timeout_millis / 1000, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - env=env, - ) - if result.returncode != 0: - raise exceptions.RefreshError( - "Executable exited with non-zero return code {}. Error: {}".format( - result.returncode, result.stdout - ) + if self._interactive: + try: + result = subprocess.run( + self._credential_source_executable_command.split(), + timeout=self._credential_source_executable_interactive_timeout_millis + / 1000, + stdin=sys.stdin, + stdout=sys.stdout, + stderr=sys.stdout, + env=env, ) - except Exception: - raise + if result.returncode != 0: + raise exceptions.RefreshError( + "Executable exited with non-zero return code {}.".format( + result.returncode + ) + ) + except Exception: + raise + else: + try: + with open( + self._credential_source_executable_output_file, encoding="utf-8" + ) as data: + response = json.load(data) + subject_token = self._parse_subject_token(response) + except Exception: + raise + return subject_token + else: try: - data = result.stdout.decode("utf-8") - response = json.loads(data) - subject_token = self._parse_subject_token(response) + result = subprocess.run( + self._credential_source_executable_command.split(), + timeout=self._credential_source_executable_timeout_millis / 1000, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + env=env, + ) + if result.returncode != 0: + raise exceptions.RefreshError( + "Executable exited with non-zero return code {}. Error: {}".format( + result.returncode, result.stdout + ) + ) except Exception: raise - - return subject_token + else: + try: + data = result.stdout.decode("utf-8") + response = json.loads(data) + subject_token = self._parse_subject_token(response) + except Exception: + raise + return subject_token @classmethod def from_info(cls, info, **kwargs): @@ -264,10 +318,11 @@ def _parse_subject_token(self, response): ) if ( "expiration_time" not in response + and not self._interactive and self._credential_source_executable_output_file ): raise ValueError( - "The executable response must contain an expiration_time for successful responses when an output_file has been specified in the configuration." + "The executable response must contain an expiration_time for successful responses when an output_file has been specified in the configuration in non-interactive mode." ) if "expiration_time" in response and response["expiration_time"] < time.time(): raise exceptions.RefreshError( From 6fcce53b52e2700019a5410791d5a4090d80283d Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Wed, 31 Aug 2022 17:21:41 +0000 Subject: [PATCH 02/36] adding test for interactive mode --- google/auth/pluggable.py | 6 +- tests/test_pluggable.py | 240 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 243 insertions(+), 3 deletions(-) diff --git a/google/auth/pluggable.py b/google/auth/pluggable.py index 3baff06bf..6c5af1021 100644 --- a/google/auth/pluggable.py +++ b/google/auth/pluggable.py @@ -176,9 +176,13 @@ def retrieve_subject_token(self, request): subject_token = self._parse_subject_token(response) if ( self._interactive and "expiration_time" not in response - ): # Always treat missing expiration_time as expired + ): # Always treat missing expiration_time as expired and proceed to executable run raise exceptions.RefreshError except ValueError: + if ( + self._interactive + ): # For any interactive mode errors in the latest run, we automatically ignore it and proceed to executable run + pass raise except exceptions.RefreshError: pass diff --git a/tests/test_pluggable.py b/tests/test_pluggable.py index 80cde7972..240f95496 100644 --- a/tests/test_pluggable.py +++ b/tests/test_pluggable.py @@ -57,6 +57,7 @@ class TestCredentials(object): CREDENTIAL_SOURCE_EXECUTABLE = { "command": CREDENTIAL_SOURCE_EXECUTABLE_COMMAND, "timeout_millis": 30000, + "interactive_timeout_millis": 300000, "output_file": CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, } CREDENTIAL_SOURCE = {"executable": CREDENTIAL_SOURCE_EXECUTABLE} @@ -68,6 +69,12 @@ class TestCredentials(object): "id_token": EXECUTABLE_OIDC_TOKEN, "expiration_time": 9999999999, } + EXECUTABLE_SUCCESSFUL_OIDC_NO_EXPIRATION_TIME_RESPONSE_ID_TOKEN = { + "version": 1, + "success": True, + "token_type": "urn:ietf:params:oauth:token-type:id_token", + "id_token": EXECUTABLE_OIDC_TOKEN, + } EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE_JWT = { "version": 1, "success": True, @@ -75,6 +82,12 @@ class TestCredentials(object): "id_token": EXECUTABLE_OIDC_TOKEN, "expiration_time": 9999999999, } + EXECUTABLE_SUCCESSFUL_OIDC_NO_EXPIRATION_TIME_RESPONSE_JWT = { + "version": 1, + "success": True, + "token_type": "urn:ietf:params:oauth:token-type:jwt", + "id_token": EXECUTABLE_OIDC_TOKEN, + } EXECUTABLE_SAML_TOKEN = "FAKE_SAML_RESPONSE" EXECUTABLE_SUCCESSFUL_SAML_RESPONSE = { "version": 1, @@ -83,6 +96,12 @@ class TestCredentials(object): "saml_response": EXECUTABLE_SAML_TOKEN, "expiration_time": 9999999999, } + EXECUTABLE_SUCCESSFUL_SAML_NO_EXPIRATION_TIME_RESPONSE = { + "version": 1, + "success": True, + "token_type": "urn:ietf:params:oauth:token-type:saml2", + "saml_response": EXECUTABLE_SAML_TOKEN, + } EXECUTABLE_FAILED_RESPONSE = { "version": 1, "success": False, @@ -294,6 +313,48 @@ def test_retrieve_subject_token_oidc_id_token(self): assert subject_token == self.EXECUTABLE_OIDC_TOKEN + @mock.patch.dict( + os.environ, + { + "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1", + "GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE": "original_audience", + "GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE": "original_token_type", + "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE": "1", + "GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL": "original_impersonated_email", + "GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE": "original_output_file", + }, + ) + def test_retrieve_subject_token_oidc_id_token_interactive_mode(self, tmpdir): + + ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE = tmpdir.join( + "actual_output_file" + ) + ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE = { + "command": "command", + "interactive_timeout_millis": 300000, + "output_file": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, + } + ACTUAL_CREDENTIAL_SOURCE = {"executable": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE} + with open(ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, "w") as output_file: + json.dump( + self.EXECUTABLE_SUCCESSFUL_OIDC_NO_EXPIRATION_TIME_RESPONSE_JWT, + output_file, + ) + + with mock.patch( + "subprocess.run", + return_value=subprocess.CompletedProcess(args=[], returncode=0), + ): + credentials = self.make_pluggable( + audience=AUDIENCE, + service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, + credential_source=ACTUAL_CREDENTIAL_SOURCE, + ) + + subject_token = credentials.retrieve_subject_token(None) + + assert subject_token == self.EXECUTABLE_OIDC_TOKEN + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) def test_retrieve_subject_token_oidc_jwt(self): with mock.patch( @@ -316,6 +377,44 @@ def test_retrieve_subject_token_oidc_jwt(self): assert subject_token == self.EXECUTABLE_OIDC_TOKEN + @mock.patch.dict( + os.environ, + { + "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1", + "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE": "1", + }, + ) + def test_retrieve_subject_token_oidc_jwt_interactive_mode(self, tmpdir): + ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE = tmpdir.join( + "actual_output_file" + ) + ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE = { + "command": "command", + "interactive_timeout_millis": 300000, + "output_file": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, + } + ACTUAL_CREDENTIAL_SOURCE = {"executable": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE} + with open(ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, "w") as output_file: + json.dump( + self.EXECUTABLE_SUCCESSFUL_OIDC_NO_EXPIRATION_TIME_RESPONSE_JWT, + output_file, + ) + + with mock.patch( + "subprocess.run", + return_value=subprocess.CompletedProcess(args=[], returncode=0), + ): + credentials = self.make_pluggable( + audience=AUDIENCE, + service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, + credential_source=ACTUAL_CREDENTIAL_SOURCE, + ) + + subject_token = credentials.retrieve_subject_token(None) + + assert subject_token == self.EXECUTABLE_OIDC_TOKEN + os.remove(ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE) + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) def test_retrieve_subject_token_saml(self): with mock.patch( @@ -334,6 +433,42 @@ def test_retrieve_subject_token_saml(self): assert subject_token == self.EXECUTABLE_SAML_TOKEN + @mock.patch.dict( + os.environ, + { + "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1", + "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE": "1", + }, + ) + def test_retrieve_subject_token_saml_interactive_mode(self, tmpdir): + + ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE = tmpdir.join( + "actual_output_file" + ) + ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE = { + "command": "command", + "interactive_timeout_millis": 300000, + "output_file": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, + } + ACTUAL_CREDENTIAL_SOURCE = {"executable": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE} + with open(ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, "w") as output_file: + json.dump( + self.EXECUTABLE_SUCCESSFUL_SAML_NO_EXPIRATION_TIME_RESPONSE, output_file + ) + + with mock.patch( + "subprocess.run", + return_value=subprocess.CompletedProcess(args=[], returncode=0), + ): + credentials = self.make_pluggable( + credential_source=ACTUAL_CREDENTIAL_SOURCE + ) + + subject_token = credentials.retrieve_subject_token(None) + + assert subject_token == self.EXECUTABLE_SAML_TOKEN + os.remove(ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE) + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) def test_retrieve_subject_token_failed(self): with mock.patch( @@ -353,6 +488,42 @@ def test_retrieve_subject_token_failed(self): r"Executable returned unsuccessful response: code: 401, message: Permission denied. Caller not authorized." ) + @mock.patch.dict( + os.environ, + { + "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1", + "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE": "1", + }, + ) + def test_retrieve_subject_token_failed_interactive_mode(self, tmpdir): + ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE = tmpdir.join( + "actual_output_file" + ) + ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE = { + "command": "command", + "interactive_timeout_millis": 300000, + "output_file": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, + } + ACTUAL_CREDENTIAL_SOURCE = {"executable": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE} + with open(ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, "w") as output_file: + json.dump(self.EXECUTABLE_FAILED_RESPONSE, output_file) + + with mock.patch( + "subprocess.run", + return_value=subprocess.CompletedProcess(args=[], returncode=0), + ): + credentials = self.make_pluggable( + credential_source=ACTUAL_CREDENTIAL_SOURCE + ) + + with pytest.raises(exceptions.RefreshError) as excinfo: + _ = credentials.retrieve_subject_token(None) + + assert excinfo.match( + r"Executable returned unsuccessful response: code: 401, message: Permission denied. Caller not authorized." + ) + os.remove(ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE) + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "0"}) def test_retrieve_subject_token_not_allowd(self): with mock.patch( @@ -642,7 +813,7 @@ def test_retrieve_subject_token_missing_error_code_message(self): ) @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) - def test_retrieve_subject_token_without_expiration_time_should_fail_when_output_file_specified( + def test_retrieve_subject_token_without_expiration_time_should_fail_when_output_file_specified_non_interactive_mode( self, ): EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE = { @@ -670,7 +841,7 @@ def test_retrieve_subject_token_without_expiration_time_should_fail_when_output_ ) @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) - def test_retrieve_subject_token_without_expiration_time_should_fail_when_retrieving_from_output_file( + def test_retrieve_subject_token_without_expiration_time_should_fail_when_retrieving_from_output_file_non_interactive_mode( self, tmpdir ): ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE = tmpdir.join( @@ -767,6 +938,22 @@ def test_credential_source_missing_command(self): r"Missing command field. Executable command must be provided." ) + @mock.patch.dict( + os.environ, + { + "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1", + "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE": "1", + }, + ) + def test_credential_source_missing_output_interactive_mode(self): + with pytest.raises(ValueError) as excinfo: + CREDENTIAL_SOURCE = { + "executable": {"command": self.CREDENTIAL_SOURCE_EXECUTABLE_COMMAND} + } + _ = self.make_pluggable(credential_source=CREDENTIAL_SOURCE) + + assert excinfo.match(r"Output file must be specified in interactive mode") + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) def test_credential_source_timeout_small(self): with pytest.raises(ValueError) as excinfo: @@ -795,6 +982,34 @@ def test_credential_source_timeout_large(self): assert excinfo.match(r"Timeout must be between 5 and 120 seconds.") + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) + def test_credential_source_interactive_timeout_small(self): + with pytest.raises(ValueError) as excinfo: + CREDENTIAL_SOURCE = { + "executable": { + "command": self.CREDENTIAL_SOURCE_EXECUTABLE_COMMAND, + "interactive_timeout_millis": 30000 - 1, + "output_file": self.CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, + } + } + _ = self.make_pluggable(credential_source=CREDENTIAL_SOURCE) + + assert excinfo.match(r"Interactive timeout must be between 5 and 30 minutes.") + + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) + def test_credential_source_interactive_timeout_large(self): + with pytest.raises(ValueError) as excinfo: + CREDENTIAL_SOURCE = { + "executable": { + "command": self.CREDENTIAL_SOURCE_EXECUTABLE_COMMAND, + "interactive_timeout_millis": 1800000 + 1, + "output_file": self.CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, + } + } + _ = self.make_pluggable(credential_source=CREDENTIAL_SOURCE) + + assert excinfo.match(r"Interactive timeout must be between 5 and 30 minutes.") + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) def test_retrieve_subject_token_executable_fail(self): with mock.patch( @@ -812,6 +1027,27 @@ def test_retrieve_subject_token_executable_fail(self): r"Executable exited with non-zero return code 1. Error: None" ) + @mock.patch.dict( + os.environ, + { + "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1", + "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE": "1", + }, + ) + def test_retrieve_subject_token_executable_fail_interactive_mode(self): + with mock.patch( + "subprocess.run", + return_value=subprocess.CompletedProcess( + args=[], stdout=None, returncode=1 + ), + ): + credentials = self.make_pluggable(credential_source=self.CREDENTIAL_SOURCE) + + with pytest.raises(exceptions.RefreshError) as excinfo: + _ = credentials.retrieve_subject_token(None) + + assert excinfo.match(r"Executable exited with non-zero return code 1.") + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) def test_retrieve_subject_token_python_2(self): with mock.patch("sys.version_info", (2, 7)): From 3ea579961dbf92ca0743fd1caf62b03459ce6006 Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Fri, 2 Sep 2022 17:27:18 +0000 Subject: [PATCH 03/36] addressing comments --- google/auth/pluggable.py | 103 ++++++++++++++------------------------- tests/test_pluggable.py | 24 +++------ 2 files changed, 42 insertions(+), 85 deletions(-) diff --git a/google/auth/pluggable.py b/google/auth/pluggable.py index 6c5af1021..0c9259f5c 100644 --- a/google/auth/pluggable.py +++ b/google/auth/pluggable.py @@ -106,7 +106,6 @@ def __init__( raise ValueError( "Missing credential_source. The credential_source is not a dict." ) - self._interactive = os.environ.get("GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE") == "1" self._credential_source_executable = credential_source.get("executable") if not self._credential_source_executable: raise ValueError( @@ -148,8 +147,6 @@ def __init__( > 30 * 60 * 1000 ): raise ValueError("Interactive timeout must be between 5 and 30 minutes.") - if self._interactive and not self._credential_source_executable_output_file: - raise ValueError("Output file must be specified in interactive mode") @_helpers.copy_docstring(external_account.Credentials) def retrieve_subject_token(self, request): @@ -160,12 +157,13 @@ def retrieve_subject_token(self, request): raise ValueError( "Executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') to run." ) + interactive_mode = os.environ.get("GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE") == "1" # Check output file. if self._credential_source_executable_output_file is not None: try: with open( - self._credential_source_executable_output_file + self._credential_source_executable_output_file, encoding="utf-8" ) as output_file: response = json.load(output_file) except Exception: @@ -175,14 +173,10 @@ def retrieve_subject_token(self, request): # If the cached response is expired, _parse_subject_token will raise an error which will be ignored and we will call the executable again. subject_token = self._parse_subject_token(response) if ( - self._interactive and "expiration_time" not in response - ): # Always treat missing expiration_time as expired and proceed to executable run + interactive_mode and "expiration_time" not in response + ): # Always treat missing expiration_time as expired and proceed to executable run. raise exceptions.RefreshError except ValueError: - if ( - self._interactive - ): # For any interactive mode errors in the latest run, we automatically ignore it and proceed to executable run - pass raise except exceptions.RefreshError: pass @@ -198,9 +192,6 @@ def retrieve_subject_token(self, request): env = os.environ.copy() env["GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE"] = self._audience env["GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE"] = self._subject_token_type - env["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = ( - "1" if self._interactive else "0" - ) # if variable not set, backfill to "0" if self._service_account_impersonation_url is not None: env[ @@ -211,61 +202,39 @@ def retrieve_subject_token(self, request): "GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE" ] = self._credential_source_executable_output_file - if self._interactive: - try: - result = subprocess.run( - self._credential_source_executable_command.split(), - timeout=self._credential_source_executable_interactive_timeout_millis - / 1000, - stdin=sys.stdin, - stdout=sys.stdout, - stderr=sys.stdout, - env=env, - ) - if result.returncode != 0: - raise exceptions.RefreshError( - "Executable exited with non-zero return code {}.".format( - result.returncode - ) - ) - except Exception: - raise - else: - try: - with open( - self._credential_source_executable_output_file, encoding="utf-8" - ) as data: - response = json.load(data) - subject_token = self._parse_subject_token(response) - except Exception: - raise - return subject_token + exe_timeout = ( + self._credential_source_executable_interactive_timeout_millis / 1000 + if interactive_mode + else self._credential_source_executable_timeout_millis / 1000 + ) + exe_stdin = sys.stdin if interactive_mode else None + exe_stdout = sys.stdout if interactive_mode else subprocess.PIPE + exe_stderr = sys.stdout if interactive_mode else subprocess.STDOUT - else: - try: - result = subprocess.run( - self._credential_source_executable_command.split(), - timeout=self._credential_source_executable_timeout_millis / 1000, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - env=env, + result = subprocess.run( + self._credential_source_executable_command.split(), + timeout=exe_timeout / 1000, + stdin=exe_stdin, + stdout=exe_stdout, + stderr=exe_stderr, + env=env, + ) + if result.returncode != 0: + raise exceptions.RefreshError( + "Executable exited with non-zero return code {}. Error: {}".format( + result.returncode, result.stdout ) - if result.returncode != 0: - raise exceptions.RefreshError( - "Executable exited with non-zero return code {}. Error: {}".format( - result.returncode, result.stdout - ) - ) - except Exception: - raise - else: - try: - data = result.stdout.decode("utf-8") - response = json.loads(data) - subject_token = self._parse_subject_token(response) - except Exception: - raise - return subject_token + ) + + response = ( + json.load( + open(self._credential_source_executable_output_file, encoding="utf-8") + ) + if interactive_mode + else json.loads(result.stdout.decode("utf-8")) + ) + subject_token = self._parse_subject_token(response) + return subject_token @classmethod def from_info(cls, info, **kwargs): @@ -322,7 +291,7 @@ def _parse_subject_token(self, response): ) if ( "expiration_time" not in response - and not self._interactive + and not os.environ.get("GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE") == "1" and self._credential_source_executable_output_file ): raise ValueError( diff --git a/tests/test_pluggable.py b/tests/test_pluggable.py index 240f95496..588666d4a 100644 --- a/tests/test_pluggable.py +++ b/tests/test_pluggable.py @@ -505,7 +505,9 @@ def test_retrieve_subject_token_failed_interactive_mode(self, tmpdir): "output_file": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, } ACTUAL_CREDENTIAL_SOURCE = {"executable": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE} - with open(ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, "w") as output_file: + with open( + ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, "w", encoding="utf-8" + ) as output_file: json.dump(self.EXECUTABLE_FAILED_RESPONSE, output_file) with mock.patch( @@ -938,22 +940,6 @@ def test_credential_source_missing_command(self): r"Missing command field. Executable command must be provided." ) - @mock.patch.dict( - os.environ, - { - "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1", - "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE": "1", - }, - ) - def test_credential_source_missing_output_interactive_mode(self): - with pytest.raises(ValueError) as excinfo: - CREDENTIAL_SOURCE = { - "executable": {"command": self.CREDENTIAL_SOURCE_EXECUTABLE_COMMAND} - } - _ = self.make_pluggable(credential_source=CREDENTIAL_SOURCE) - - assert excinfo.match(r"Output file must be specified in interactive mode") - @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) def test_credential_source_timeout_small(self): with pytest.raises(ValueError) as excinfo: @@ -1046,7 +1032,9 @@ def test_retrieve_subject_token_executable_fail_interactive_mode(self): with pytest.raises(exceptions.RefreshError) as excinfo: _ = credentials.retrieve_subject_token(None) - assert excinfo.match(r"Executable exited with non-zero return code 1.") + assert excinfo.match( + r"Executable exited with non-zero return code 1. Error: None" + ) @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) def test_retrieve_subject_token_python_2(self): From 9eec181229dc2f52b90eb1858a84c1393dc6ae24 Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Tue, 6 Sep 2022 22:13:27 +0000 Subject: [PATCH 04/36] move interactive source of truth to kwargs in constructor and make check in the retrieve_subject_token --- google/auth/pluggable.py | 26 +++++++++----- tests/test_pluggable.py | 76 ++++++++++++++++++++++++++-------------- 2 files changed, 66 insertions(+), 36 deletions(-) diff --git a/google/auth/pluggable.py b/google/auth/pluggable.py index 0c9259f5c..d5788da08 100644 --- a/google/auth/pluggable.py +++ b/google/auth/pluggable.py @@ -93,6 +93,7 @@ def __init__( :meth:`from_info` are used instead of calling the constructor directly. """ + self.interactive = kwargs.pop("interactive", False) super(Credentials, self).__init__( audience=audience, subject_token_type=subject_token_type, @@ -157,7 +158,13 @@ def retrieve_subject_token(self, request): raise ValueError( "Executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') to run." ) - interactive_mode = os.environ.get("GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE") == "1" + if self.interactive and not self._credential_source_executable_output_file: + raise ValueError( + "An output_file must be specified in the credential configuration for interactive mode." + ) + + if self.interactive and not self.is_workforce_pool: + raise ValueError("Interactive mode is only enabled for workforce pool.") # Check output file. if self._credential_source_executable_output_file is not None: @@ -173,7 +180,7 @@ def retrieve_subject_token(self, request): # If the cached response is expired, _parse_subject_token will raise an error which will be ignored and we will call the executable again. subject_token = self._parse_subject_token(response) if ( - interactive_mode and "expiration_time" not in response + self.interactive and "expiration_time" not in response ): # Always treat missing expiration_time as expired and proceed to executable run. raise exceptions.RefreshError except ValueError: @@ -192,6 +199,7 @@ def retrieve_subject_token(self, request): env = os.environ.copy() env["GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE"] = self._audience env["GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE"] = self._subject_token_type + env["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = "1" if self.interactive else "0" if self._service_account_impersonation_url is not None: env[ @@ -204,16 +212,16 @@ def retrieve_subject_token(self, request): exe_timeout = ( self._credential_source_executable_interactive_timeout_millis / 1000 - if interactive_mode + if self.interactive else self._credential_source_executable_timeout_millis / 1000 ) - exe_stdin = sys.stdin if interactive_mode else None - exe_stdout = sys.stdout if interactive_mode else subprocess.PIPE - exe_stderr = sys.stdout if interactive_mode else subprocess.STDOUT + exe_stdin = sys.stdin if self.interactive else None + exe_stdout = sys.stdout if self.interactive else subprocess.PIPE + exe_stderr = sys.stdout if self.interactive else subprocess.STDOUT result = subprocess.run( self._credential_source_executable_command.split(), - timeout=exe_timeout / 1000, + timeout=exe_timeout, stdin=exe_stdin, stdout=exe_stdout, stderr=exe_stderr, @@ -230,7 +238,7 @@ def retrieve_subject_token(self, request): json.load( open(self._credential_source_executable_output_file, encoding="utf-8") ) - if interactive_mode + if self.interactive else json.loads(result.stdout.decode("utf-8")) ) subject_token = self._parse_subject_token(response) @@ -291,7 +299,7 @@ def _parse_subject_token(self, response): ) if ( "expiration_time" not in response - and not os.environ.get("GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE") == "1" + and not self.interactive and self._credential_source_executable_output_file ): raise ValueError( diff --git a/tests/test_pluggable.py b/tests/test_pluggable.py index 588666d4a..00564d731 100644 --- a/tests/test_pluggable.py +++ b/tests/test_pluggable.py @@ -24,8 +24,9 @@ # from six.moves import urllib # from google.auth import _helpers -from google.auth import exceptions +from google.auth import credentials, exceptions from google.auth import pluggable +from tests.test__default import WORKFORCE_AUDIENCE # from google.auth import transport @@ -123,6 +124,7 @@ def make_pluggable( service_account_impersonation_url=None, credential_source=None, workforce_pool_user_project=None, + interactive=None, ): return pluggable.Credentials( audience=audience, @@ -136,6 +138,7 @@ def make_pluggable( scopes=scopes, default_scopes=default_scopes, workforce_pool_user_project=workforce_pool_user_project, + interactive=interactive, ) @mock.patch.object(pluggable.Credentials, "__init__", return_value=None) @@ -346,9 +349,10 @@ def test_retrieve_subject_token_oidc_id_token_interactive_mode(self, tmpdir): return_value=subprocess.CompletedProcess(args=[], returncode=0), ): credentials = self.make_pluggable( - audience=AUDIENCE, + audience=WORKFORCE_AUDIENCE, service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, credential_source=ACTUAL_CREDENTIAL_SOURCE, + interactive=True, ) subject_token = credentials.retrieve_subject_token(None) @@ -377,13 +381,7 @@ def test_retrieve_subject_token_oidc_jwt(self): assert subject_token == self.EXECUTABLE_OIDC_TOKEN - @mock.patch.dict( - os.environ, - { - "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1", - "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE": "1", - }, - ) + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) def test_retrieve_subject_token_oidc_jwt_interactive_mode(self, tmpdir): ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE = tmpdir.join( "actual_output_file" @@ -405,9 +403,10 @@ def test_retrieve_subject_token_oidc_jwt_interactive_mode(self, tmpdir): return_value=subprocess.CompletedProcess(args=[], returncode=0), ): credentials = self.make_pluggable( - audience=AUDIENCE, + audience=WORKFORCE_AUDIENCE, service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, credential_source=ACTUAL_CREDENTIAL_SOURCE, + interactive=True, ) subject_token = credentials.retrieve_subject_token(None) @@ -433,13 +432,7 @@ def test_retrieve_subject_token_saml(self): assert subject_token == self.EXECUTABLE_SAML_TOKEN - @mock.patch.dict( - os.environ, - { - "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1", - "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE": "1", - }, - ) + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) def test_retrieve_subject_token_saml_interactive_mode(self, tmpdir): ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE = tmpdir.join( @@ -461,7 +454,9 @@ def test_retrieve_subject_token_saml_interactive_mode(self, tmpdir): return_value=subprocess.CompletedProcess(args=[], returncode=0), ): credentials = self.make_pluggable( - credential_source=ACTUAL_CREDENTIAL_SOURCE + audience=WORKFORCE_AUDIENCE, + credential_source=ACTUAL_CREDENTIAL_SOURCE, + interactive=True, ) subject_token = credentials.retrieve_subject_token(None) @@ -515,7 +510,9 @@ def test_retrieve_subject_token_failed_interactive_mode(self, tmpdir): return_value=subprocess.CompletedProcess(args=[], returncode=0), ): credentials = self.make_pluggable( - credential_source=ACTUAL_CREDENTIAL_SOURCE + audience=WORKFORCE_AUDIENCE, + credential_source=ACTUAL_CREDENTIAL_SOURCE, + interactive=True, ) with pytest.raises(exceptions.RefreshError) as excinfo: @@ -940,6 +937,21 @@ def test_credential_source_missing_command(self): r"Missing command field. Executable command must be provided." ) + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) + def test_credential_source_missing_output_interactive_mode(self): + CREDENTIAL_SOURCE = { + "executable": {"command": self.CREDENTIAL_SOURCE_EXECUTABLE_COMMAND} + } + credentials = self.make_pluggable( + credential_source=CREDENTIAL_SOURCE, interactive=True + ) + with pytest.raises(ValueError) as excinfo: + _ = credentials.retrieve_subject_token(None) + + assert excinfo.match( + r"An output_file must be specified in the credential configuration for interactive mode." + ) + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) def test_credential_source_timeout_small(self): with pytest.raises(ValueError) as excinfo: @@ -1013,13 +1025,19 @@ def test_retrieve_subject_token_executable_fail(self): r"Executable exited with non-zero return code 1. Error: None" ) - @mock.patch.dict( - os.environ, - { - "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1", - "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE": "1", - }, - ) + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) + def test_retrieve_subject_token_non_workforce_fail_interactive_mode(self): + credentials = self.make_pluggable( + credential_source=self.CREDENTIAL_SOURCE, interactive=True + ) + with pytest.raises(ValueError) as excinfo: + _ = credentials.retrieve_subject_token(None) + + assert excinfo.match( + r"Interactive mode is only enabled for workforce pool." + ) + + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) def test_retrieve_subject_token_executable_fail_interactive_mode(self): with mock.patch( "subprocess.run", @@ -1027,7 +1045,11 @@ def test_retrieve_subject_token_executable_fail_interactive_mode(self): args=[], stdout=None, returncode=1 ), ): - credentials = self.make_pluggable(credential_source=self.CREDENTIAL_SOURCE) + credentials = self.make_pluggable( + audience=WORKFORCE_AUDIENCE, + credential_source=self.CREDENTIAL_SOURCE, + interactive=True, + ) with pytest.raises(exceptions.RefreshError) as excinfo: _ = credentials.retrieve_subject_token(None) From 97fb34a1dab76f6a46aa6721c8c2ecba96f5f139 Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Tue, 6 Sep 2022 23:33:39 +0000 Subject: [PATCH 05/36] implement revoke method and refactor some constants --- google/auth/pluggable.py | 65 ++++++++++++++++++++++++++++++++++++---- tests/test_pluggable.py | 49 ++++++++++++++++++++++++++++-- 2 files changed, 105 insertions(+), 9 deletions(-) diff --git a/google/auth/pluggable.py b/google/auth/pluggable.py index d5788da08..c0415a5cb 100644 --- a/google/auth/pluggable.py +++ b/google/auth/pluggable.py @@ -48,6 +48,14 @@ # The max supported executable spec version. EXECUTABLE_SUPPORTED_MAX_VERSION = 1 +EXECUTABLE_TIMEOUT_MILLIS_DEFAULT = 30 * 1000 # 30 seconds +EXECUTABLE_TIMEOUT_MILLIS_LOWER_BOUND = 5 * 1000 # 5 seconds +EXECUTABLE_TIMEOUT_MILLIS_UPPER_BOUND = 120 * 1000 # 2 minutes + +EXECUTABLE_INTERACTIVE_TIMEOUT_MILLIS_DEFAULT = 5 * 60 * 1000 # 5 minutes +EXECUTABLE_INTERACTIVE_TIMEOUT_MILLIS_LOWER_BOUND = 5 * 60 * 1000 # 5 minutes +EXECUTABLE_INTERACTIVE_TIMEOUT_MILLIS_UPPER_BOUND = 30 * 60 * 1000 # 30 minutes + class Credentials(external_account.Credentials): """External account credentials sourced from executables.""" @@ -130,22 +138,26 @@ def __init__( "Missing command field. Executable command must be provided." ) if not self._credential_source_executable_timeout_millis: - self._credential_source_executable_timeout_millis = 30 * 1000 + self._credential_source_executable_timeout_millis = ( + EXECUTABLE_TIMEOUT_MILLIS_DEFAULT + ) elif ( - self._credential_source_executable_timeout_millis < 5 * 1000 - or self._credential_source_executable_timeout_millis > 120 * 1000 + self._credential_source_executable_timeout_millis + < EXECUTABLE_TIMEOUT_MILLIS_LOWER_BOUND + or self._credential_source_executable_timeout_millis + > EXECUTABLE_TIMEOUT_MILLIS_UPPER_BOUND ): raise ValueError("Timeout must be between 5 and 120 seconds.") if not self._credential_source_executable_interactive_timeout_millis: self._credential_source_executable_interactive_timeout_millis = ( - 5 * 60 * 1000 + EXECUTABLE_INTERACTIVE_TIMEOUT_MILLIS_DEFAULT ) elif ( self._credential_source_executable_interactive_timeout_millis - < 5 * 60 * 1000 + < EXECUTABLE_INTERACTIVE_TIMEOUT_MILLIS_LOWER_BOUND or self._credential_source_executable_interactive_timeout_millis - > 30 * 60 * 1000 + > EXECUTABLE_INTERACTIVE_TIMEOUT_MILLIS_UPPER_BOUND ): raise ValueError("Interactive timeout must be between 5 and 30 minutes.") @@ -244,6 +256,47 @@ def retrieve_subject_token(self, request): subject_token = self._parse_subject_token(response) return subject_token + def revoke(self, request): + """Revokes the subject token using the credential_source object. + + Args: + request (google.auth.transport.Request): A callable used to make + HTTP requests. + Raises: + google.auth.exceptions.RefreshError: If the executable revocation + not properly executed. + + """ + env_allow_executables = os.environ.get( + "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES" + ) + if env_allow_executables != "1": + raise ValueError( + "Executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') to run." + ) + + if not self.interactive: + raise ValueError("Revoke is only enabled under interactive mode.") + + # Inject variables + env = os.environ.copy() + env["GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE"] = self._audience + env["GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE"] = self._subject_token_type + env["GOOGLE_EXTERNAL_ACCOUNT_REVOKE"] = "1" + env["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = "1" + + result = subprocess.run( + self._credential_source_executable_command.split(), + timeout=self._credential_source_executable_interactive_timeout_millis + / 1000, + env=env, + ) + + if result.returncode != 0: + raise exceptions.RefreshError("Auth revoke failed on executable.") + + # TODO: clear cache when the in memory cache feature implemented. + @classmethod def from_info(cls, info, **kwargs): """Creates a Pluggable Credentials instance from parsed external account info. diff --git a/tests/test_pluggable.py b/tests/test_pluggable.py index 00564d731..8a7697ef7 100644 --- a/tests/test_pluggable.py +++ b/tests/test_pluggable.py @@ -1033,9 +1033,7 @@ def test_retrieve_subject_token_non_workforce_fail_interactive_mode(self): with pytest.raises(ValueError) as excinfo: _ = credentials.retrieve_subject_token(None) - assert excinfo.match( - r"Interactive mode is only enabled for workforce pool." - ) + assert excinfo.match(r"Interactive mode is only enabled for workforce pool.") @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) def test_retrieve_subject_token_executable_fail_interactive_mode(self): @@ -1058,6 +1056,51 @@ def test_retrieve_subject_token_executable_fail_interactive_mode(self): r"Executable exited with non-zero return code 1. Error: None" ) + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "0"}) + def test_revoke_failed_executable_not_allowed(self): + credentials = self.make_pluggable(credential_source=self.CREDENTIAL_SOURCE) + with pytest.raises(ValueError) as excinfo: + _ = credentials.revoke(None) + + assert excinfo.match(r"Executables need to be explicitly allowed") + + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) + def test_revoke_failed_non_interactive_mode(self): + credentials = self.make_pluggable(credential_source=self.CREDENTIAL_SOURCE) + with pytest.raises(ValueError) as excinfo: + _ = credentials.revoke(None) + + assert excinfo.match(r"Revoke is only enabled under interactive mode.") + + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) + def test_revoke_failed_executable(self): + with mock.patch( + "subprocess.run", + return_value=subprocess.CompletedProcess( + args=[], stdout=None, returncode=1 + ), + ): + credentials = self.make_pluggable( + credential_source=self.CREDENTIAL_SOURCE, interactive=True + ) + with pytest.raises(exceptions.RefreshError) as excinfo: + _ = credentials.revoke(None) + + assert excinfo.match(r"Auth revoke failed on executable.") + + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) + def test_revoke_successfully(self): + with mock.patch( + "subprocess.run", + return_value=subprocess.CompletedProcess( + args=[], stdout=None, returncode=0 + ), + ): + credentials = self.make_pluggable( + credential_source=self.CREDENTIAL_SOURCE, interactive=True + ) + _ = credentials.revoke(None) + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) def test_retrieve_subject_token_python_2(self): with mock.patch("sys.version_info", (2, 7)): From 95b6ce66964ce1487c4b4733500439c3447a22b6 Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Wed, 7 Sep 2022 00:15:32 +0000 Subject: [PATCH 06/36] adding 2.7 check in revoke() --- google/auth/pluggable.py | 6 ++++++ tests/test_pluggable.py | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/google/auth/pluggable.py b/google/auth/pluggable.py index c0415a5cb..9fc96278e 100644 --- a/google/auth/pluggable.py +++ b/google/auth/pluggable.py @@ -278,10 +278,16 @@ def revoke(self, request): if not self.interactive: raise ValueError("Revoke is only enabled under interactive mode.") + if not _helpers.is_python_3(): + raise exceptions.RefreshError( + "Pluggable auth is only supported for python 3.6+" + ) + # Inject variables env = os.environ.copy() env["GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE"] = self._audience env["GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE"] = self._subject_token_type + env["GOOGLE_EXTERNAL_ACCOUNT_ID"] = self.service_account_email env["GOOGLE_EXTERNAL_ACCOUNT_REVOKE"] = "1" env["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = "1" diff --git a/tests/test_pluggable.py b/tests/test_pluggable.py index 8a7697ef7..a239f9ca9 100644 --- a/tests/test_pluggable.py +++ b/tests/test_pluggable.py @@ -1110,3 +1110,17 @@ def test_retrieve_subject_token_python_2(self): _ = credentials.retrieve_subject_token(None) assert excinfo.match(r"Pluggable auth is only supported for python 3.6+") + + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) + def test_revoke_subject_token_python_2(self): + with mock.patch("sys.version_info", (2, 7)): + credentials = self.make_pluggable( + audience=WORKFORCE_AUDIENCE, + credential_source=self.CREDENTIAL_SOURCE, + interactive=True, + ) + + with pytest.raises(exceptions.RefreshError) as excinfo: + _ = credentials.revoke(None) + + assert excinfo.match(r"Pluggable auth is only supported for python 3.6+") From 36b770996a6aa9e1fa6a83e10854dd31efdd6e88 Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Wed, 7 Sep 2022 17:37:13 +0000 Subject: [PATCH 07/36] fix lint --- google/auth/pluggable.py | 2 +- tests/test_pluggable.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/google/auth/pluggable.py b/google/auth/pluggable.py index 9fc96278e..2d8719980 100644 --- a/google/auth/pluggable.py +++ b/google/auth/pluggable.py @@ -265,7 +265,7 @@ def revoke(self, request): Raises: google.auth.exceptions.RefreshError: If the executable revocation not properly executed. - + """ env_allow_executables = os.environ.get( "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES" diff --git a/tests/test_pluggable.py b/tests/test_pluggable.py index a239f9ca9..7b60ea8b2 100644 --- a/tests/test_pluggable.py +++ b/tests/test_pluggable.py @@ -24,7 +24,7 @@ # from six.moves import urllib # from google.auth import _helpers -from google.auth import credentials, exceptions +from google.auth import exceptions from google.auth import pluggable from tests.test__default import WORKFORCE_AUDIENCE From 16504927ad9eaefc959c4c828012ab373785d18d Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Wed, 7 Sep 2022 18:15:49 +0000 Subject: [PATCH 08/36] chore: update token --- system_tests/secrets.tar.enc | Bin 10324 -> 10324 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc index 30a208ef5e1e6a09e1110541a05998d1db2559d6..e2b0f9afd2213641a93b60559b903c2da601cbc1 100644 GIT binary patch literal 10324 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 From 27950766abeb7652e73fc2364019a7be85c67da4 Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Wed, 7 Sep 2022 20:08:45 +0000 Subject: [PATCH 09/36] include error return code in exception --- google/auth/pluggable.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/google/auth/pluggable.py b/google/auth/pluggable.py index 2d8719980..b5e2e12ff 100644 --- a/google/auth/pluggable.py +++ b/google/auth/pluggable.py @@ -299,7 +299,11 @@ def revoke(self, request): ) if result.returncode != 0: - raise exceptions.RefreshError("Auth revoke failed on executable.") + raise exceptions.RefreshError( + "Auth revoke failed on executable. Exit with non-zero return code {}".format( + result.returncode + ) + ) # TODO: clear cache when the in memory cache feature implemented. From f361340defa2f00fe2fb33a58fa50ff908e731ff Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Thu, 8 Sep 2022 17:31:12 +0000 Subject: [PATCH 10/36] unify the expiration_time check behavior. --- docs/user-guide.rst | 3 +- google/auth/pluggable.py | 8 ------ tests/test_pluggable.py | 59 +--------------------------------------- 3 files changed, 3 insertions(+), 67 deletions(-) diff --git a/docs/user-guide.rst b/docs/user-guide.rst index e689b11c6..685a6cb70 100644 --- a/docs/user-guide.rst +++ b/docs/user-guide.rst @@ -450,7 +450,8 @@ Response format fields summary: All response types must include both the ``version`` and ``success`` fields. Successful responses must include the ``token_type``, and one of ``id_token`` or ``saml_response``. -If output file is specified, ``expiration_time`` is mandatory. +``expiration_time`` is optional. Missing ``expiration_time`` while retrieving +from output file will be treat as "expired" and proceed to run the executable. Error responses must include both the ``code`` and ``message`` fields. The library will populate the following environment variables when the diff --git a/google/auth/pluggable.py b/google/auth/pluggable.py index b5e2e12ff..47c786a12 100644 --- a/google/auth/pluggable.py +++ b/google/auth/pluggable.py @@ -360,14 +360,6 @@ def _parse_subject_token(self, response): response["code"], response["message"] ) ) - if ( - "expiration_time" not in response - and not self.interactive - and self._credential_source_executable_output_file - ): - raise ValueError( - "The executable response must contain an expiration_time for successful responses when an output_file has been specified in the configuration in non-interactive mode." - ) if "expiration_time" in response and response["expiration_time"] < time.time(): raise exceptions.RefreshError( "The token returned by the executable is expired." diff --git a/tests/test_pluggable.py b/tests/test_pluggable.py index 7b60ea8b2..280cc2aa1 100644 --- a/tests/test_pluggable.py +++ b/tests/test_pluggable.py @@ -812,64 +812,7 @@ def test_retrieve_subject_token_missing_error_code_message(self): ) @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) - def test_retrieve_subject_token_without_expiration_time_should_fail_when_output_file_specified_non_interactive_mode( - self, - ): - EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE = { - "version": 1, - "success": True, - "token_type": "urn:ietf:params:oauth:token-type:id_token", - "id_token": self.EXECUTABLE_OIDC_TOKEN, - } - - with mock.patch( - "subprocess.run", - return_value=subprocess.CompletedProcess( - args=[], - stdout=json.dumps(EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE).encode("UTF-8"), - returncode=0, - ), - ): - credentials = self.make_pluggable(credential_source=self.CREDENTIAL_SOURCE) - - with pytest.raises(ValueError) as excinfo: - _ = credentials.retrieve_subject_token(None) - - assert excinfo.match( - r"The executable response must contain an expiration_time for successful responses when an output_file has been specified in the configuration." - ) - - @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) - def test_retrieve_subject_token_without_expiration_time_should_fail_when_retrieving_from_output_file_non_interactive_mode( - self, tmpdir - ): - ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE = tmpdir.join( - "actual_output_file" - ) - ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE = { - "command": "command", - "timeout_millis": 30000, - "output_file": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, - } - ACTUAL_CREDENTIAL_SOURCE = {"executable": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE} - data = self.EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE_ID_TOKEN.copy() - data.pop("expiration_time") - - with open(ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, "w") as output_file: - json.dump(data, output_file) - - credentials = self.make_pluggable(credential_source=ACTUAL_CREDENTIAL_SOURCE) - - with pytest.raises(ValueError) as excinfo: - _ = credentials.retrieve_subject_token(None) - - assert excinfo.match( - r"The executable response must contain an expiration_time for successful responses when an output_file has been specified in the configuration." - ) - os.remove(ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE) - - @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) - def test_retrieve_subject_token_without_expiration_time_should_pass_when_output_file_not_specified( + def test_retrieve_subject_token_without_expiration_time_should_pass( self, ): EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE = { From 2a7d288c526649352ca161da4ef9d218cb2902f4 Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Thu, 8 Sep 2022 19:07:31 +0000 Subject: [PATCH 11/36] fix lint --- tests/test_pluggable.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_pluggable.py b/tests/test_pluggable.py index 280cc2aa1..58db4876a 100644 --- a/tests/test_pluggable.py +++ b/tests/test_pluggable.py @@ -812,9 +812,7 @@ def test_retrieve_subject_token_missing_error_code_message(self): ) @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) - def test_retrieve_subject_token_without_expiration_time_should_pass( - self, - ): + def test_retrieve_subject_token_without_expiration_time_should_pass(self,): EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE = { "version": 1, "success": True, From b15b9d5827ddbf2b3df8cfbf31ae851d669e70a4 Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Thu, 8 Sep 2022 20:21:15 +0000 Subject: [PATCH 12/36] addressing comments --- google/auth/pluggable.py | 4 +++- tests/test_pluggable.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/google/auth/pluggable.py b/google/auth/pluggable.py index 47c786a12..dad41d5cc 100644 --- a/google/auth/pluggable.py +++ b/google/auth/pluggable.py @@ -192,7 +192,7 @@ def retrieve_subject_token(self, request): # If the cached response is expired, _parse_subject_token will raise an error which will be ignored and we will call the executable again. subject_token = self._parse_subject_token(response) if ( - self.interactive and "expiration_time" not in response + "expiration_time" not in response ): # Always treat missing expiration_time as expired and proceed to executable run. raise exceptions.RefreshError except ValueError: @@ -211,7 +211,9 @@ def retrieve_subject_token(self, request): env = os.environ.copy() env["GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE"] = self._audience env["GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE"] = self._subject_token_type + env["GOOGLE_EXTERNAL_ACCOUNT_ID"] = self.service_account_email env["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = "1" if self.interactive else "0" + env["GOOGLE_EXTERNAL_ACCOUNT_REVOKE"] = 0 if self._service_account_impersonation_url is not None: env[ diff --git a/tests/test_pluggable.py b/tests/test_pluggable.py index 58db4876a..fa9933a84 100644 --- a/tests/test_pluggable.py +++ b/tests/test_pluggable.py @@ -893,6 +893,21 @@ def test_credential_source_missing_output_interactive_mode(self): r"An output_file must be specified in the credential configuration for interactive mode." ) + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) + def test_credential_source_timeout_missing_will_use_default_timeout_value(self): + CREDENTIAL_SOURCE = { + "executable": { + "command": self.CREDENTIAL_SOURCE_EXECUTABLE_COMMAND, + "output_file": self.CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, + } + } + credentials = self.make_pluggable(credential_source=CREDENTIAL_SOURCE) + + assert ( + credentials._credential_source_executable_timeout_millis + == pluggable.EXECUTABLE_TIMEOUT_MILLIS_DEFAULT + ) + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) def test_credential_source_timeout_small(self): with pytest.raises(ValueError) as excinfo: @@ -921,6 +936,23 @@ def test_credential_source_timeout_large(self): assert excinfo.match(r"Timeout must be between 5 and 120 seconds.") + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) + def test_credential_source_interactive_timeout_missing_will_use_default_interactive_timeout_value( + self + ): + CREDENTIAL_SOURCE = { + "executable": { + "command": self.CREDENTIAL_SOURCE_EXECUTABLE_COMMAND, + "output_file": self.CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, + } + } + credentials = self.make_pluggable(credential_source=CREDENTIAL_SOURCE) + + assert ( + credentials._credential_source_executable_interactive_timeout_millis + == pluggable.EXECUTABLE_INTERACTIVE_TIMEOUT_MILLIS_DEFAULT + ) + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) def test_credential_source_interactive_timeout_small(self): with pytest.raises(ValueError) as excinfo: From 1460d6f2812341249272c77fd1a2ec77951faaf2 Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Thu, 8 Sep 2022 21:31:45 +0000 Subject: [PATCH 13/36] chore: update token --- system_tests/secrets.tar.enc | Bin 10324 -> 10324 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc index e2b0f9afd2213641a93b60559b903c2da601cbc1..876b98d6fac3594ea71330bf6d26e5c8d5b1385a 100644 GIT binary patch literal 10324 zcmV-aD67{BB>?tKRTE-x8MwOZU{H@!{Gq=IlnU;s+e|!-11rMCc+(Ug(4!KnPyjnQ zDV*tvP2A5V3<}Ms^cc8zx`D~dU?6v(Lxt-mSL?ALX&20*?ZfJc%~CW4$m8?B3|M_E z4WpU0Fp2tv;n!X3KppeKzA@T9AMAp74`Oba%nz2)O>?O5i1H!CYQbzTcr#l{%0KBX zc(Dmt;`KYaXLnzAS5s0B2mq-X)L;z-gAQgRQC1kcNLUI~tS;N~T*5D0A zxxV1Xn)K<(j4`jbka=aH3Deo;t0#!hj(9p3n+;o!mo&4OYZILxbv^j6^3Xy8Z0`E7 zDgb;d5@bsDwDGIOuCehe;5NX4;0|Wh9uAd?{i~Uc)Q6ic`Sdq{U<&1jwdO4ki)2g?*v{x=<|Su%n-doY^e_FGuw!^d>m`D&jjScL6n@*301 zns*$kqn*%R;-zQ8aaR3H`LF6EIQ1Xc7|#7n%sdddpnDsu^>oqB9vurn1a9Pizdlp7 zPm57=`-24`pWKks&6a+{hd$cfDz(4mk8@a0>QeN>xNgM&fdi(c0Qb?JMeNp!06kX*4DSgK{T(vTSR~=+y=Zh`$Hm3uk9-S2zES~o$S&rNIEsqZ2+9{s@$J+Z3lPBn_cTT@AIyNUw7vBwVc1#z4* z)&Ra4pe+vvPQGfLnfR{la#CA{@YG;$9#`ut$h+mi&qt1SO3#l zq}f(a4o4TU!DcqGsc??M>Mz+_C38++Q)?lqd*oDObsUZG7+v5b!YzWmp*u zXbAJezt7#2>(*jsWWy0aST=s?z@a7wm*5h-f44X;TeM~L4l248U&X-V(cV7wm8uIZ zMJ#XhSS~DpaG4j&a;f|z$bbT`ft2Bynq?tluZ z2*d=Or7k`p?#jG0s3{6cG&M}Slih^vOWAWA%a8wpSI|ng?(>ruStUfVC^ImEZ3J3Y2#8s4W^i;W#_500- z_=ZaygjJ|LXIh4-Zl~E#qsu)!zd{nlTz+6*$fDld*lM%V5=jA02IVfwC+zDoI(3ce ztyf8=-&cOp?ijL)gM=P^^jQSj?fMJa~IRWt+Qw`4j#13XPt2Fc>@N=vK<);dtIQt$rTN>*xO_hTUP4W1ol0dj;0G zB>e>OtgAocN~Qz6&-0#hO|lGsAqYmqA@(xV2vSw!|NDJ29=u~>9j*zmDB*WB+St{YrE-gHL@SH8 zcCc*@&pRd1?fDssQU7mH(aRjknt_J?*Mh{1c(n0*;6_PVnRT`Y-2+0%JB@X9iNn5dcx4!Zj9RJjv||DTV=XEv=EYbN5z z7m*JtP;ZPrQaf4tfN6TT$e$lOS4GEcb5Vt3^2Q?Hx@E+<+_Q5=3hb? z%~thgAsOv={d(ImMk)TT<;$D)tmNm%IDA8sP4%S>Befn1_)YYPF-Q)Nno$}V}>um;hQGZB&{4JHm z$Cka1=smn9rF@*6Ct^B`l3D~P-^J6jUldHPZ#+L0_Jj_hkuwePdNdII?Sw*w*Jr8P zLF2aV*79&n&Q({j(CbWH}X{mt!(h+hL-#pc=V$Ufwwm6zVLs z&(COjhS7@#GmKqhOI_LHy?v3JfC~Bap|+@56tNMba!kV7Iip@BNrkIh75RVmjB z1CM@@c%$cCb=7X#KXv>$mxGCxx1b- zv1xfuDb z9@jiLTVre|w@)eQ)*g)P2OZKx1;JU+4{v2AxzaH@a)^dunBzFOKl}XS1w6kXHIzbU zHr;FPcKlBXmt1h@6OsLpGoxYnA$Hp$C-SmmdrCYFqS)+V#`Nb4@-~95a2HjX^GXxY zW1&^Xc0^ku)1!8(*E^duXD6o=p1O6WQN}?q-gi z3rBqPHaqtTP%QQW0beC7Y8jrG%ljavUxYGYTVZt3!&5ZIE3!oWA6O!5QccXL8vjK( z{Y2nuOVYh!_fap*@NarudrgN~Yl8OGX#N)Z0EsQ-Pn>Z29eY}~1X0gSz#IUSN|09; zQg(1yI#bmZr#iq&D}H|FSYS><*y*9=d&-BX!F=;XkeIqh! z5G9DVq{|eSbwBw!ovqtIN_qL3&`O*u=ElW6UNMUpyJk{P2$wPT6xL;E3%vQ2|J2Tp z)4~YuEQ*Vut_GU$m2}F{WlAn`RA;Iy3aDQP@B`UNP%@S&!Qb0>qTFskH|BVd z;g}niRqU}I)5}Bxn}U1a#l)PcYC3slhEQLXV4%9Wh?jlg^xwNEY^J$Hw!xHEU%|6v zKiQsds(^(H6=-tgxqtW*Q!g?KA6IKLCU?3Og5CjynsB!uz?Z32h(Sn=y8CvlYb%o) zm)K4EB2_OFG5?^1%@k9N#gpW;(mGfMF-WVt!lb`CJrMNcvo4XfDmg+LYS~?k%M2U} zz$TmHWtu2!wUK+8$rM+824`m2rfr37r8bBal?XlvI9>hET{q^k|B78`*8#nM+e#Bp zqOP|rn*tX~pm2(}FB?MVx159vAJlUw8pL4M+X%O`3K%h+@Ed!E;49u9g5i!EbuhXV zs^YqFh`Wv?>+=(8SMGpQ$`je@xUiy?n3Orf$SllGuw$$_#YQwx+|10=1(-ewSa?;T zXuT!shV*E`^*!vP3T>uM&@hZ<12n=SAZfJl9UA*_G+45iPpX1XkP|j-a{uZkb*?Eq zBSE}6){Ml1s(;$#3?rB5MoLP1LQYMwP&Hw~2R!Y5o})rpz8F{5F$TyeG`yqcKy@kJ z?7&7ns+Y<2Ga7^$$+oT4WE@0SX*M{YKnAwZuYrRl9>?}rbHu$9bGg3?j~l-R$M{#I zde&S1c%VyFM_d0=I={Q!;wFtQ;o8&;qf(U6k&R|eoDCei|BT!?kHYN?fsY5-?Rytm zqab78XPPpawzX#s9FWhnhevF)1kJ+vByc{vQs{e0&t3x`i#l9~v<)A{r;+syOMEc2 z%qdP-!$C_rD#7P~znj5|OeX$l0?aT!OWyQ6TjPguu9}}$Kb8gQ^Fa@+D1x_0%O!H= z0gOF3CSLsa@6)JuR-zosN*IS;+0j=L`1i5Pih^gb$emoU*0Zy>rP;~<`i;ecqsoBa zI`dJ*JS`U0>IWv|*&*be!Kdv2ZF3TxpxGFUXl0Q`HTVT0cJe!_lMuje2oF#%GWR3BxZg!2PKoR;4nCO2%DjE z7vA~F6IN;$-ptgcYK#I!!Z!dc_JMqY7TnT~;O%+$EJLZ{GCl+s93cqtF#3>g3l)6VB_gS%I!h-~Fi9c^UiVKsNPp|5^{bNon) zblc15kTHLSjpXpX#JNYfp{_3s@IST8o0a&)$anyqXy#30auXmc|W*0|3y$z(R`4)Q;!b!|knTYP~O9 zScdP_9DjI+3f>dxLb9U0G*Y3%Kj=8Y&S*QZ`N(K$n4QLC(1`}#t@5y3r7^j98)MkJ z9qTRJ4U0vlqeE8Mf(($awdh^zx05Jf%sfPMIT%5Xr(0XuL?kJOawQHT-iMdHAff4M z`gic!9f>mWIQ*zdwwoGSg1nB1W|xqtAIhhy8r9zrpT{{mAnS8AYQ=#Ys9$$iR9L}n zF+p2-XR}ryAQfv9j0^fyP_H{%_$3vsyqC|Ikz$zga)xn=UyaZFA3C{q!6K^Wrr5nA zvUg*D=0F8H1vC1PR|%Srpb%l|xWj?O$MkedT9KdD;*`|)5(+8Pxst$AR&Z3P(6ak| z@4Po56qc>Ki&b_Uc7;*3iQvf(cn2O`tpZ&tui%lLm%05^xVy8O@eNi2DMOg~N5$Bp z?1qK5jw(qqwnlJ(PqG0?z&l1nR|y!$SPKTRCAD6I_9C>X23LW~5O!Yr>Q9(O#4X?6-8Ps#_rJNxPpSy+;EJA6mAJ!F-HunUPduE99McI@4Q5LI@4UD zHHr0J1dLvo{D`&c-hJ@H-|uucr}d;WZp*xDz<*b)pos|+cUf_gSHyQ8Hp?t1;VxKK zOnQ49BP(eGNMU~?JQ=c$xM4W#Z(MD7e!`2awqJOGQzi7R|2FVX6pI@%N6qazM-f*7 zl~ZN~)nnt$^|ChZ(w8;>uIQhJJTH2PpXmn{OjDFd?^{6^mzbFD*)z!0V$`wKcl{9L zYwaWB%jXFFT=&=u@DPaX=@+NTxS9TI8yefNY7qYt= zs0ZP~!yyLqSiA;Fii}lSpnD>picaY72yU<1?0cShng~*$5>?1@%cqTX{BLU5>tmc9 zAadPxLPJ=oym`Quu+R%A4wqu0;>C4H?=n#$l~o^H5m?`7$aE5^%j|K28ol*f=5=`ytBp3|-1De+25OF1LkmVQTA|;ZX`=?8yQ8z&6MZ}oG zFEw%JC#0=Bb7!c-seM9BZ>XUN6+UWlij6+7bLL~yiImS!RzJVtBp~c_2yB7Qvt{0JQ~(s=a&hHB>=7t?eAZ^wDYui zr#3s#;La@|2B?XvXQj_+4nmqr*Xl&70izYxv4lS2a@$6+>l%36|JOSnt>px`&FXA2 zcPURxIh_i7K6t&A4l)nkwzH@kHJk=O@hU23hPrW?(+3#cuZTFVGwLE@9;OP-J7ZP@`P!BM&Zjhz9PifW<<7tho+HO7h&OEk; z*Wjkbu9)`v1*qfbvVP+?3RU2^?WA@9!q8Sztx5?79hHoy^oLogo4vvykbH$iVEDwt z&xt1g??-mGoX%2vfBz~}0Ws8G^i9GEpx*mjT%kdgu4Zj)?PT@9*P6N$@$zBEa6t6v zHMixWjLH+vBwyEy-x0wQf5E#pwzXDQ3CSQa;YkKL|MTgn2ib&dFwF{rls5+Ug`iPb zRErmeLBS5;cA>q%z(CPT-V=SYb?xS}JmnF<{8@A=qLP5v3|&E7Ln4B6+ibie|Lr1Z z2xt$bZxPgQK$sdE-2nFL7UpEPFbU z6by@UJ1V{)SBwQ}-+Bg(Tc_V@q_J;WZ#vSzD}NUohymDL=tC}g^OPBqNSPq1)z z4f)5Aa{~?UDVDe%zKteO+)}04y zEY})Wu@WlHRy_{Cn=zCffI_mHz}f$)S8msaqMQ?9_717*)H~2QtJSYllWYLE>o)iA zHp~-Pw+#V|QDQ{xoAJFURWLY(RnX9OB2QqtXQwZG7>N2HU3GZ1^9_DG8ZJ!WE!T8B zIaD-UQXGY2$nlShT?CvMM=*-r^jZ#@?+;D|me&d3=pzC3g)+{IZtt-E_p+6{i$>!~ ze^Mv1(uiO@KzTttXBor2USPeG-V{rA`4PiG?jKf<~xdlRhgPc1p*x!9H z9(!nlkXXyB-!d+sp+iFDF9vbFu4OhVm|_7xAI9c>Y}~qxoT^$$wA^lr=tH@y$HlVh z4btFz-%9uhGEEWIf4-$`AQ+AkiJF&Eeo-5W)4q)_9A$em)~v)&@u10Qi5p6o%3m$y z=B=o%^=0-TD5k_UpEAaWO!!zPPgaP!o#ssPDQE~Lp7x$Da&kdYOLX*-eQ5b0xg#qa z^Emc-6-@yrDz`}W&B2w%MT+Pdi~Gq1Vm)Tu)IfW({2qj0yS8qKkku}3WB1E^#WPxA zd_+!dq0g=Q0dSj?JAE65d^BB99eh(ZrZ=jz*lj})lH|ZJNSx`QllK)d=h3^=E)Sa` z7xAK9-3W^W-j{15lUMSqXf)+XBBuwOj4S6lm%s}M1Y-<}6M^IY?6ljSXUA1EgTpjG zbO$Uzk|ES?`Sd=aG7E;hOH6*X6db(S*u0*upTq?J05`F&?&OUbkG8Q=f{USLmJYL`o+oO#K zEqL)CF`rUK;)Z{{8!w_#HCwwZcfD4Wb{7P;H`J8#iSUEA*=EwZ^_3Colqn%|M4Ng9 zq-axC$IKkwpm`AxtE=Lak^~zMW(l|!JJ#7O+Z`rnbm!w=|Mc;iDN229&0Cuc`jQIG z0I*&*=9!Y*EBm8AP$Eggn9DFb7{xqt=Ykhohtb=GhT_?C=MHUE!KY_g1R6YGk)Kh7 z6uzUtvH!31K=;~3wVefL%p{uzfr466%~~-?*wOskl)Y`*c7Fx}3&qW9 zsX7ZBrS_;@wxSc>2_pI1)nSgD33m?9{^H;+8qBeq3Qte&tI{&Lhfm5Fo&aKZ{y^i{ z_wQ9-CjUYuH-B|bE}>X+xY`{u&LpQ4cRCmvpgxOTe^s*HbUhWPbfynPEod*3l#WXe zi#UlcH>wXRKTb{{5<((82HL^i8^#>B58=I40EpM&&(BJKfZ!pqG-$aYXn%%-)kfr( zJuNS-{+lu9$ap{2OxtA!2%fixMAyEhb5tdSsz64+l z4(0}xzlSn%P2%uO zp}^lPT&RBr>c2-mgQwI*bmELTjCKyz>RuM=%M=0~fR&pgPP~?pD0&0;;8k&skLZo2 z5CdbB+w!OJ#nCQ`iwoE5G-;ZjvXbrN%%yqKTJ?%x|?6%ooV<~jtheN(e2fx@t|oheEQMkJD7i~-xV z&=5PLS*U`_l!?bptdlGy$r&hwU^YU%K_7$zz^W`#%cfX7(Db*+YAKAM+c)jxmz}($ z(ok7%j(3h7@^HF1^5Ln+QiPKJER&3UWzd5E&aw689QC(Of`zC1rWtg-2|?D+ zc9K=Dev>se2?&o9<+Kn&XL1sB7yE`5ZW5#VFGg#tFZr)?@r7FuGzJ!J7rzAN2>4uF{% z>GQEYWSE|iJOpG_FTkYkYi_DUupJq{1MG)&Y(~^kHY+^v>Prt{;_~Vc!xJ_l3-Y0; zr&wZs#r#=D>os3z9mScwlvvORZwp)@sf8*o3FmxaiK(BjoA8?oFe~=4-HK=1TRH z-x0)bSq}+|AG2`(cPq(*i{B0YYg-3bIw+Pm+|bb0SosJMxIVQ+zOz+IT*5_E~ zczFF9{Jtza_8M-Ph?#6LaioNj%2Ndx_h5OFD)&bhl}ONjA>a$Vcz20k`hZ{(awL9> z?*sK`ciu`oQ@RsOjl{;^c{%b>+s%Qm?<=r^L{{k%O4xg70(S%4F&Pr@OX)uC_hj>| zjQg0&YXSbLU+240%gF%pwV9j(h|N+g5a-4{wPmMJ*!(B7UTVwr;66G3ZB(Pt9Q^`L zP9dDbsK<4SSahP7dnp9{VBsx|rHa)$?IYB!aY#&_!Shl$9n35lr!uWjgQOtc!I1`? zq#SeyhDI$}Bc5#D=jUc>XgIS7{&Z}G&d_48F9f8N)kly=JCeUc`l5i3uGglXN$*Bi zS6h0ayM`I$?Mh|m2cHfqW!@s^Aj7-yQJ>=>fjN=L=T0GDSPB>w+wnfY+1 zRV(O8-UwLIz{kBxsx$gkrO0BELD3#Gk&!9ePc>3XikpfDNcQqP{Zi zu$KN{&OBiAUWW3Zi=%?L;sJi9NdWn4)~+TwSXkmgMK7WF&ecu*uo|M8XHia~Kfz(h zpV4Avf&({xotnpA<;+ad&&Ppc5s+#^M+bzqR=L<(zPkz8BaZhg*bQo zCN;7>sh`men(q$Nf&nWl1X|i3e$Jbu)DX%HHb4D2kQ>)BS_vq?l{kn?}6#(QRodJ`}avfSF+E-o%l z{KnUzcx|68mo{XZ%;B-V!oxYPk=KFCZqhLE&?QJnZES7@MHFeDSm+-ZEHx3BC`P3J zq`N7TVm1fya<`Q?VZAoXTpM{GsK9EX>FH@mBuA?pBuQhkgTH=3Vyc}wc)%SCh~wo- z;qLnUWh%^`m(41jaMNN^=VKMA} zq#yh47(vj2L5zL=n_lFgz|DhPLFAGTV%^ZZpjegWgAq)MU6}j%=zwD#ZUUKOpaByo zC*PX>Kb+db4%_HV>e*^r+#)enhP?)2A1VAcx|&PvMgLP|AV6$dr|5U1JA}h!x=~Fnj1?p m%1m93@!nh*zbR7W4S}%Nt&_^I*0<>nL{1&LF|<|JPFs!pCMDAV literal 10324 zcmV-aD67{BB>?tKRTIX#i37a2Mg;HB-Q8);18cy4bk$l}`?PG_Wg$gYYr+z$PyjnQ zDV#;Zdcm~*Iwfg~-aIrHEc#$&wWODNd4>uw{AQ#Kl~rBxG0acKsMgTmzaVa?wv)lk zgS@E8rwrJd()g74FtDM_quwFZczVtOsvg)VpuW*Gm`VlH za)P4dSLR4Gk1>(|U@Ht|5YWg?TMzXGbsDxs=XaPx6s@84U(SDc{3m?#C2#1mhFDRlh} zQh^>g>%H^x?2x8Pt{7PVyAbmaKcR%a)2xfa^i{h!WGlY^jP> zoR&RBb}KaYUl6YD(F9;ms^w?1v5JWbE*l{wAj`apUDT($P?JMc*c3!ouj505CF|Ep zc8HNgi93v{Fu{yX63>Xj4sJ&OdW{Qb0v~ zVVO;N(?^GvRtgBE+L7^ruyPtmVc+^S`-a!D;CZGR;n(NW$@!yn_P{NSfXuIAZ)!L9 zzl;!%-mAfF=V3cM)D9f);}OXzQ|(bHt`2^`G<=~(#OV!9-j?PZM|Rxk!HJ$kFga-3 zVQNnrFWlAiw;$}<2+$oVEyzy7;u%%CJfW)TP)d)23ybzd&z8ym&}-)FntOAzGY8^r z+4?#+s^7y6McH)ayrFb+pQJj=Lx3RXmmrJ}#@fzMybWRAOZ}j?1*SgLg9$kq^;kYim2n5wsNHPxHn8(;gs8J}qKFKr zfyJx?EB*rC@9}w`btw`BDr{EEbF6>Sz+w`oD_ua(3jb)HG%xcC=AD2iq3L{HH(5pn zrD)t5Evir%ec{V2#n29B=^k57Z~BZ_)<@RnC?o+0suRF;I@ z##SD*REf=V5M9&6@=I5J(I+Pz{M}dI*+i?~N8hIPy+H=L`Tyr~`u9q4C!iqQ4e*dw zHr@vh0esl>JZJQys|G>2@Or4B{P+ys`h*JSS>65*l?z>gh4MAY>)Gd&xbBE5Te^b? zAJ8>uGadto0Iup?f=)aHEa`!I8x;<3t4en=AyS3X2E3pRDR0`i4-uG2^oM;@S)Eap zZk6YAw1ZjC70^U(VR&1dbk0$B6e2iI3OnWEK$&;S+x?lZ6DqQG0y|fn%h;ll4b4r=A`zlLP4nvW0+~w@-%f>WcELH+e9q;|}o_X!1-slRaK!1qT zIh$1H^*bMf+_f3(>fZIj8URQEW3n(|2weJsz#(!&rdq{o>pRjhZGO~II}_cd%3z2= z2sw{b9+)D>PeF$}PrtZ?;#E+s$0!)XPl7W4{0E!>t@C!>%$4*`aU3p$rR8Vh%S&L9 zb=xNDC#2D-$bNIxu>Yu%D0ZnZdD5MAVN&sMvpQRCXh&j6^`9|5LI$11u9N)&e4adB zsgac$5C`yh5Y3q(e0~cQ4#J5n_scC#-hEMZ3lGLnO6m~21521ZvYmJ9#@PXESGZ=D z+R9Umy?wY#xIv>SrH70^y^AQSl`pL=2mTd>hHpa+C1_6n_Un@*ah0f!X;^8wEMa&M z8DUK@>K2kit8vplnY0&P6a3z6fWJnUxs8FHXRl|K?}g;~2I zauX)i)2Ii|65lLc1R6~h8?4ypNqKX=?r0!;IIz$2!mcnQM>_3K`P}v%(u3xCP;3qL z(CfCJ?;C=TPnD^=Q;Pqqp+-w`CFFYfZrK3hjWdwbvGVesNg!z>v>Z(c_8ES~_t@7P zl3x3x?c};Sxp%|WOk3JDK%H@pmk(qZDSg$C%9$~GW^84b4dAeHVz`_5C_{t8aQ`r9 z%ll?y0DY)Tc4aNdoalOpLNT;&(ng?AR(-3Of!|FIkxObM<+1BO8~6v?hGo4Ol+~X- zu_r?Rl#wZX2Vg)t;gLU1j_Y1$w0K+bRy*a_2Q-y>4q&^+grfOd(|$*5rJ~VtKv6s7 zJL-$~UB0?QkfkAZ8%{Mn(W<;sMTEd!YgEZ7z&KN#2 zNPODMs0TpL%oT*`ETxeLmAM%DS8Vjs4El5Mvk;$mwX@d_Cw88@mnkn^_# zM#7~_)$lDIfqWLkewU-3*uBWKFVpsv9S7~Ni$)2!PH0!#>+d z>W66a(}6OF297Xw)3%m5l0^s>L21n?5=Vxrd_HdaRQoYjOqftUnJzaFv7!jUiqORwxuy~<-S z!0jjYP8l+uOO6Q|fuT6B25?69<akYNh4-1j9(O6dad9l|d%l z(czNQpzJvApd9cE)}uJy#mqy2q4j+)poXmy@+JIJIvMkz2!7#D+n=H`I%X`5oYd2h zN*{g!)|IApJ+U{JCTRJL^uZt~Z9b{a&?=}`l+<=U7IVLAX=uTg!camd7+Qk>;_JGg z@HU)Ne2g4Tr_7%40pf7Ab|P&EJpuI0aa*PjMZxt^$}F#@v*ZaN;UCdGot9%KIHl`) z*l^3U0)`QHP@=>zBy0}eGfLW4gyn%9GGgx-P&x1Nha6~z03~uqHiDWlI+m=nfWW(m z@%Zg5)0RL3;QQQ3g?MaB9w>u9raD}=T=WkP5;tH-mKsT_IB|jsib>aDNhMCgxy)dh zOE*M-N|#P|{Wf*;z_8&|L_q3rv&6WT%NXSW$UZ@(*OJyTH7Hug#6W1pe(Z@Xb>YqN zS7H=oysC&^aM7TpXGzP~;ziKuPIvPKU8``#t9AJxq(tll4rBql!FXy*lX0}Hea5)o82zuZq=U$Grq7R##IMuGG8jWw}(fm1Gnw!%wE1N^r;I zURc`lCMzN!Qw)tr5b>EhE?VXW&HX*{oCPna)dTBQ*^Cd7(<$DR&9O6rx~p77vKP9W zH@!GKWk0S9Vh#FI(9rmxWO=O?r-8KBpcY7dF{fFjxyY+>3qt296cIN$G@nfVzx~Et?7t$HDNK>0FR6VIlwrJptTuP<82rzh-6XSkj6XINS3ituce? zmNdqe-r0fahCgFyg=#S4!JU=_Hl3nh1O4Hhii3=in(q%}w|!yGpMK&c4I(VzrD)G1 zb^#-q@uy3Q@K0j01)RBqRbS>hpWfrWCMWV_GU+pkGY1aYv+H20NXMioT93#%%$7kd zLO7Q|f3|VPNe?5j3CZCT`rM~!WmpxJs0(ZMJA;fZvNlVLXF&k8d^{MW>cT%lRz#FE zq1wWqLhEJ0S}wc!Qy-M$tp=QSg`^hfKwDZ_Ieo|Bd#Z(4ES|T;)AEHsaYmjd3iC{^ ze`1kyZhl?WxN>6EJVQn(xJC#WjGUlP?7ng0fib$=PE%DeKUU95x3H7?Q@E)pIS4?a zKT59%!OD>C3J*5+T3ABm>#Lh7unRkp3L*c={Bn6rum0gLr>@8=kMxl{t{tKG+U6@$ zOus|!+esRZQpeJyBP1sj4r62Xd>`77tyZw>ooB6}E-}f+ryFF-6)KiP?REh4f*b&F zOQVWOa1QWMlZ>w0Ih!)_Zr|BmS3TQ27zgVoM^aKi&jFYA4%vM2k3i?Qp{9RP9{HKF zIG8>E$sbbB=Xx2xs$mPrIdn*Gm_e!-!j``Xz=?`AmLf40HX=>C_Zz;!=OHG|{r#dR zCM_s~bbYqY5k7$Z*4UEhCZ77In&oz{4Bg`Gwf}qI!!U2y^tw`Vum*U9cM4$dTmZx2 zfSZVfTqyX29464~Y4~D6?#7aRfF}P(rGJ}TBvJKZ19(biiSN5m<;<=ke z;JThr|L=sgMW@XjLy+y+-sB^!cpC8->UwWcP#e!WNtU`WEpUEJ^#H9G>|Hhr9!-wD zi3oaWB)Gxi-gQ**s}K4o!gz;1^4AfqNXZ2av|KGyL6rWL9WW4^)`hO!E<4L)-x_m( zZ&S!`L;#Vg8ojPNWOJzqfp7^HT`6Fre*MplQZ1Y`n>~AOOd~%=3qX%Ztt&hW7YgS_ zw?2^O*EVXfZM^&vU46s1krmFKeMlLhq)VZCwJlQpkvex&Vs24ImXj=Z%B9Wx>nEZ#GSqE+A)(*9ntn6BMC8tUH za3D*^(hdhlA?pUE*tA}swf2PkCRI-2w-(+i8)Q#15q1dI*)~R;Tn!vUQy#+9@Dc!) zG|+t?meUv%ze)EmVBk%L%HHZ^*q-hfhdfoTkN zqKG%S$_6$zHQ5E^B(PijDLaS}{z#Iy<*$e=s$2-h z#c(K|u0Q_mNeygR^*x@XT_5OQBes)D9zWD$)V507=d_K}seW zwR?%3FzwmKxYYXP4li}G_EKNtEEUeB-#OTGm0e@DCX*dT7@5~_XLN+(fg^cQWpbNF z>Uctc_DR&tG_8G5Drs7TgGGOpGz_pE(#6yM!MK=O;2@g`76 zc+`&Sr!7l4{~Sy{sB>di$BNJj!J z@tM!EsAaG-8Fil;;U>Yo%r~1xV{ZSTj4kHtj{CDd%3B$E&UTxddr-+)1g<;dwf42b z_snkoX<;ErMg7%3;5aFLD$#E`Ud{{Eb>`eIj3oc)UB#``6(P3~)9+1eWF(bYogzjs52$|j_ zp$WEr0iw?&G@`}jGAXRlR%OmXP96&(iG&1+@E8yCT>uSOTuZgO+4fd!rB)JQb{}L? zs2Gx&@QjL}Dgp|s)V1cbC?aFy(T7*&*3KQ~oR@;c5lW=nI5t5bgF4(r&u9Q-@gJ=N zGXoCW1T+0How6m`$HhjAfO#oF<|n()icWYX%%cF>T&#=?}%;E>RsFfF(?+=j&h60LjxiE24Dw%C)Yw9I_-`EJJ+V~+)amZn7}Kyl zTRJaa8Ar|z7p7d|- z=-Kp+M9gEEHu3iTX*dYl^|;SZI+qLyqeKD^>wgAxXBMtL^8piXzKj&LE{%^DR{!Ur zu$+EIm6BkR9%)Z@Sx{2T)wKu#S2Se`t2$IB_L6&VKNq9W9E5Qxf81G^R9Rp@)x4?^ z73hkkW7n_M3z+F?{W+M!fbKC$b*`7Tvt7?==f89zvP#aWM@VA>x^Jh8g_PLd@@7|*>=>{r>!mf*xw@?y`K8fZ#&h!x% zZ!?G?%=i%jQl8m3S^fyX7J0oh?SjCMrD1JEC^+*31OESqrx=SfWg$I}$TEBx?nWEe z2@C_|`-0_F-;!C4AFi@#vePD4({wQ_q3WCza5wcmF=a+Qowal|+R? zQjAao(-f~jl+>)$C4h-pQ}$PwYbr1SBdcC2wE=zst}#HR%ka5n6tI1x@1onWL+xgD zI()9~9QtKi9`tYAyI9_jn1n^ zx|ykazk%E4rZ=Ik1ESj8vp9mhWMa=pajY~cF!Feq`X97Sy}kE1QlE$?Cth)+3T7bq zbxdMZau4d1ZHmSH?1^8YjI=;0Kr=a2*>HoEdO@;|sh06YK<35;u+|IlE{|6knIF^4 zks&@0vN}R36CU{xH5lF4)fv`)Uzi~F;#$P=#6M6z!66;{rcoSfYrzg>x(Le>>FX-K zDu?10HC3;FpGJYOsdF641`<1;SO8qSz=^UJ^j&8)>F>o2$d@MW`WOS%u`RK|E)o7* zl2z5DqZOk0o1B>ynOT}t{Pvv)@~v7^YdGJu{^VKAUd}e? zy3w+r4FY??YZ}yz?{c9vsSJcq3;(W?OmR{6DI!f16nMQ zfOIFFaLtW%P_Q9h!FA!f);dG8#@xfBpI&o~;S}%(NfG9=9DbQQEkN)kNNL&E;M9!z zVgcPP?@3*WgOea9rT&16h(Y^mnW!G@@k-y z?*BpY^Nfi5qil%~eZ{6;xnkdiOY+siHm#N^7 z8JYY%2$|Ze5uMd(E-8q5S8hkw_IL%lNZudt1nqjxdds9>j-!wbmXZ9Q@e&~I(gBsy1 zhOs-2&`+sehJGU9$Ic#Rj|I{ETrLsw{(^k?ByX{^JuNU*u_P(ZXRtZ_YuAzjD?E8L z0fWB?IC(g1mFuvaTbdcmR_AmcImzLi(OtSSC7`ZS&n#fU-8=rH9eVn3V{aSnYG4X8_n9x>OuQjl zQg(QGMcYPTC?F=C(%^x+*FCZns;^TviEJ{M>T8sVQ9SsP^tc)Il35ukqB65l@N?cY z*1E&pk`;lFIYzhB24s=~JMJEtT3Z=_U78TTAbpe~HEs%=+p_CT z{*YDc<1Nv~>W)zyk4n4%Dukn@4(skw!8=$9G^(z9E#8IMOR_8d*=t7kx8 zX-g8a6^oBDHL_^$#1B{t9Zo>`pTqmb8{DnE3xGKjs84BZ0E zuNBm_ii)S?u%qb{j52WcI^}>=tW(w4M>jkl%*Vuwk^xqo>cW=BxQAdM`*A6;9%h<$ z{MGAYQi?HM~Toi%g&#{h7?y@%9kuK`kF~U zBa{ni^ZRldb@xNt%Ry6r6HDkoL7BQW)up~IxN5G&pidDJmb)7?m&GougIF$+xjnA2 zy$U{;$x5bmE;L@O>L-pGcx=TW#$`#-btF4n#c0zK=$Aoa_YBMqPPH@5lHz6aajXGb zV|GYDR5~82HML8fsk=XE0gLWcRIrdy;_Ko|1aN8rzK?G=^0+uRJUelF)ml|g{){Ne z@fc8|_`665osK$LNu(A8M2AhHS2&Jr?X6GeIiNAiKGd@4h_vC^2Ov3{Ed{NeAtXV5~%ZlgV#wdk0A7fdV+$IoSkD`m#p zL@LLIB7C44LeR3w_BK(&k0bWz2^9Bfv6NV~0Rfh*D*vrdI2U%a-5Ek^tfGz9ijF7(BzjYr3#+|mNu{NE;aEtnkLoKvsVx+Qq;c`JTQUF%0H#%p`4wh01f%$e`J;< zSQ>zcQc~DgW{O)i=}tUCi_vCzo0iLkDPVwb!t0OG`n$Md67q)W`%~B~)CrSQ>vC}z z@mTPDnj9psSb@UQov-*ekfEt92q;^DoAQuWP>Rc4E_9dO{fy9hoeKwMR7{D^l!J-l zl(x;48)mc7ot=Wtq_c&u4v3F0iXpR>DWK#M@KD{b0D#$bCAe8|n{EkUB|!O1+P?!a zITyY=o>ZTt)INQz{>haJl6vy0Pd&GWD2d@zGgrgzkSwJb=K&62e%zdz*41~NVaAjr z>uPk*cRQ(S)A8t{iw}U1wqxD3``AeITFo6jH_ZvB@GPagk~W)h=-$21PQBb^`7KIp zb?YzBtaa;~Ly&vUk9`@*?jC4>3O1$Et+&H8h|A!ap8WqaiJzX+02&McHNAt{3cfaIEp1ue^XyP_*XW=c8=q#G6T--FBc~y74*d4NlI><}JDC zfjG|4qYtqqBUXFRaUU@dxc@ALq`H>4)A*%;T%gSV|Cl>)fr->kYJy>TII`1l6UPS)e^5h%u+T4IVh)z~2;VY{2|n_NOvS2Wu0nf^M(!|^Z(5K&M#BSdNY>HS;V-Z_Cc2M{KL4BqezoxHbNdOnG2EwmlQ ztik^s<^v>P8*w;F;#|9mW_&PrdgNOG+vo*Kv0qKeMsZ+%w7&1^_!8d$owasm4jRy; z(n32wx5Mi0D>}_PlHBJj?+(yVm&Z-TU72WRnC;|S`2Fph&2nF`=$OXk%DmV-C(WDp zY(^FcOMd9^+BMWd-gi4P4E`lhCfvBN6p_DHPr;D0TToq)S$g+$yQP9g;f~_5MtiH0 zRPL^An&F+5EA#`ff5amobL!dw-4r%LQ$@JxO6UnU=SB9?kz5Tr)Me#-yUcIe^jCtQ zBSJm{dUL6=ZRG{w000LX-{pBaIfnVsRBq~w9k8f)`n<+$y#~3hD!s+$Bbnp-$2R?g z{#$^S>)V5kSFsL=;A~|UhetE-fW$f}x zb|%sk^L!)~hRyxumXO{A7H$mA@7RCSr0&6nm1a&dp z%jx)C@tAfsD1d}^dYVi2CDc-&I|K$8ibmA%sAb}oG>5+yp#~mA2_ncuF{VBCUfnQ>wQaBgFHk>}q}(K6kcpFf9Mc3tq; zLNET7BC6L=_w*$i-Hy&9x2w{1HoQY0&McreaSd)}hj{=r^j;2Sq zXcUPl2;F+E1`OqQU2Glw4tcgiN)$;Jw--NnYd30k5Tb3O2=_t);Fal3KCbEQ20)lB z9QiO#XT-m;IzxWE0-99Yjq7A9x5D&Oy6y5HHQrK#w2Cs03Z_}u8bz?u-I@{-lnrN! zjm>bQy3eW?yS}uojK!;R^}a_O6|}=e%EiSV&9~tun^>QRAzpYl$xah=-^O7V6K@3= z0^l&(RcMQ?4{LQht-sTM^geVMd{&B@T4Frn-Ci{8BzdISI{0DUZI zA87AfV&Xh+7(4Fa;^pwukbAI*N5hq@0krOzh+GH}_gpL1^=xC8>)_>JBm}y7u!ro8 zh?ubssL)7T_p!&z=idw%2StvhtaEaA9s)qC^LPG_*-*{sb}71 zPN*ndSOKqUiOEM$+j0=rQFhrcs$!(@5I%dt6cdi?xMP!H;N*aX)j_x>{OqTb> Date: Fri, 9 Sep 2022 17:45:45 +0000 Subject: [PATCH 14/36] adding environment injection for revoke --- google/auth/pluggable.py | 9 ++++++++- tests/test_pluggable.py | 5 +++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/google/auth/pluggable.py b/google/auth/pluggable.py index dad41d5cc..16c67a87b 100644 --- a/google/auth/pluggable.py +++ b/google/auth/pluggable.py @@ -290,8 +290,15 @@ def revoke(self, request): env["GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE"] = self._audience env["GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE"] = self._subject_token_type env["GOOGLE_EXTERNAL_ACCOUNT_ID"] = self.service_account_email - env["GOOGLE_EXTERNAL_ACCOUNT_REVOKE"] = "1" env["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = "1" + env["GOOGLE_EXTERNAL_ACCOUNT_REVOKE"] = "1" + if self._service_account_impersonation_url is not None: + env[ + "GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL" + ] = self.service_account_email + env[ + "GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE" + ] = self._credential_source_executable_output_file result = subprocess.run( self._credential_source_executable_command.split(), diff --git a/tests/test_pluggable.py b/tests/test_pluggable.py index fa9933a84..f9f384239 100644 --- a/tests/test_pluggable.py +++ b/tests/test_pluggable.py @@ -45,7 +45,6 @@ SUBJECT_TOKEN_FIELD_NAME = "access_token" TOKEN_URL = "https://sts.googleapis.com/v1/token" -SERVICE_ACCOUNT_IMPERSONATION_URL = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/byoid-test@cicpclientproj.iam.gserviceaccount.com:generateAccessToken" SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:jwt" AUDIENCE = "//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID" @@ -1070,7 +1069,9 @@ def test_revoke_successfully(self): ), ): credentials = self.make_pluggable( - credential_source=self.CREDENTIAL_SOURCE, interactive=True + service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, + credential_source=self.CREDENTIAL_SOURCE, + interactive=True, ) _ = credentials.revoke(None) From 1dce93a2de1eee929b803ab4bf11f8405b960d95 Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Fri, 9 Sep 2022 20:24:57 +0000 Subject: [PATCH 15/36] Revert "unify the expiration_time check behavior." This reverts commit f361340defa2f00fe2fb33a58fa50ff908e731ff. --- docs/user-guide.rst | 3 +- google/auth/pluggable.py | 8 ++++++ tests/test_pluggable.py | 61 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/docs/user-guide.rst b/docs/user-guide.rst index 685a6cb70..e689b11c6 100644 --- a/docs/user-guide.rst +++ b/docs/user-guide.rst @@ -450,8 +450,7 @@ Response format fields summary: All response types must include both the ``version`` and ``success`` fields. Successful responses must include the ``token_type``, and one of ``id_token`` or ``saml_response``. -``expiration_time`` is optional. Missing ``expiration_time`` while retrieving -from output file will be treat as "expired" and proceed to run the executable. +If output file is specified, ``expiration_time`` is mandatory. Error responses must include both the ``code`` and ``message`` fields. The library will populate the following environment variables when the diff --git a/google/auth/pluggable.py b/google/auth/pluggable.py index 16c67a87b..b0501bc32 100644 --- a/google/auth/pluggable.py +++ b/google/auth/pluggable.py @@ -369,6 +369,14 @@ def _parse_subject_token(self, response): response["code"], response["message"] ) ) + if ( + "expiration_time" not in response + and not self.interactive + and self._credential_source_executable_output_file + ): + raise ValueError( + "The executable response must contain an expiration_time for successful responses when an output_file has been specified in the configuration in non-interactive mode." + ) if "expiration_time" in response and response["expiration_time"] < time.time(): raise exceptions.RefreshError( "The token returned by the executable is expired." diff --git a/tests/test_pluggable.py b/tests/test_pluggable.py index f9f384239..9ff458885 100644 --- a/tests/test_pluggable.py +++ b/tests/test_pluggable.py @@ -811,7 +811,66 @@ def test_retrieve_subject_token_missing_error_code_message(self): ) @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) - def test_retrieve_subject_token_without_expiration_time_should_pass(self,): + def test_retrieve_subject_token_without_expiration_time_should_fail_when_output_file_specified_non_interactive_mode( + self, + ): + EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE = { + "version": 1, + "success": True, + "token_type": "urn:ietf:params:oauth:token-type:id_token", + "id_token": self.EXECUTABLE_OIDC_TOKEN, + } + + with mock.patch( + "subprocess.run", + return_value=subprocess.CompletedProcess( + args=[], + stdout=json.dumps(EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE).encode("UTF-8"), + returncode=0, + ), + ): + credentials = self.make_pluggable(credential_source=self.CREDENTIAL_SOURCE) + + with pytest.raises(ValueError) as excinfo: + _ = credentials.retrieve_subject_token(None) + + assert excinfo.match( + r"The executable response must contain an expiration_time for successful responses when an output_file has been specified in the configuration." + ) + + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) + def test_retrieve_subject_token_without_expiration_time_should_fail_when_retrieving_from_output_file_non_interactive_mode( + self, tmpdir + ): + ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE = tmpdir.join( + "actual_output_file" + ) + ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE = { + "command": "command", + "timeout_millis": 30000, + "output_file": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, + } + ACTUAL_CREDENTIAL_SOURCE = {"executable": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE} + data = self.EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE_ID_TOKEN.copy() + data.pop("expiration_time") + + with open(ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, "w") as output_file: + json.dump(data, output_file) + + credentials = self.make_pluggable(credential_source=ACTUAL_CREDENTIAL_SOURCE) + + with pytest.raises(ValueError) as excinfo: + _ = credentials.retrieve_subject_token(None) + + assert excinfo.match( + r"The executable response must contain an expiration_time for successful responses when an output_file has been specified in the configuration." + ) + os.remove(ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE) + + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) + def test_retrieve_subject_token_without_expiration_time_should_pass_when_output_file_not_specified( + self, + ): EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE = { "version": 1, "success": True, From e0d20993ee4560ecfc722e5d36dba83638b5f7ef Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Fri, 9 Sep 2022 20:59:56 +0000 Subject: [PATCH 16/36] adding stdin/out to revoke subprocess in case some prompts may needed --- google/auth/pluggable.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/google/auth/pluggable.py b/google/auth/pluggable.py index b0501bc32..22451a3b5 100644 --- a/google/auth/pluggable.py +++ b/google/auth/pluggable.py @@ -304,6 +304,8 @@ def revoke(self, request): self._credential_source_executable_command.split(), timeout=self._credential_source_executable_interactive_timeout_millis / 1000, + stdin=sys.stdin, + stdout=sys.stdout, env=env, ) From e48cc4a325f2a3a4a17e4e621098e1bf54d528e6 Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Mon, 12 Sep 2022 21:26:31 +0000 Subject: [PATCH 17/36] unifying the behavior of reading response for non-interactive and interactive --- google/auth/pluggable.py | 19 ++++---------- tests/test_pluggable.py | 57 ---------------------------------------- 2 files changed, 5 insertions(+), 71 deletions(-) diff --git a/google/auth/pluggable.py b/google/auth/pluggable.py index 22451a3b5..b6909563e 100644 --- a/google/auth/pluggable.py +++ b/google/auth/pluggable.py @@ -247,14 +247,13 @@ def retrieve_subject_token(self, request): result.returncode, result.stdout ) ) - - response = ( - json.load( + + response = json.loads(result.stdout.decode("utf-8")) if result.stdout else None + if not response and self._credential_source_executable_output_file is not None: + response = json.load( open(self._credential_source_executable_output_file, encoding="utf-8") ) - if self.interactive - else json.loads(result.stdout.decode("utf-8")) - ) + subject_token = self._parse_subject_token(response) return subject_token @@ -371,14 +370,6 @@ def _parse_subject_token(self, response): response["code"], response["message"] ) ) - if ( - "expiration_time" not in response - and not self.interactive - and self._credential_source_executable_output_file - ): - raise ValueError( - "The executable response must contain an expiration_time for successful responses when an output_file has been specified in the configuration in non-interactive mode." - ) if "expiration_time" in response and response["expiration_time"] < time.time(): raise exceptions.RefreshError( "The token returned by the executable is expired." diff --git a/tests/test_pluggable.py b/tests/test_pluggable.py index 9ff458885..9448a7809 100644 --- a/tests/test_pluggable.py +++ b/tests/test_pluggable.py @@ -810,63 +810,6 @@ def test_retrieve_subject_token_missing_error_code_message(self): r"Error code and message fields are required in the response." ) - @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) - def test_retrieve_subject_token_without_expiration_time_should_fail_when_output_file_specified_non_interactive_mode( - self, - ): - EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE = { - "version": 1, - "success": True, - "token_type": "urn:ietf:params:oauth:token-type:id_token", - "id_token": self.EXECUTABLE_OIDC_TOKEN, - } - - with mock.patch( - "subprocess.run", - return_value=subprocess.CompletedProcess( - args=[], - stdout=json.dumps(EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE).encode("UTF-8"), - returncode=0, - ), - ): - credentials = self.make_pluggable(credential_source=self.CREDENTIAL_SOURCE) - - with pytest.raises(ValueError) as excinfo: - _ = credentials.retrieve_subject_token(None) - - assert excinfo.match( - r"The executable response must contain an expiration_time for successful responses when an output_file has been specified in the configuration." - ) - - @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) - def test_retrieve_subject_token_without_expiration_time_should_fail_when_retrieving_from_output_file_non_interactive_mode( - self, tmpdir - ): - ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE = tmpdir.join( - "actual_output_file" - ) - ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE = { - "command": "command", - "timeout_millis": 30000, - "output_file": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, - } - ACTUAL_CREDENTIAL_SOURCE = {"executable": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE} - data = self.EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE_ID_TOKEN.copy() - data.pop("expiration_time") - - with open(ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, "w") as output_file: - json.dump(data, output_file) - - credentials = self.make_pluggable(credential_source=ACTUAL_CREDENTIAL_SOURCE) - - with pytest.raises(ValueError) as excinfo: - _ = credentials.retrieve_subject_token(None) - - assert excinfo.match( - r"The executable response must contain an expiration_time for successful responses when an output_file has been specified in the configuration." - ) - os.remove(ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE) - @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) def test_retrieve_subject_token_without_expiration_time_should_pass_when_output_file_not_specified( self, From 27648e76b5f89d3ced3e10354a128eeb34b7ab68 Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Mon, 12 Sep 2022 21:38:45 +0000 Subject: [PATCH 18/36] adding the logic of getting external account id --- google/auth/pluggable.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/google/auth/pluggable.py b/google/auth/pluggable.py index b6909563e..f482fb62f 100644 --- a/google/auth/pluggable.py +++ b/google/auth/pluggable.py @@ -132,6 +132,7 @@ def __init__( self._credential_source_executable_output_file = self._credential_source_executable.get( "output_file" ) + self._tokeninfo_username = kwargs.get("tokeninfo_username", None) if not self._credential_source_executable_command: raise ValueError( @@ -211,7 +212,7 @@ def retrieve_subject_token(self, request): env = os.environ.copy() env["GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE"] = self._audience env["GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE"] = self._subject_token_type - env["GOOGLE_EXTERNAL_ACCOUNT_ID"] = self.service_account_email + env["GOOGLE_EXTERNAL_ACCOUNT_ID"] = self.external_account_id env["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = "1" if self.interactive else "0" env["GOOGLE_EXTERNAL_ACCOUNT_REVOKE"] = 0 @@ -247,7 +248,7 @@ def retrieve_subject_token(self, request): result.returncode, result.stdout ) ) - + response = json.loads(result.stdout.decode("utf-8")) if result.stdout else None if not response and self._credential_source_executable_output_file is not None: response = json.load( @@ -288,7 +289,7 @@ def revoke(self, request): env = os.environ.copy() env["GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE"] = self._audience env["GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE"] = self._subject_token_type - env["GOOGLE_EXTERNAL_ACCOUNT_ID"] = self.service_account_email + env["GOOGLE_EXTERNAL_ACCOUNT_ID"] = self.external_account_id env["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = "1" env["GOOGLE_EXTERNAL_ACCOUNT_REVOKE"] = "1" if self._service_account_impersonation_url is not None: @@ -317,6 +318,19 @@ def revoke(self, request): # TODO: clear cache when the in memory cache feature implemented. + @property + def external_account_id(self): + """Get the GOOGLE_EXTERNAL_ACCOUNT_ID which needs to be polulated to executable + When service account impersonation is used, it will be parsed from the impersonation url + in the form of: + byoid-test@cicpclientproj.iam.gserviceaccount.com + + When no service account impersonation is used, it will be retrieved from the tokeninfo url + in the form of: (Currently phase we populate this variable from gcloud and carried here) + principal://iam.googleapis.com/locations/global/workforcePools/$POOL_ID/subject/john.smith@acme.com + """ + return self.service_account_email or self._tokeninfo_username + @classmethod def from_info(cls, info, **kwargs): """Creates a Pluggable Credentials instance from parsed external account info. From 2515fc7f0fb95061e3c14d9d4b04950150ac9cb6 Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Mon, 12 Sep 2022 23:05:32 +0000 Subject: [PATCH 19/36] update token --- google/auth/pluggable.py | 4 ++-- system_tests/secrets.tar.enc | Bin 10324 -> 10324 bytes 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/google/auth/pluggable.py b/google/auth/pluggable.py index f482fb62f..161f31858 100644 --- a/google/auth/pluggable.py +++ b/google/auth/pluggable.py @@ -325,8 +325,8 @@ def external_account_id(self): in the form of: byoid-test@cicpclientproj.iam.gserviceaccount.com - When no service account impersonation is used, it will be retrieved from the tokeninfo url - in the form of: (Currently phase we populate this variable from gcloud and carried here) + When no service account impersonation is used, it will be retrieved from the token info url + (Currently phase we populate this variable from gcloud and carried here) in the form of: principal://iam.googleapis.com/locations/global/workforcePools/$POOL_ID/subject/john.smith@acme.com """ return self.service_account_email or self._tokeninfo_username diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc index 876b98d6fac3594ea71330bf6d26e5c8d5b1385a..6fe2da815472d710a3c6609ca6f673380b714552 100644 GIT binary patch literal 10324 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 literal 10324 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 From 7d3e7bfa8585ef85fac8bf885215662a92f2d22e Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Tue, 13 Sep 2022 19:56:28 +0000 Subject: [PATCH 20/36] explicity inject subprocess environment variables --- google/auth/pluggable.py | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/google/auth/pluggable.py b/google/auth/pluggable.py index 161f31858..032f2d58a 100644 --- a/google/auth/pluggable.py +++ b/google/auth/pluggable.py @@ -212,18 +212,15 @@ def retrieve_subject_token(self, request): env = os.environ.copy() env["GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE"] = self._audience env["GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE"] = self._subject_token_type - env["GOOGLE_EXTERNAL_ACCOUNT_ID"] = self.external_account_id + env["GOOGLE_EXTERNAL_ACCOUNT_ID"] = self.external_account_id or "" env["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = "1" if self.interactive else "0" env["GOOGLE_EXTERNAL_ACCOUNT_REVOKE"] = 0 - - if self._service_account_impersonation_url is not None: - env[ - "GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL" - ] = self.service_account_email - if self._credential_source_executable_output_file is not None: - env[ - "GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE" - ] = self._credential_source_executable_output_file + env["GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL"] = ( + self.service_account_email or "" + ) + env["GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE"] = ( + self._credential_source_executable_output_file or "" + ) exe_timeout = ( self._credential_source_executable_interactive_timeout_millis / 1000 @@ -289,16 +286,15 @@ def revoke(self, request): env = os.environ.copy() env["GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE"] = self._audience env["GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE"] = self._subject_token_type - env["GOOGLE_EXTERNAL_ACCOUNT_ID"] = self.external_account_id + env["GOOGLE_EXTERNAL_ACCOUNT_ID"] = self.external_account_id or "" env["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = "1" env["GOOGLE_EXTERNAL_ACCOUNT_REVOKE"] = "1" - if self._service_account_impersonation_url is not None: - env[ - "GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL" - ] = self.service_account_email - env[ - "GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE" - ] = self._credential_source_executable_output_file + env["GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL"] = ( + self.service_account_email or "" + ) + env["GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE"] = ( + self._credential_source_executable_output_file or "" + ) result = subprocess.run( self._credential_source_executable_command.split(), From a703e573666b9c1378071ad672ca9ebb52200149 Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Tue, 13 Sep 2022 21:23:19 +0000 Subject: [PATCH 21/36] using a dummy value instead of None as a temporary solution --- google/auth/pluggable.py | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/google/auth/pluggable.py b/google/auth/pluggable.py index 032f2d58a..ab49328c0 100644 --- a/google/auth/pluggable.py +++ b/google/auth/pluggable.py @@ -132,7 +132,8 @@ def __init__( self._credential_source_executable_output_file = self._credential_source_executable.get( "output_file" ) - self._tokeninfo_username = kwargs.get("tokeninfo_username", None) + # TODO: remove this when the tokeninfo endpoint query implemented in auth library + self._tokeninfo_username = kwargs.get("tokeninfo_username", "") # dummy value if not self._credential_source_executable_command: raise ValueError( @@ -212,15 +213,18 @@ def retrieve_subject_token(self, request): env = os.environ.copy() env["GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE"] = self._audience env["GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE"] = self._subject_token_type - env["GOOGLE_EXTERNAL_ACCOUNT_ID"] = self.external_account_id or "" + env["GOOGLE_EXTERNAL_ACCOUNT_ID"] = self.external_account_id env["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = "1" if self.interactive else "0" env["GOOGLE_EXTERNAL_ACCOUNT_REVOKE"] = 0 - env["GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL"] = ( - self.service_account_email or "" - ) - env["GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE"] = ( - self._credential_source_executable_output_file or "" - ) + + if self._service_account_impersonation_url is not None: + env[ + "GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL" + ] = self.service_account_email + if self._credential_source_executable_output_file is not None: + env[ + "GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE" + ] = self._credential_source_executable_output_file exe_timeout = ( self._credential_source_executable_interactive_timeout_millis / 1000 @@ -286,15 +290,16 @@ def revoke(self, request): env = os.environ.copy() env["GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE"] = self._audience env["GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE"] = self._subject_token_type - env["GOOGLE_EXTERNAL_ACCOUNT_ID"] = self.external_account_id or "" + env["GOOGLE_EXTERNAL_ACCOUNT_ID"] = self.external_account_id env["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = "1" env["GOOGLE_EXTERNAL_ACCOUNT_REVOKE"] = "1" - env["GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL"] = ( - self.service_account_email or "" - ) - env["GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE"] = ( - self._credential_source_executable_output_file or "" - ) + if self._service_account_impersonation_url is not None: + env[ + "GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL" + ] = self.service_account_email + env[ + "GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE" + ] = self._credential_source_executable_output_file result = subprocess.run( self._credential_source_executable_command.split(), From 38267f10691445002b17912642fdbef1b48ea88c Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Tue, 13 Sep 2022 22:03:34 +0000 Subject: [PATCH 22/36] fix environment variable injection --- google/auth/pluggable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google/auth/pluggable.py b/google/auth/pluggable.py index ab49328c0..da105da57 100644 --- a/google/auth/pluggable.py +++ b/google/auth/pluggable.py @@ -215,7 +215,7 @@ def retrieve_subject_token(self, request): env["GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE"] = self._subject_token_type env["GOOGLE_EXTERNAL_ACCOUNT_ID"] = self.external_account_id env["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = "1" if self.interactive else "0" - env["GOOGLE_EXTERNAL_ACCOUNT_REVOKE"] = 0 + env["GOOGLE_EXTERNAL_ACCOUNT_REVOKE"] = "0" if self._service_account_impersonation_url is not None: env[ From 5b242dbfc6050fda3ef83c7893b1902991b1a81a Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Tue, 13 Sep 2022 23:34:55 +0000 Subject: [PATCH 23/36] chore: update token --- system_tests/secrets.tar.enc | Bin 10324 -> 10324 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc index 6fe2da815472d710a3c6609ca6f673380b714552..9c05a6d500d15f1f54ee5fa37632cc0b30d300d8 100644 GIT binary patch literal 10324 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 literal 10324 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 From 5082d5a2bd3bdb658f9711f034704078ab3f507a Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Thu, 15 Sep 2022 19:59:43 +0000 Subject: [PATCH 24/36] addressing comments --- docs/user-guide.rst | 3 +- google/auth/pluggable.py | 42 +++++++++++++------- tests/test_pluggable.py | 82 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 111 insertions(+), 16 deletions(-) diff --git a/docs/user-guide.rst b/docs/user-guide.rst index e689b11c6..dbbb5edb7 100644 --- a/docs/user-guide.rst +++ b/docs/user-guide.rst @@ -450,7 +450,8 @@ Response format fields summary: All response types must include both the ``version`` and ``success`` fields. Successful responses must include the ``token_type``, and one of ``id_token`` or ``saml_response``. -If output file is specified, ``expiration_time`` is mandatory. +``expiration_time`` is optional. Missing ``expiration_time`` during retrieve +from file will be treat as "expired". Error responses must include both the ``code`` and ``message`` fields. The library will populate the following environment variables when the diff --git a/google/auth/pluggable.py b/google/auth/pluggable.py index da105da57..79ef2dae8 100644 --- a/google/auth/pluggable.py +++ b/google/auth/pluggable.py @@ -305,31 +305,32 @@ def revoke(self, request): self._credential_source_executable_command.split(), timeout=self._credential_source_executable_interactive_timeout_millis / 1000, - stdin=sys.stdin, - stdout=sys.stdout, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, env=env, ) if result.returncode != 0: raise exceptions.RefreshError( - "Auth revoke failed on executable. Exit with non-zero return code {}".format( - result.returncode + "Auth revoke failed on executable. Exit with non-zero return code {}. Error: {}".format( + result.returncode, result.stdout ) ) - # TODO: clear cache when the in memory cache feature implemented. + response = json.loads(result.stdout.decode("utf-8")) + self._validate_revoke_response(response) @property def external_account_id(self): - """Get the GOOGLE_EXTERNAL_ACCOUNT_ID which needs to be polulated to executable - When service account impersonation is used, it will be parsed from the impersonation url - in the form of: - byoid-test@cicpclientproj.iam.gserviceaccount.com - - When no service account impersonation is used, it will be retrieved from the token info url - (Currently phase we populate this variable from gcloud and carried here) in the form of: - principal://iam.googleapis.com/locations/global/workforcePools/$POOL_ID/subject/john.smith@acme.com + """Returns the external account identifier. + + When service account impersonation is used the identifier is the service + account email. + + Without service account impersonation, this returns None, unless it is + being used by the Google Cloud CLI which populates this field. """ + return self.service_account_email or self._tokeninfo_username @classmethod @@ -400,3 +401,18 @@ def _parse_subject_token(self, response): return response["saml_response"] else: raise exceptions.RefreshError("Executable returned unsupported token type.") + + def _validate_revoke_response(self, response): + if "version" not in response: + raise ValueError("The executable response is missing the version field.") + if response["version"] > EXECUTABLE_SUPPORTED_MAX_VERSION: + raise exceptions.RefreshError( + "Executable returned unsupported version {}.".format( + response["version"] + ) + ) + + if "success" not in response: + raise ValueError("The executable response is missing the success field.") + if not response["success"]: + raise exceptions.RefreshError("Executable returned unsuccessful response.") diff --git a/tests/test_pluggable.py b/tests/test_pluggable.py index 9448a7809..05b3cc100 100644 --- a/tests/test_pluggable.py +++ b/tests/test_pluggable.py @@ -1063,11 +1063,67 @@ def test_revoke_failed_executable(self): assert excinfo.match(r"Auth revoke failed on executable.") @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) - def test_revoke_successfully(self): + def test_revoke_failed_response_validation_missing_version(self): + with mock.patch( + "subprocess.run", + return_value=subprocess.CompletedProcess( + args=[], stdout=json.dumps({}).encode("utf-8"), returncode=0 + ), + ): + credentials = self.make_pluggable( + credential_source=self.CREDENTIAL_SOURCE, interactive=True + ) + with pytest.raises(ValueError) as excinfo: + _ = credentials.revoke(None) + + assert excinfo.match( + r"The executable response is missing the version field." + ) + + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) + def test_revoke_failed_response_validation_invalid_version(self): with mock.patch( "subprocess.run", return_value=subprocess.CompletedProcess( - args=[], stdout=None, returncode=0 + args=[], stdout=json.dumps({"version": 2}).encode("utf-8"), returncode=0 + ), + ): + credentials = self.make_pluggable( + credential_source=self.CREDENTIAL_SOURCE, interactive=True + ) + with pytest.raises(exceptions.RefreshError) as excinfo: + _ = credentials.revoke(None) + + assert excinfo.match(r"Executable returned unsupported version.") + + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) + def test_revoke_failed_response_validation_missing_success(self): + with mock.patch( + "subprocess.run", + return_value=subprocess.CompletedProcess( + args=[], stdout=json.dumps({"version": 1}).encode("utf-8"), returncode=0 + ), + ): + credentials = self.make_pluggable( + credential_source=self.CREDENTIAL_SOURCE, interactive=True + ) + with pytest.raises(ValueError) as excinfo: + _ = credentials.revoke(None) + + assert excinfo.match( + r"The executable response is missing the success field." + ) + + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) + def test_revoke_failed_response_validation_missing_error_code_message(self): + INVALID_REVOKE_RESPONSE = {"version": 1, "success": False} + + with mock.patch( + "subprocess.run", + return_value=subprocess.CompletedProcess( + args=[], + stdout=json.dumps(INVALID_REVOKE_RESPONSE).encode("UTF-8"), + returncode=0, ), ): credentials = self.make_pluggable( @@ -1075,6 +1131,28 @@ def test_revoke_successfully(self): credential_source=self.CREDENTIAL_SOURCE, interactive=True, ) + + with pytest.raises(exceptions.RefreshError) as excinfo: + _ = credentials.revoke(None) + + assert excinfo.match( + r"Executable returned unsuccessful response." + ) + + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) + def test_revoke_successfully(self): + ACTUAL_RESPONSE = {"version": 1, "success": True} + with mock.patch( + "subprocess.run", + return_value=subprocess.CompletedProcess( + args=[], + stdout=json.dumps(ACTUAL_RESPONSE).encode("utf-8"), + returncode=0, + ), + ): + credentials = self.make_pluggable( + credential_source=self.CREDENTIAL_SOURCE, interactive=True + ) _ = credentials.revoke(None) @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) From be691f92b5c4797a0a6e804294946362c94e1a23 Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Thu, 15 Sep 2022 20:19:15 +0000 Subject: [PATCH 25/36] fix lint --- tests/test_pluggable.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_pluggable.py b/tests/test_pluggable.py index 05b3cc100..92c868562 100644 --- a/tests/test_pluggable.py +++ b/tests/test_pluggable.py @@ -1135,9 +1135,7 @@ def test_revoke_failed_response_validation_missing_error_code_message(self): with pytest.raises(exceptions.RefreshError) as excinfo: _ = credentials.revoke(None) - assert excinfo.match( - r"Executable returned unsuccessful response." - ) + assert excinfo.match(r"Executable returned unsuccessful response.") @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) def test_revoke_successfully(self): From 1a3915540be6f79beefb7ddedcdaab9ba8f53325 Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Fri, 16 Sep 2022 00:15:05 +0000 Subject: [PATCH 26/36] chore: update token --- system_tests/secrets.tar.enc | Bin 10324 -> 10324 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc index 9c05a6d500d15f1f54ee5fa37632cc0b30d300d8..85983ddd6d5b71e129e959715f5764e446fb4bf8 100644 GIT binary patch literal 10324 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 From f18009a56f797d724750a8147ebc1bf9fc392960 Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Mon, 19 Sep 2022 23:48:04 +0000 Subject: [PATCH 27/36] refactor the common code between retrieve_subject_token and revoke --- google/auth/pluggable.py | 103 +++++++++++++++++---------------------- tests/test_pluggable.py | 25 +++++++--- 2 files changed, 64 insertions(+), 64 deletions(-) diff --git a/google/auth/pluggable.py b/google/auth/pluggable.py index 79ef2dae8..ab32b76d1 100644 --- a/google/auth/pluggable.py +++ b/google/auth/pluggable.py @@ -165,20 +165,7 @@ def __init__( @_helpers.copy_docstring(external_account.Credentials) def retrieve_subject_token(self, request): - env_allow_executables = os.environ.get( - "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES" - ) - if env_allow_executables != "1": - raise ValueError( - "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( - "An output_file must be specified in the credential configuration for interactive mode." - ) - - if self.interactive and not self.is_workforce_pool: - raise ValueError("Interactive mode is only enabled for workforce pool.") + self._validate_running_mode() # Check output file. if self._credential_source_executable_output_file is not None: @@ -211,21 +198,10 @@ def retrieve_subject_token(self, request): # Inject env vars. env = os.environ.copy() - env["GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE"] = self._audience - env["GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE"] = self._subject_token_type - env["GOOGLE_EXTERNAL_ACCOUNT_ID"] = self.external_account_id - env["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = "1" if self.interactive else "0" + self._inject_env_variables(env) env["GOOGLE_EXTERNAL_ACCOUNT_REVOKE"] = "0" - if self._service_account_impersonation_url is not None: - env[ - "GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL" - ] = self.service_account_email - if self._credential_source_executable_output_file is not None: - env[ - "GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE" - ] = self._credential_source_executable_output_file - + # Run executable exe_timeout = ( self._credential_source_executable_interactive_timeout_millis / 1000 if self.interactive @@ -250,6 +226,7 @@ def retrieve_subject_token(self, request): ) ) + # Handling executable output response = json.loads(result.stdout.decode("utf-8")) if result.stdout else None if not response and self._credential_source_executable_output_file is not None: response = json.load( @@ -270,16 +247,9 @@ def revoke(self, request): not properly executed. """ - env_allow_executables = os.environ.get( - "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES" - ) - if env_allow_executables != "1": - raise ValueError( - "Executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') to run." - ) - if not self.interactive: raise ValueError("Revoke is only enabled under interactive mode.") + self._validate_running_mode() if not _helpers.is_python_3(): raise exceptions.RefreshError( @@ -288,19 +258,10 @@ def revoke(self, request): # Inject variables env = os.environ.copy() - env["GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE"] = self._audience - env["GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE"] = self._subject_token_type - env["GOOGLE_EXTERNAL_ACCOUNT_ID"] = self.external_account_id - env["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = "1" + self._inject_env_variables(env) env["GOOGLE_EXTERNAL_ACCOUNT_REVOKE"] = "1" - if self._service_account_impersonation_url is not None: - env[ - "GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL" - ] = self.service_account_email - env[ - "GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE" - ] = self._credential_source_executable_output_file + # Run executable result = subprocess.run( self._credential_source_executable_command.split(), timeout=self._credential_source_executable_interactive_timeout_millis @@ -365,17 +326,23 @@ def from_file(cls, filename, **kwargs): """ return super(Credentials, cls).from_file(filename, **kwargs) + def _inject_env_variables(self, env): + env["GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE"] = self._audience + env["GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE"] = self._subject_token_type + env["GOOGLE_EXTERNAL_ACCOUNT_ID"] = self.external_account_id + env["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = "1" if self.interactive else "0" + + if self._service_account_impersonation_url is not None: + env[ + "GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL" + ] = self.service_account_email + if self._credential_source_executable_output_file is not None: + env[ + "GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE" + ] = self._credential_source_executable_output_file + def _parse_subject_token(self, response): - if "version" not in response: - raise ValueError("The executable response is missing the version field.") - if response["version"] > EXECUTABLE_SUPPORTED_MAX_VERSION: - raise exceptions.RefreshError( - "Executable returned unsupported version {}.".format( - response["version"] - ) - ) - if "success" not in response: - raise ValueError("The executable response is missing the success field.") + self._validate_response_schema(response) if not response["success"]: if "code" not in response or "message" not in response: raise ValueError( @@ -403,6 +370,11 @@ def _parse_subject_token(self, response): raise exceptions.RefreshError("Executable returned unsupported token type.") def _validate_revoke_response(self, response): + self._validate_response_schema(response) + if not response["success"]: + raise exceptions.RefreshError("Executable returned unsuccessful response.") + + def _validate_response_schema(self, response): if "version" not in response: raise ValueError("The executable response is missing the version field.") if response["version"] > EXECUTABLE_SUPPORTED_MAX_VERSION: @@ -414,5 +386,20 @@ def _validate_revoke_response(self, response): if "success" not in response: raise ValueError("The executable response is missing the success field.") - if not response["success"]: - raise exceptions.RefreshError("Executable returned unsuccessful response.") + + def _validate_running_mode(self): + env_allow_executables = os.environ.get( + "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES" + ) + if env_allow_executables != "1": + raise ValueError( + "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( + "An output_file must be specified in the credential configuration for interactive mode." + ) + + if self.interactive and not self.is_workforce_pool: + raise ValueError("Interactive mode is only enabled for workforce pool.") diff --git a/tests/test_pluggable.py b/tests/test_pluggable.py index 92c868562..db7a065f0 100644 --- a/tests/test_pluggable.py +++ b/tests/test_pluggable.py @@ -1032,7 +1032,9 @@ def test_retrieve_subject_token_executable_fail_interactive_mode(self): @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "0"}) def test_revoke_failed_executable_not_allowed(self): - credentials = self.make_pluggable(credential_source=self.CREDENTIAL_SOURCE) + credentials = self.make_pluggable( + credential_source=self.CREDENTIAL_SOURCE, interactive=True + ) with pytest.raises(ValueError) as excinfo: _ = credentials.revoke(None) @@ -1055,7 +1057,9 @@ def test_revoke_failed_executable(self): ), ): credentials = self.make_pluggable( - credential_source=self.CREDENTIAL_SOURCE, interactive=True + audience=WORKFORCE_AUDIENCE, + credential_source=self.CREDENTIAL_SOURCE, + interactive=True, ) with pytest.raises(exceptions.RefreshError) as excinfo: _ = credentials.revoke(None) @@ -1071,7 +1075,9 @@ def test_revoke_failed_response_validation_missing_version(self): ), ): credentials = self.make_pluggable( - credential_source=self.CREDENTIAL_SOURCE, interactive=True + audience=WORKFORCE_AUDIENCE, + credential_source=self.CREDENTIAL_SOURCE, + interactive=True, ) with pytest.raises(ValueError) as excinfo: _ = credentials.revoke(None) @@ -1089,7 +1095,9 @@ def test_revoke_failed_response_validation_invalid_version(self): ), ): credentials = self.make_pluggable( - credential_source=self.CREDENTIAL_SOURCE, interactive=True + audience=WORKFORCE_AUDIENCE, + credential_source=self.CREDENTIAL_SOURCE, + interactive=True, ) with pytest.raises(exceptions.RefreshError) as excinfo: _ = credentials.revoke(None) @@ -1105,7 +1113,9 @@ def test_revoke_failed_response_validation_missing_success(self): ), ): credentials = self.make_pluggable( - credential_source=self.CREDENTIAL_SOURCE, interactive=True + audience=WORKFORCE_AUDIENCE, + credential_source=self.CREDENTIAL_SOURCE, + interactive=True, ) with pytest.raises(ValueError) as excinfo: _ = credentials.revoke(None) @@ -1127,6 +1137,7 @@ def test_revoke_failed_response_validation_missing_error_code_message(self): ), ): credentials = self.make_pluggable( + audience=WORKFORCE_AUDIENCE, service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, credential_source=self.CREDENTIAL_SOURCE, interactive=True, @@ -1149,7 +1160,9 @@ def test_revoke_successfully(self): ), ): credentials = self.make_pluggable( - credential_source=self.CREDENTIAL_SOURCE, interactive=True + audience=WORKFORCE_AUDIENCE, + credential_source=self.CREDENTIAL_SOURCE, + interactive=True, ) _ = credentials.revoke(None) From 65869a2dcb5cec349e6ffe6d110828f0f97b24f9 Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Tue, 20 Sep 2022 20:26:55 +0000 Subject: [PATCH 28/36] chore: update token --- system_tests/secrets.tar.enc | Bin 10324 -> 10324 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc index 85983ddd6d5b71e129e959715f5764e446fb4bf8..fa53f6eece0a4410b873f1b300ba4078f76b74df 100644 GIT binary patch literal 10324 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 literal 10324 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 m%(EMmarz$M> Date: Wed, 21 Sep 2022 20:56:02 +0000 Subject: [PATCH 29/36] refactoring tests --- docs/user-guide.rst | 11 ++- google/auth/pluggable.py | 7 +- tests/test_pluggable.py | 179 ++++++++++++++------------------------- 3 files changed, 74 insertions(+), 123 deletions(-) diff --git a/docs/user-guide.rst b/docs/user-guide.rst index dbbb5edb7..682b58a76 100644 --- a/docs/user-guide.rst +++ b/docs/user-guide.rst @@ -434,8 +434,10 @@ Response format fields summary: - ``version``: The version of the JSON output. Currently only version 1 is supported. - ``success``: The status of the response. - - When true, the response must contain the 3rd party token, token type, and expiration. The executable must also exit with exit code 0. - - When false, the response must contain the error code and message fields and exit with a non-zero value. + - When true, the response must contain the 3rd party token, token type, and + expiration. The executable must also exit with exit code 0. + - When false, the response must contain the error code and message fields + and exit with a non-zero value. - ``token_type``: The 3rd party subject token type. Must be - *urn:ietf:params:oauth:token-type:jwt* - *urn:ietf:params:oauth:token-type:id_token* @@ -450,8 +452,9 @@ Response format fields summary: All response types must include both the ``version`` and ``success`` fields. Successful responses must include the ``token_type``, and one of ``id_token`` or ``saml_response``. -``expiration_time`` is optional. Missing ``expiration_time`` during retrieve -from file will be treat as "expired". +``expiration_time`` is optional. If the output file does not contain the +``expiration_time`` field, the response will be considered expired and the +executable will be called. Error responses must include both the ``code`` and ``message`` fields. The library will populate the following environment variables when the diff --git a/google/auth/pluggable.py b/google/auth/pluggable.py index ab32b76d1..6be8222c1 100644 --- a/google/auth/pluggable.py +++ b/google/auth/pluggable.py @@ -132,7 +132,6 @@ def __init__( self._credential_source_executable_output_file = self._credential_source_executable.get( "output_file" ) - # TODO: remove this when the tokeninfo endpoint query implemented in auth library self._tokeninfo_username = kwargs.get("tokeninfo_username", "") # dummy value if not self._credential_source_executable_command: @@ -201,7 +200,7 @@ def retrieve_subject_token(self, request): self._inject_env_variables(env) env["GOOGLE_EXTERNAL_ACCOUNT_REVOKE"] = "0" - # Run executable + # Run executable. exe_timeout = ( self._credential_source_executable_interactive_timeout_millis / 1000 if self.interactive @@ -226,7 +225,7 @@ def retrieve_subject_token(self, request): ) ) - # Handling executable output + # Handle executable output. response = json.loads(result.stdout.decode("utf-8")) if result.stdout else None if not response and self._credential_source_executable_output_file is not None: response = json.load( @@ -372,7 +371,7 @@ def _parse_subject_token(self, response): def _validate_revoke_response(self, response): self._validate_response_schema(response) if not response["success"]: - raise exceptions.RefreshError("Executable returned unsuccessful response.") + raise exceptions.RefreshError("Revoke failed with unsuccessful response.") def _validate_response_schema(self, response): if "version" not in response: diff --git a/tests/test_pluggable.py b/tests/test_pluggable.py index db7a065f0..af71d8f58 100644 --- a/tests/test_pluggable.py +++ b/tests/test_pluggable.py @@ -283,51 +283,8 @@ def test_info_with_credential_source(self): "credential_source": self.CREDENTIAL_SOURCE, } - @mock.patch.dict( - os.environ, - { - "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1", - "GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE": "original_audience", - "GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE": "original_token_type", - "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE": "0", - "GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL": "original_impersonated_email", - "GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE": "original_output_file", - }, - ) - def test_retrieve_subject_token_oidc_id_token(self): - with mock.patch( - "subprocess.run", - return_value=subprocess.CompletedProcess( - args=[], - stdout=json.dumps( - self.EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE_ID_TOKEN - ).encode("UTF-8"), - returncode=0, - ), - ): - credentials = self.make_pluggable( - audience=AUDIENCE, - service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, - credential_source=self.CREDENTIAL_SOURCE, - ) - - subject_token = credentials.retrieve_subject_token(None) - - assert subject_token == self.EXECUTABLE_OIDC_TOKEN - - @mock.patch.dict( - os.environ, - { - "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1", - "GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE": "original_audience", - "GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE": "original_token_type", - "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE": "1", - "GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL": "original_impersonated_email", - "GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE": "original_output_file", - }, - ) - def test_retrieve_subject_token_oidc_id_token_interactive_mode(self, tmpdir): - + @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) + def test_retrieve_subject_token_successfully(self, tmpdir): ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE = tmpdir.join( "actual_output_file" ) @@ -337,80 +294,72 @@ def test_retrieve_subject_token_oidc_id_token_interactive_mode(self, tmpdir): "output_file": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, } ACTUAL_CREDENTIAL_SOURCE = {"executable": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE} - with open(ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, "w") as output_file: - json.dump( - self.EXECUTABLE_SUCCESSFUL_OIDC_NO_EXPIRATION_TIME_RESPONSE_JWT, - output_file, - ) - - with mock.patch( - "subprocess.run", - return_value=subprocess.CompletedProcess(args=[], returncode=0), - ): - credentials = self.make_pluggable( - audience=WORKFORCE_AUDIENCE, - service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, - credential_source=ACTUAL_CREDENTIAL_SOURCE, - interactive=True, - ) - - subject_token = credentials.retrieve_subject_token(None) - - assert subject_token == self.EXECUTABLE_OIDC_TOKEN - @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) - def test_retrieve_subject_token_oidc_jwt(self): - with mock.patch( - "subprocess.run", - return_value=subprocess.CompletedProcess( - args=[], - stdout=json.dumps(self.EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE_JWT).encode( + testData = { + "subject_token_oidc_id_token": { + "stdout": json.dumps( + self.EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE_ID_TOKEN + ).encode("UTF-8"), + "impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL, + "file_content": self.EXECUTABLE_SUCCESSFUL_OIDC_NO_EXPIRATION_TIME_RESPONSE_ID_TOKEN, + "expect_token": self.EXECUTABLE_OIDC_TOKEN, + }, + "subject_token_oidc_id_token_interacitve_mode": { + "audience": WORKFORCE_AUDIENCE, + "file_content": self.EXECUTABLE_SUCCESSFUL_OIDC_NO_EXPIRATION_TIME_RESPONSE_ID_TOKEN, + "interactive": True, + "expect_token": self.EXECUTABLE_OIDC_TOKEN, + }, + "subject_token_oidc_jwt": { + "stdout": json.dumps( + self.EXECUTABLE_SUCCESSFUL_OIDC_RESPONSE_JWT + ).encode("UTF-8"), + "impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL, + "file_content": self.EXECUTABLE_SUCCESSFUL_OIDC_NO_EXPIRATION_TIME_RESPONSE_JWT, + "expect_token": self.EXECUTABLE_OIDC_TOKEN, + }, + "subject_token_oidc_jwt_interactive_mode": { + "audience": WORKFORCE_AUDIENCE, + "file_content": self.EXECUTABLE_SUCCESSFUL_OIDC_NO_EXPIRATION_TIME_RESPONSE_JWT, + "interactive": True, + "expect_token": self.EXECUTABLE_OIDC_TOKEN, + }, + "subject_token_saml": { + "stdout": json.dumps(self.EXECUTABLE_SUCCESSFUL_SAML_RESPONSE).encode( "UTF-8" ), - returncode=0, - ), - ): - credentials = self.make_pluggable( - audience=AUDIENCE, - service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, - credential_source=self.CREDENTIAL_SOURCE, - ) - - subject_token = credentials.retrieve_subject_token(None) - - assert subject_token == self.EXECUTABLE_OIDC_TOKEN - - @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) - def test_retrieve_subject_token_oidc_jwt_interactive_mode(self, tmpdir): - ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE = tmpdir.join( - "actual_output_file" - ) - ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE = { - "command": "command", - "interactive_timeout_millis": 300000, - "output_file": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, + "impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL, + "file_content": self.EXECUTABLE_SUCCESSFUL_SAML_NO_EXPIRATION_TIME_RESPONSE, + "expect_token": self.EXECUTABLE_SAML_TOKEN, + }, + "subject_token_saml_interactive_mode": { + "audience": WORKFORCE_AUDIENCE, + "file_content": self.EXECUTABLE_SUCCESSFUL_SAML_NO_EXPIRATION_TIME_RESPONSE, + "interactive": True, + "expect_token": self.EXECUTABLE_SAML_TOKEN, + }, } - ACTUAL_CREDENTIAL_SOURCE = {"executable": ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE} - with open(ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, "w") as output_file: - json.dump( - self.EXECUTABLE_SUCCESSFUL_OIDC_NO_EXPIRATION_TIME_RESPONSE_JWT, - output_file, - ) - - with mock.patch( - "subprocess.run", - return_value=subprocess.CompletedProcess(args=[], returncode=0), - ): - credentials = self.make_pluggable( - audience=WORKFORCE_AUDIENCE, - service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, - credential_source=ACTUAL_CREDENTIAL_SOURCE, - interactive=True, - ) - subject_token = credentials.retrieve_subject_token(None) + for data in testData.values(): + with open( + ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE, "w" + ) as output_file: + json.dump(data.get("file_content"), output_file) - assert subject_token == self.EXECUTABLE_OIDC_TOKEN + with mock.patch( + "subprocess.run", + return_value=subprocess.CompletedProcess( + args=[], stdout=data.get("stdout"), returncode=0 + ), + ): + credentials = self.make_pluggable( + audience=data.get("audience", AUDIENCE), + service_account_impersonation_url=data.get("impersonation_url"), + credential_source=ACTUAL_CREDENTIAL_SOURCE, + interactive=data.get("interactive", False), + ) + subject_token = credentials.retrieve_subject_token(None) + assert subject_token == data.get("expect_token") os.remove(ACTUAL_CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE) @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) @@ -1125,7 +1074,7 @@ def test_revoke_failed_response_validation_missing_success(self): ) @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) - def test_revoke_failed_response_validation_missing_error_code_message(self): + def test_revoke_failed_response_validation_with_success_field_is_false(self): INVALID_REVOKE_RESPONSE = {"version": 1, "success": False} with mock.patch( @@ -1146,7 +1095,7 @@ def test_revoke_failed_response_validation_missing_error_code_message(self): with pytest.raises(exceptions.RefreshError) as excinfo: _ = credentials.revoke(None) - assert excinfo.match(r"Executable returned unsuccessful response.") + assert excinfo.match(r"Revoke failed with unsuccessful response.") @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) def test_revoke_successfully(self): From 7c39fd892166f2ecc9715aeebfa0a3e274619a39 Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Wed, 21 Sep 2022 21:25:59 +0000 Subject: [PATCH 30/36] chore: update token --- system_tests/secrets.tar.enc | Bin 10324 -> 10324 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc index fa53f6eece0a4410b873f1b300ba4078f76b74df..6dc14d78e8d934436d54ec752357b030ac692e0d 100644 GIT binary patch literal 10324 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 literal 10324 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 From 7de8944b0bafd96545e4ced016b4529456e395cd Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Thu, 22 Sep 2022 20:59:53 +0000 Subject: [PATCH 31/36] refactor tests --- tests/test_pluggable.py | 155 +++++++++++++--------------------------- 1 file changed, 51 insertions(+), 104 deletions(-) diff --git a/tests/test_pluggable.py b/tests/test_pluggable.py index af71d8f58..293d5c6ed 100644 --- a/tests/test_pluggable.py +++ b/tests/test_pluggable.py @@ -990,112 +990,59 @@ def test_revoke_failed_executable_not_allowed(self): assert excinfo.match(r"Executables need to be explicitly allowed") @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) - def test_revoke_failed_non_interactive_mode(self): - credentials = self.make_pluggable(credential_source=self.CREDENTIAL_SOURCE) - with pytest.raises(ValueError) as excinfo: - _ = credentials.revoke(None) - - assert excinfo.match(r"Revoke is only enabled under interactive mode.") - - @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) - def test_revoke_failed_executable(self): - with mock.patch( - "subprocess.run", - return_value=subprocess.CompletedProcess( - args=[], stdout=None, returncode=1 - ), - ): - credentials = self.make_pluggable( - audience=WORKFORCE_AUDIENCE, - credential_source=self.CREDENTIAL_SOURCE, - interactive=True, - ) - with pytest.raises(exceptions.RefreshError) as excinfo: - _ = credentials.revoke(None) - - assert excinfo.match(r"Auth revoke failed on executable.") - - @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) - def test_revoke_failed_response_validation_missing_version(self): - with mock.patch( - "subprocess.run", - return_value=subprocess.CompletedProcess( - args=[], stdout=json.dumps({}).encode("utf-8"), returncode=0 - ), - ): - credentials = self.make_pluggable( - audience=WORKFORCE_AUDIENCE, - credential_source=self.CREDENTIAL_SOURCE, - interactive=True, - ) - with pytest.raises(ValueError) as excinfo: - _ = credentials.revoke(None) - - assert excinfo.match( - r"The executable response is missing the version field." - ) - - @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) - def test_revoke_failed_response_validation_invalid_version(self): - with mock.patch( - "subprocess.run", - return_value=subprocess.CompletedProcess( - args=[], stdout=json.dumps({"version": 2}).encode("utf-8"), returncode=0 - ), - ): - credentials = self.make_pluggable( - audience=WORKFORCE_AUDIENCE, - credential_source=self.CREDENTIAL_SOURCE, - interactive=True, - ) - with pytest.raises(exceptions.RefreshError) as excinfo: - _ = credentials.revoke(None) - - assert excinfo.match(r"Executable returned unsupported version.") - - @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) - def test_revoke_failed_response_validation_missing_success(self): - with mock.patch( - "subprocess.run", - return_value=subprocess.CompletedProcess( - args=[], stdout=json.dumps({"version": 1}).encode("utf-8"), returncode=0 - ), - ): - credentials = self.make_pluggable( - audience=WORKFORCE_AUDIENCE, - credential_source=self.CREDENTIAL_SOURCE, - interactive=True, - ) - with pytest.raises(ValueError) as excinfo: - _ = credentials.revoke(None) - - assert excinfo.match( - r"The executable response is missing the success field." - ) - - @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) - def test_revoke_failed_response_validation_with_success_field_is_false(self): - INVALID_REVOKE_RESPONSE = {"version": 1, "success": False} - - with mock.patch( - "subprocess.run", - return_value=subprocess.CompletedProcess( - args=[], - stdout=json.dumps(INVALID_REVOKE_RESPONSE).encode("UTF-8"), - returncode=0, - ), - ): - credentials = self.make_pluggable( - audience=WORKFORCE_AUDIENCE, - service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, - credential_source=self.CREDENTIAL_SOURCE, - interactive=True, - ) + def test_revoke_failed(self): + testData = { + "non_interactive_mode": { + "interactive": False, + "expectErrType": ValueError, + "expectErrPattern": r"Revoke is only enabled under interactive mode.", + }, + "executable_failed": { + "returncode": 1, + "expectErrType": exceptions.RefreshError, + "expectErrPattern": r"Auth revoke failed on executable.", + }, + "response_validation_missing_version": { + "response": {}, + "expectErrType": ValueError, + "expectErrPattern": r"The executable response is missing the version field.", + }, + "response_validation_invalid_version": { + "response": {"version": 2}, + "expectErrType": exceptions.RefreshError, + "expectErrPattern": r"Executable returned unsupported version.", + }, + "response_validation_missing_success": { + "response": {"version": 1}, + "expectErrType": ValueError, + "expectErrPattern": r"The executable response is missing the success field.", + }, + "response_validation_failed_with_success_field_is_false": { + "response": {"version": 1, "success": False}, + "expectErrType": exceptions.RefreshError, + "expectErrPattern": r"Revoke failed with unsuccessful response.", + }, + } + for data in testData.values(): + with mock.patch( + "subprocess.run", + return_value=subprocess.CompletedProcess( + args=[], + stdout=json.dumps(data.get("response")).encode("UTF-8"), + returncode=data.get("returncode", 0), + ), + ): + credentials = self.make_pluggable( + audience=WORKFORCE_AUDIENCE, + service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, + credential_source=self.CREDENTIAL_SOURCE, + interactive=data.get("interactive", True), + ) - with pytest.raises(exceptions.RefreshError) as excinfo: - _ = credentials.revoke(None) + with pytest.raises(data.get("expectErrType")) as excinfo: + _ = credentials.revoke(None) - assert excinfo.match(r"Revoke failed with unsuccessful response.") + assert excinfo.match(data.get("expectErrPattern")) @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1"}) def test_revoke_successfully(self): From 06fff40ad9050651024e7b61cbfc5da22fa27f9b Mon Sep 17 00:00:00 2001 From: Carl Lundin <108372512+clundin25@users.noreply.github.com> Date: Thu, 22 Sep 2022 21:52:10 +0000 Subject: [PATCH 32/36] feat: Retry behavior (#1113) * feat: Retry behavior * Introduce `retryable` property to auth library exceptions. This can be used to determine if an exception should be retried. * Introduce `should_retry` parameter to token endpoints. If set to `False` the auth library will not retry failed requests. If set to `True` the auth library will retry failed requests. The default value is `True` to maintain existing behavior. * Expanded list of HTTP Status codes that will be retried. * Modified retry behavior to use exponential backoff. * Increased default retry attempts from 2 to 3. --- google/auth/_exponential_backoff.py | 111 ++++++++++++ google/auth/exceptions.py | 17 +- google/auth/transport/__init__.py | 14 +- google/oauth2/_client.py | 171 +++++++++++++----- google/oauth2/_client_async.py | 95 ++++++---- google/oauth2/_reauth_async.py | 5 +- google/oauth2/reauth.py | 12 +- tests/compute_engine/test_credentials.py | 2 +- tests/oauth2/test__client.py | 192 ++++++++++++++++++-- tests/oauth2/test_reauth.py | 11 +- tests/test__exponential_backoff.py | 41 +++++ tests/test_exceptions.py | 55 ++++++ tests_async/oauth2/test__client_async.py | 214 +++++++++++++++++++++-- tests_async/oauth2/test_reauth_async.py | 16 +- 14 files changed, 841 insertions(+), 115 deletions(-) create mode 100644 google/auth/_exponential_backoff.py create mode 100644 tests/test__exponential_backoff.py create mode 100644 tests/test_exceptions.py diff --git a/google/auth/_exponential_backoff.py b/google/auth/_exponential_backoff.py new file mode 100644 index 000000000..b5801bec9 --- /dev/null +++ b/google/auth/_exponential_backoff.py @@ -0,0 +1,111 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +import time + +import six + +# The default amount of retry attempts +_DEFAULT_RETRY_TOTAL_ATTEMPTS = 3 + +# The default initial backoff period (1.0 second). +_DEFAULT_INITIAL_INTERVAL_SECONDS = 1.0 + +# The default randomization factor (0.1 which results in a random period ranging +# between 10% below and 10% above the retry interval). +_DEFAULT_RANDOMIZATION_FACTOR = 0.1 + +# The default multiplier value (2 which is 100% increase per back off). +_DEFAULT_MULTIPLIER = 2.0 + +"""Exponential Backoff Utility + +This is a private module that implements the exponential back off algorithm. +It can be used as a utility for code that needs to retry on failure, for example +an HTTP request. +""" + + +class ExponentialBackoff(six.Iterator): + """An exponential backoff iterator. This can be used in a for loop to + perform requests with exponential backoff. + + Args: + total_attempts Optional[int]: + The maximum amount of retries that should happen. + The default value is 3 attempts. + initial_wait_seconds Optional[int]: + The amount of time to sleep in the first backoff. This parameter + should be in seconds. + The default value is 1 second. + randomization_factor Optional[float]: + The amount of jitter that should be in each backoff. For example, + a value of 0.1 will introduce a jitter range of 10% to the + current backoff period. + The default value is 0.1. + multiplier Optional[float]: + The backoff multipler. This adjusts how much each backoff will + increase. For example a value of 2.0 leads to a 200% backoff + on each attempt. If the initial_wait is 1.0 it would look like + this sequence [1.0, 2.0, 4.0, 8.0]. + The default value is 2.0. + """ + + def __init__( + self, + total_attempts=_DEFAULT_RETRY_TOTAL_ATTEMPTS, + initial_wait_seconds=_DEFAULT_INITIAL_INTERVAL_SECONDS, + randomization_factor=_DEFAULT_RANDOMIZATION_FACTOR, + multiplier=_DEFAULT_MULTIPLIER, + ): + self._total_attempts = total_attempts + self._initial_wait_seconds = initial_wait_seconds + + self._current_wait_in_seconds = self._initial_wait_seconds + + self._randomization_factor = randomization_factor + self._multiplier = multiplier + self._backoff_count = 0 + + def __iter__(self): + self._backoff_count = 0 + self._current_wait_in_seconds = self._initial_wait_seconds + return self + + def __next__(self): + if self._backoff_count >= self._total_attempts: + raise StopIteration + self._backoff_count += 1 + + jitter_variance = self._current_wait_in_seconds * self._randomization_factor + jitter = random.uniform( + self._current_wait_in_seconds - jitter_variance, + self._current_wait_in_seconds + jitter_variance, + ) + + time.sleep(jitter) + + self._current_wait_in_seconds *= self._multiplier + return self._backoff_count + + @property + def total_attempts(self): + """The total amount of backoff attempts that will be made.""" + return self._total_attempts + + @property + def backoff_count(self): + """The current amount of backoff attempts that have been made.""" + return self._backoff_count diff --git a/google/auth/exceptions.py b/google/auth/exceptions.py index e9e737780..7760c87b8 100644 --- a/google/auth/exceptions.py +++ b/google/auth/exceptions.py @@ -18,6 +18,15 @@ class GoogleAuthError(Exception): """Base class for all google.auth errors.""" + def __init__(self, *args, **kwargs): + super(GoogleAuthError, self).__init__(*args) + retryable = kwargs.get("retryable", False) + self._retryable = retryable + + @property + def retryable(self): + return self._retryable + class TransportError(GoogleAuthError): """Used to indicate an error occurred during an HTTP request.""" @@ -44,6 +53,10 @@ class MutualTLSChannelError(GoogleAuthError): class ClientCertError(GoogleAuthError): """Used to indicate that client certificate is missing or invalid.""" + @property + def retryable(self): + return False + class OAuthError(GoogleAuthError): """Used to indicate an error occurred during an OAuth related HTTP @@ -53,9 +66,9 @@ class OAuthError(GoogleAuthError): class ReauthFailError(RefreshError): """An exception for when reauth failed.""" - def __init__(self, message=None): + def __init__(self, message=None, **kwargs): super(ReauthFailError, self).__init__( - "Reauthentication failed. {0}".format(message) + "Reauthentication failed. {0}".format(message), **kwargs ) diff --git a/google/auth/transport/__init__.py b/google/auth/transport/__init__.py index 374e7b4d7..8334145a1 100644 --- a/google/auth/transport/__init__.py +++ b/google/auth/transport/__init__.py @@ -29,9 +29,21 @@ import six from six.moves import http_client +TOO_MANY_REQUESTS = 429 # Python 2.7 six is missing this status code. + +DEFAULT_RETRYABLE_STATUS_CODES = ( + http_client.INTERNAL_SERVER_ERROR, + http_client.SERVICE_UNAVAILABLE, + http_client.REQUEST_TIMEOUT, + TOO_MANY_REQUESTS, +) +"""Sequence[int]: HTTP status codes indicating a request can be retried. +""" + + DEFAULT_REFRESH_STATUS_CODES = (http_client.UNAUTHORIZED,) """Sequence[int]: Which HTTP status code indicate that credentials should be -refreshed and a request should be retried. +refreshed. """ DEFAULT_MAX_REFRESH_ATTEMPTS = 2 diff --git a/google/oauth2/_client.py b/google/oauth2/_client.py index 847c5db8a..7f866d446 100644 --- a/google/oauth2/_client.py +++ b/google/oauth2/_client.py @@ -30,9 +30,11 @@ from six.moves import http_client from six.moves import urllib +from google.auth import _exponential_backoff from google.auth import _helpers from google.auth import exceptions from google.auth import jwt +from google.auth import transport _URLENCODED_CONTENT_TYPE = "application/x-www-form-urlencoded" _JSON_CONTENT_TYPE = "application/json" @@ -40,17 +42,22 @@ _REFRESH_GRANT_TYPE = "refresh_token" -def _handle_error_response(response_data): +def _handle_error_response(response_data, retryable_error): """Translates an error response into an exception. Args: response_data (Mapping | str): The decoded response data. + retryable_error Optional[bool]: A boolean indicating if an error is retryable. + Defaults to False. Raises: google.auth.exceptions.RefreshError: The errors contained in response_data. """ + + retryable_error = retryable_error if retryable_error else False + if isinstance(response_data, six.string_types): - raise exceptions.RefreshError(response_data) + raise exceptions.RefreshError(response_data, retryable=retryable_error) try: error_details = "{}: {}".format( response_data["error"], response_data.get("error_description") @@ -59,7 +66,45 @@ def _handle_error_response(response_data): except (KeyError, ValueError): error_details = json.dumps(response_data) - raise exceptions.RefreshError(error_details, response_data) + raise exceptions.RefreshError( + error_details, response_data, retryable=retryable_error + ) + + +def _can_retry(status_code, response_data): + """Checks if a request can be retried by inspecting the status code + and response body of the request. + + Args: + status_code (int): The response status code. + response_data (Mapping | str): The decoded response data. + + Returns: + bool: True if the response is retryable. False otherwise. + """ + if status_code in transport.DEFAULT_RETRYABLE_STATUS_CODES: + return True + + try: + # For a failed response, response_body could be a string + error_desc = response_data.get("error_description") or "" + error_code = response_data.get("error") or "" + + # Per Oauth 2.0 RFC https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.2.1 + # This is needed because a redirect will not return a 500 status code. + retryable_error_descriptions = { + "internal_failure", + "server_error", + "temporarily_unavailable", + } + + if any(e in retryable_error_descriptions for e in (error_code, error_desc)): + return True + + except AttributeError: + pass + + return False def _parse_expiry(response_data): @@ -81,7 +126,13 @@ def _parse_expiry(response_data): def _token_endpoint_request_no_throw( - request, token_uri, body, access_token=None, use_json=False, **kwargs + request, + token_uri, + body, + access_token=None, + use_json=False, + can_retry=True, + **kwargs ): """Makes a request to the OAuth 2.0 authorization server's token endpoint. This function doesn't throw on response errors. @@ -95,6 +146,7 @@ def _token_endpoint_request_no_throw( access_token (Optional(str)): The access token needed to make the request. use_json (Optional(bool)): Use urlencoded format or json format for the content type. The default value is False. + can_retry (bool): Enable or disable request retry behavior. kwargs: Additional arguments passed on to the request method. The kwargs will be passed to `requests.request` method, see: https://docs.python-requests.org/en/latest/api/#requests.request. @@ -104,8 +156,10 @@ def _token_endpoint_request_no_throw( side SSL certificate verification. Returns: - Tuple(bool, Mapping[str, str]): A boolean indicating if the request is - successful, and a mapping for the JSON-decoded response data. + Tuple(bool, Mapping[str, str], Optional[bool]): A boolean indicating + if the request is successful, a mapping for the JSON-decoded response + data and in the case of an error a boolean indicating if the error + is retryable. """ if use_json: headers = {"Content-Type": _JSON_CONTENT_TYPE} @@ -117,10 +171,7 @@ def _token_endpoint_request_no_throw( if access_token: headers["Authorization"] = "Bearer {}".format(access_token) - retry = 0 - # retry to fetch token for maximum of two times if any internal failure - # occurs. - while True: + def _perform_request(): response = request( method="POST", url=token_uri, headers=headers, body=body, **kwargs ) @@ -129,32 +180,44 @@ def _token_endpoint_request_no_throw( if hasattr(response.data, "decode") else response.data ) - - if response.status == http_client.OK: + response_data = "" + try: # response_body should be a JSON response_data = json.loads(response_body) - break - else: - # For a failed response, response_body could be a string - try: - response_data = json.loads(response_body) - error_desc = response_data.get("error_description") or "" - error_code = response_data.get("error") or "" - if ( - any(e == "internal_failure" for e in (error_code, error_desc)) - and retry < 1 - ): - retry += 1 - continue - except ValueError: - response_data = response_body - return False, response_data - - return True, response_data + except ValueError: + response_data = response_body + + if response.status == http_client.OK: + return True, response_data, None + + retryable_error = _can_retry( + status_code=response.status, response_data=response_data + ) + + return False, response_data, retryable_error + + request_succeeded, response_data, retryable_error = _perform_request() + + if request_succeeded or not retryable_error or not can_retry: + return request_succeeded, response_data, retryable_error + + retries = _exponential_backoff.ExponentialBackoff() + for _ in retries: + request_succeeded, response_data, retryable_error = _perform_request() + if request_succeeded or not retryable_error: + return request_succeeded, response_data, retryable_error + + return False, response_data, retryable_error def _token_endpoint_request( - request, token_uri, body, access_token=None, use_json=False, **kwargs + request, + token_uri, + body, + access_token=None, + use_json=False, + can_retry=True, + **kwargs ): """Makes a request to the OAuth 2.0 authorization server's token endpoint. @@ -167,6 +230,7 @@ def _token_endpoint_request( access_token (Optional(str)): The access token needed to make the request. use_json (Optional(bool)): Use urlencoded format or json format for the content type. The default value is False. + can_retry (bool): Enable or disable request retry behavior. kwargs: Additional arguments passed on to the request method. The kwargs will be passed to `requests.request` method, see: https://docs.python-requests.org/en/latest/api/#requests.request. @@ -182,15 +246,22 @@ def _token_endpoint_request( google.auth.exceptions.RefreshError: If the token endpoint returned an error. """ - response_status_ok, response_data = _token_endpoint_request_no_throw( - request, token_uri, body, access_token=access_token, use_json=use_json, **kwargs + + response_status_ok, response_data, retryable_error = _token_endpoint_request_no_throw( + request, + token_uri, + body, + access_token=access_token, + use_json=use_json, + can_retry=can_retry, + **kwargs ) if not response_status_ok: - _handle_error_response(response_data) + _handle_error_response(response_data, retryable_error) return response_data -def jwt_grant(request, token_uri, assertion): +def jwt_grant(request, token_uri, assertion, can_retry=True): """Implements the JWT Profile for OAuth 2.0 Authorization Grants. For more details, see `rfc7523 section 4`_. @@ -201,6 +272,7 @@ def jwt_grant(request, token_uri, assertion): token_uri (str): The OAuth 2.0 authorizations server's token endpoint URI. assertion (str): The OAuth 2.0 assertion. + can_retry (bool): Enable or disable request retry behavior. Returns: Tuple[str, Optional[datetime], Mapping[str, str]]: The access token, @@ -214,12 +286,16 @@ def jwt_grant(request, token_uri, assertion): """ body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE} - response_data = _token_endpoint_request(request, token_uri, body) + response_data = _token_endpoint_request( + request, token_uri, body, can_retry=can_retry + ) try: access_token = response_data["access_token"] except KeyError as caught_exc: - new_exc = exceptions.RefreshError("No access token in response.", response_data) + new_exc = exceptions.RefreshError( + "No access token in response.", response_data, retryable=False + ) six.raise_from(new_exc, caught_exc) expiry = _parse_expiry(response_data) @@ -227,7 +303,7 @@ def jwt_grant(request, token_uri, assertion): return access_token, expiry, response_data -def id_token_jwt_grant(request, token_uri, assertion): +def id_token_jwt_grant(request, token_uri, assertion, can_retry=True): """Implements the JWT Profile for OAuth 2.0 Authorization Grants, but requests an OpenID Connect ID Token instead of an access token. @@ -242,6 +318,7 @@ def id_token_jwt_grant(request, token_uri, assertion): URI. assertion (str): JWT token signed by a service account. The token's payload must include a ``target_audience`` claim. + can_retry (bool): Enable or disable request retry behavior. Returns: Tuple[str, Optional[datetime], Mapping[str, str]]: @@ -254,12 +331,16 @@ def id_token_jwt_grant(request, token_uri, assertion): """ body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE} - response_data = _token_endpoint_request(request, token_uri, body) + response_data = _token_endpoint_request( + request, token_uri, body, can_retry=can_retry + ) try: id_token = response_data["id_token"] except KeyError as caught_exc: - new_exc = exceptions.RefreshError("No ID token in response.", response_data) + new_exc = exceptions.RefreshError( + "No ID token in response.", response_data, retryable=False + ) six.raise_from(new_exc, caught_exc) payload = jwt.decode(id_token, verify=False) @@ -288,7 +369,9 @@ def _handle_refresh_grant_response(response_data, refresh_token): try: access_token = response_data["access_token"] except KeyError as caught_exc: - new_exc = exceptions.RefreshError("No access token in response.", response_data) + new_exc = exceptions.RefreshError( + "No access token in response.", response_data, retryable=False + ) six.raise_from(new_exc, caught_exc) refresh_token = response_data.get("refresh_token", refresh_token) @@ -305,6 +388,7 @@ def refresh_grant( client_secret, scopes=None, rapt_token=None, + can_retry=True, ): """Implements the OAuth 2.0 refresh token grant. @@ -324,6 +408,7 @@ def refresh_grant( token has a wild card scope (e.g. 'https://www.googleapis.com/auth/any-api'). rapt_token (Optional(str)): The reauth Proof Token. + can_retry (bool): Enable or disable request retry behavior. Returns: Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access @@ -347,5 +432,7 @@ def refresh_grant( if rapt_token: body["rapt"] = rapt_token - response_data = _token_endpoint_request(request, token_uri, body) + response_data = _token_endpoint_request( + request, token_uri, body, can_retry=can_retry + ) return _handle_refresh_grant_response(response_data, refresh_token) diff --git a/google/oauth2/_client_async.py b/google/oauth2/_client_async.py index cf5121137..428084a70 100644 --- a/google/oauth2/_client_async.py +++ b/google/oauth2/_client_async.py @@ -30,13 +30,14 @@ from six.moves import http_client from six.moves import urllib +from google.auth import _exponential_backoff from google.auth import exceptions from google.auth import jwt from google.oauth2 import _client as client async def _token_endpoint_request_no_throw( - request, token_uri, body, access_token=None, use_json=False + request, token_uri, body, access_token=None, use_json=False, can_retry=True ): """Makes a request to the OAuth 2.0 authorization server's token endpoint. This function doesn't throw on response errors. @@ -50,10 +51,13 @@ async def _token_endpoint_request_no_throw( access_token (Optional(str)): The access token needed to make the request. use_json (Optional(bool)): Use urlencoded format or json format for the content type. The default value is False. + can_retry (bool): Enable or disable request retry behavior. Returns: - Tuple(bool, Mapping[str, str]): A boolean indicating if the request is - successful, and a mapping for the JSON-decoded response data. + Tuple(bool, Mapping[str, str], Optional[bool]): A boolean indicating + if the request is successful, a mapping for the JSON-decoded response + data and in the case of an error a boolean indicating if the error + is retryable. """ if use_json: headers = {"Content-Type": client._JSON_CONTENT_TYPE} @@ -65,11 +69,7 @@ async def _token_endpoint_request_no_throw( if access_token: headers["Authorization"] = "Bearer {}".format(access_token) - retry = 0 - # retry to fetch token for maximum of two times if any internal failure - # occurs. - while True: - + async def _perform_request(): response = await request( method="POST", url=token_uri, headers=headers, body=body ) @@ -83,26 +83,36 @@ async def _token_endpoint_request_no_throw( else response_body1 ) - response_data = json.loads(response_body) + try: + response_data = json.loads(response_body) + except ValueError: + response_data = response_body if response.status == http_client.OK: - break - else: - error_desc = response_data.get("error_description") or "" - error_code = response_data.get("error") or "" - if ( - any(e == "internal_failure" for e in (error_code, error_desc)) - and retry < 1 - ): - retry += 1 - continue - return response.status == http_client.OK, response_data + return True, response_data, None + + retryable_error = client._can_retry( + status_code=response.status, response_data=response_data + ) + + return False, response_data, retryable_error + + request_succeeded, response_data, retryable_error = await _perform_request() + + if request_succeeded or not retryable_error or not can_retry: + return request_succeeded, response_data, retryable_error + + retries = _exponential_backoff.ExponentialBackoff() + for _ in retries: + request_succeeded, response_data, retryable_error = await _perform_request() + if request_succeeded or not retryable_error: + return request_succeeded, response_data, retryable_error - return response.status == http_client.OK, response_data + return False, response_data, retryable_error async def _token_endpoint_request( - request, token_uri, body, access_token=None, use_json=False + request, token_uri, body, access_token=None, use_json=False, can_retry=True ): """Makes a request to the OAuth 2.0 authorization server's token endpoint. @@ -115,6 +125,7 @@ async def _token_endpoint_request( access_token (Optional(str)): The access token needed to make the request. use_json (Optional(bool)): Use urlencoded format or json format for the content type. The default value is False. + can_retry (bool): Enable or disable request retry behavior. Returns: Mapping[str, str]: The JSON-decoded response data. @@ -123,15 +134,21 @@ async def _token_endpoint_request( google.auth.exceptions.RefreshError: If the token endpoint returned an error. """ - response_status_ok, response_data = await _token_endpoint_request_no_throw( - request, token_uri, body, access_token=access_token, use_json=use_json + + response_status_ok, response_data, retryable_error = await _token_endpoint_request_no_throw( + request, + token_uri, + body, + access_token=access_token, + use_json=use_json, + can_retry=can_retry, ) if not response_status_ok: - client._handle_error_response(response_data) + client._handle_error_response(response_data, retryable_error) return response_data -async def jwt_grant(request, token_uri, assertion): +async def jwt_grant(request, token_uri, assertion, can_retry=True): """Implements the JWT Profile for OAuth 2.0 Authorization Grants. For more details, see `rfc7523 section 4`_. @@ -142,6 +159,7 @@ async def jwt_grant(request, token_uri, assertion): token_uri (str): The OAuth 2.0 authorizations server's token endpoint URI. assertion (str): The OAuth 2.0 assertion. + can_retry (bool): Enable or disable request retry behavior. Returns: Tuple[str, Optional[datetime], Mapping[str, str]]: The access token, @@ -155,12 +173,16 @@ async def jwt_grant(request, token_uri, assertion): """ body = {"assertion": assertion, "grant_type": client._JWT_GRANT_TYPE} - response_data = await _token_endpoint_request(request, token_uri, body) + response_data = await _token_endpoint_request( + request, token_uri, body, can_retry=can_retry + ) try: access_token = response_data["access_token"] except KeyError as caught_exc: - new_exc = exceptions.RefreshError("No access token in response.", response_data) + new_exc = exceptions.RefreshError( + "No access token in response.", response_data, retryable=False + ) six.raise_from(new_exc, caught_exc) expiry = client._parse_expiry(response_data) @@ -168,7 +190,7 @@ async def jwt_grant(request, token_uri, assertion): return access_token, expiry, response_data -async def id_token_jwt_grant(request, token_uri, assertion): +async def id_token_jwt_grant(request, token_uri, assertion, can_retry=True): """Implements the JWT Profile for OAuth 2.0 Authorization Grants, but requests an OpenID Connect ID Token instead of an access token. @@ -183,6 +205,7 @@ async def id_token_jwt_grant(request, token_uri, assertion): URI. assertion (str): JWT token signed by a service account. The token's payload must include a ``target_audience`` claim. + can_retry (bool): Enable or disable request retry behavior. Returns: Tuple[str, Optional[datetime], Mapping[str, str]]: @@ -195,12 +218,16 @@ async def id_token_jwt_grant(request, token_uri, assertion): """ body = {"assertion": assertion, "grant_type": client._JWT_GRANT_TYPE} - response_data = await _token_endpoint_request(request, token_uri, body) + response_data = await _token_endpoint_request( + request, token_uri, body, can_retry=can_retry + ) try: id_token = response_data["id_token"] except KeyError as caught_exc: - new_exc = exceptions.RefreshError("No ID token in response.", response_data) + new_exc = exceptions.RefreshError( + "No ID token in response.", response_data, retryable=False + ) six.raise_from(new_exc, caught_exc) payload = jwt.decode(id_token, verify=False) @@ -217,6 +244,7 @@ async def refresh_grant( client_secret, scopes=None, rapt_token=None, + can_retry=True, ): """Implements the OAuth 2.0 refresh token grant. @@ -236,6 +264,7 @@ async def refresh_grant( token has a wild card scope (e.g. 'https://www.googleapis.com/auth/any-api'). rapt_token (Optional(str)): The reauth Proof Token. + can_retry (bool): Enable or disable request retry behavior. Returns: Tuple[str, Optional[str], Optional[datetime], Mapping[str, str]]: The @@ -259,5 +288,7 @@ async def refresh_grant( if rapt_token: body["rapt"] = rapt_token - response_data = await _token_endpoint_request(request, token_uri, body) + response_data = await _token_endpoint_request( + request, token_uri, body, can_retry=can_retry + ) return client._handle_refresh_grant_response(response_data, refresh_token) diff --git a/google/oauth2/_reauth_async.py b/google/oauth2/_reauth_async.py index 30b0b0b1e..6b69c6e67 100644 --- a/google/oauth2/_reauth_async.py +++ b/google/oauth2/_reauth_async.py @@ -292,7 +292,7 @@ async def refresh_grant( if rapt_token: body["rapt"] = rapt_token - response_status_ok, response_data = await _client_async._token_endpoint_request_no_throw( + response_status_ok, response_data, retryable_error = await _client_async._token_endpoint_request_no_throw( request, token_uri, body ) if ( @@ -317,12 +317,13 @@ async def refresh_grant( ( response_status_ok, response_data, + retryable_error, ) = await _client_async._token_endpoint_request_no_throw( request, token_uri, body ) if not response_status_ok: - _client._handle_error_response(response_data) + _client._handle_error_response(response_data, retryable_error) refresh_response = _client._handle_refresh_grant_response( response_data, refresh_token ) diff --git a/google/oauth2/reauth.py b/google/oauth2/reauth.py index 2c32bda2a..ad2ad1b2e 100644 --- a/google/oauth2/reauth.py +++ b/google/oauth2/reauth.py @@ -319,7 +319,7 @@ def refresh_grant( if rapt_token: body["rapt"] = rapt_token - response_status_ok, response_data = _client._token_endpoint_request_no_throw( + response_status_ok, response_data, retryable_error = _client._token_endpoint_request_no_throw( request, token_uri, body ) if ( @@ -339,12 +339,14 @@ def refresh_grant( request, client_id, client_secret, refresh_token, token_uri, scopes=scopes ) body["rapt"] = rapt_token - (response_status_ok, response_data) = _client._token_endpoint_request_no_throw( - request, token_uri, body - ) + ( + response_status_ok, + response_data, + retryable_error, + ) = _client._token_endpoint_request_no_throw(request, token_uri, body) if not response_status_ok: - _client._handle_error_response(response_data) + _client._handle_error_response(response_data, retryable_error) return _client._handle_refresh_grant_response(response_data, refresh_token) + ( rapt_token, ) diff --git a/tests/compute_engine/test_credentials.py b/tests/compute_engine/test_credentials.py index ff01720c4..ebce176e8 100644 --- a/tests/compute_engine/test_credentials.py +++ b/tests/compute_engine/test_credentials.py @@ -609,7 +609,7 @@ def test_refresh_error(self, sign, get, utcnow): request = mock.create_autospec(transport.Request, instance=True) response = mock.Mock() response.data = b'{"error": "http error"}' - response.status = 500 + response.status = 404 # Throw a 404 so the request is not retried. request.side_effect = [response] self.credentials = credentials.IDTokenCredentials( diff --git a/tests/oauth2/test__client.py b/tests/oauth2/test__client.py index bd4cc5001..13c42dc52 100644 --- a/tests/oauth2/test__client.py +++ b/tests/oauth2/test__client.py @@ -47,12 +47,14 @@ ) -def test__handle_error_response(): +@pytest.mark.parametrize("retryable", [True, False]) +def test__handle_error_response(retryable): response_data = {"error": "help", "error_description": "I'm alive"} with pytest.raises(exceptions.RefreshError) as excinfo: - _client._handle_error_response(response_data) + _client._handle_error_response(response_data, retryable) + assert excinfo.value.retryable == retryable assert excinfo.match(r"help: I\'m alive") @@ -60,8 +62,9 @@ def test__handle_error_response_no_error(): response_data = {"foo": "bar"} with pytest.raises(exceptions.RefreshError) as excinfo: - _client._handle_error_response(response_data) + _client._handle_error_response(response_data, False) + assert not excinfo.value.retryable assert excinfo.match(r"{\"foo\": \"bar\"}") @@ -69,11 +72,33 @@ def test__handle_error_response_not_json(): response_data = "this is an error message" with pytest.raises(exceptions.RefreshError) as excinfo: - _client._handle_error_response(response_data) + _client._handle_error_response(response_data, False) + assert not excinfo.value.retryable assert excinfo.match(response_data) +def test__can_retry_retryable(): + retryable_codes = transport.DEFAULT_RETRYABLE_STATUS_CODES + for status_code in range(100, 600): + if status_code in retryable_codes: + assert _client._can_retry(status_code, {"error": "invalid_scope"}) + else: + assert not _client._can_retry(status_code, {"error": "invalid_scope"}) + + +@pytest.mark.parametrize( + "response_data", [{"error": "internal_failure"}, {"error": "server_error"}] +) +def test__can_retry_message(response_data): + assert _client._can_retry(http_client.OK, response_data) + + +@pytest.mark.parametrize("response_data", [{"error": "invalid_scope"}]) +def test__can_retry_no_retry_message(response_data): + assert not _client._can_retry(http_client.OK, response_data) + + @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) def test__parse_expiry(unused_utcnow): result = _client._parse_expiry({"expires_in": 500}) @@ -154,8 +179,8 @@ def test__token_endpoint_request_internal_failure_error(): _client._token_endpoint_request( request, "http://example.com", {"error_description": "internal_failure"} ) - # request should be called twice due to the retry - assert request.call_count == 2 + # request should be called once and then with 3 retries + assert request.call_count == 4 request = make_request( {"error": "internal_failure"}, status=http_client.BAD_REQUEST @@ -165,7 +190,55 @@ def test__token_endpoint_request_internal_failure_error(): _client._token_endpoint_request( request, "http://example.com", {"error": "internal_failure"} ) - # request should be called twice due to the retry + # request should be called once and then with 3 retries + assert request.call_count == 4 + + +def test__token_endpoint_request_internal_failure_and_retry_failure_error(): + retryable_error = mock.create_autospec(transport.Response, instance=True) + retryable_error.status = http_client.BAD_REQUEST + retryable_error.data = json.dumps({"error_description": "internal_failure"}).encode( + "utf-8" + ) + + unretryable_error = mock.create_autospec(transport.Response, instance=True) + unretryable_error.status = http_client.BAD_REQUEST + unretryable_error.data = json.dumps({"error_description": "invalid_scope"}).encode( + "utf-8" + ) + + request = mock.create_autospec(transport.Request) + + request.side_effect = [retryable_error, retryable_error, unretryable_error] + + with pytest.raises(exceptions.RefreshError): + _client._token_endpoint_request( + request, "http://example.com", {"error_description": "invalid_scope"} + ) + # request should be called three times. Two retryable errors and one + # unretryable error to break the retry loop. + assert request.call_count == 3 + + +def test__token_endpoint_request_internal_failure_and_retry_succeeds(): + retryable_error = mock.create_autospec(transport.Response, instance=True) + retryable_error.status = http_client.BAD_REQUEST + retryable_error.data = json.dumps({"error_description": "internal_failure"}).encode( + "utf-8" + ) + + response = mock.create_autospec(transport.Response, instance=True) + response.status = http_client.OK + response.data = json.dumps({"hello": "world"}).encode("utf-8") + + request = mock.create_autospec(transport.Request) + + request.side_effect = [retryable_error, response] + + _ = _client._token_endpoint_request( + request, "http://example.com", {"test": "params"} + ) + assert request.call_count == 2 @@ -219,8 +292,9 @@ def test_jwt_grant_no_access_token(): } ) - with pytest.raises(exceptions.RefreshError): + with pytest.raises(exceptions.RefreshError) as excinfo: _client.jwt_grant(request, "http://example.com", "assertion_value") + assert not excinfo.value.retryable def test_id_token_jwt_grant(): @@ -255,8 +329,9 @@ def test_id_token_jwt_grant_no_access_token(): } ) - with pytest.raises(exceptions.RefreshError): + with pytest.raises(exceptions.RefreshError) as excinfo: _client.id_token_jwt_grant(request, "http://example.com", "assertion_value") + assert not excinfo.value.retryable @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) @@ -348,7 +423,104 @@ def test_refresh_grant_no_access_token(): } ) - with pytest.raises(exceptions.RefreshError): + with pytest.raises(exceptions.RefreshError) as excinfo: _client.refresh_grant( request, "http://example.com", "refresh_token", "client_id", "client_secret" ) + assert not excinfo.value.retryable + + +@mock.patch("google.oauth2._client._parse_expiry", return_value=None) +@mock.patch.object(_client, "_token_endpoint_request", autospec=True) +def test_jwt_grant_retry_default(mock_token_endpoint_request, mock_expiry): + _client.jwt_grant(mock.Mock(), mock.Mock(), mock.Mock()) + mock_token_endpoint_request.assert_called_with( + mock.ANY, mock.ANY, mock.ANY, can_retry=True + ) + + +@pytest.mark.parametrize("can_retry", [True, False]) +@mock.patch("google.oauth2._client._parse_expiry", return_value=None) +@mock.patch.object(_client, "_token_endpoint_request", autospec=True) +def test_jwt_grant_retry_with_retry( + mock_token_endpoint_request, mock_expiry, can_retry +): + _client.jwt_grant(mock.Mock(), mock.Mock(), mock.Mock(), can_retry=can_retry) + mock_token_endpoint_request.assert_called_with( + mock.ANY, mock.ANY, mock.ANY, can_retry=can_retry + ) + + +@mock.patch("google.auth.jwt.decode", return_value={"exp": 0}) +@mock.patch.object(_client, "_token_endpoint_request", autospec=True) +def test_id_token_jwt_grant_retry_default(mock_token_endpoint_request, mock_jwt_decode): + _client.id_token_jwt_grant(mock.Mock(), mock.Mock(), mock.Mock()) + mock_token_endpoint_request.assert_called_with( + mock.ANY, mock.ANY, mock.ANY, can_retry=True + ) + + +@pytest.mark.parametrize("can_retry", [True, False]) +@mock.patch("google.auth.jwt.decode", return_value={"exp": 0}) +@mock.patch.object(_client, "_token_endpoint_request", autospec=True) +def test_id_token_jwt_grant_retry_with_retry( + mock_token_endpoint_request, mock_jwt_decode, can_retry +): + _client.id_token_jwt_grant( + mock.Mock(), mock.Mock(), mock.Mock(), can_retry=can_retry + ) + mock_token_endpoint_request.assert_called_with( + mock.ANY, mock.ANY, mock.ANY, can_retry=can_retry + ) + + +@mock.patch("google.oauth2._client._parse_expiry", return_value=None) +@mock.patch.object(_client, "_token_endpoint_request", autospec=True) +def test_refresh_grant_retry_default(mock_token_endpoint_request, mock_parse_expiry): + _client.refresh_grant( + mock.Mock(), mock.Mock(), mock.Mock(), mock.Mock(), mock.Mock() + ) + mock_token_endpoint_request.assert_called_with( + mock.ANY, mock.ANY, mock.ANY, can_retry=True + ) + + +@pytest.mark.parametrize("can_retry", [True, False]) +@mock.patch("google.oauth2._client._parse_expiry", return_value=None) +@mock.patch.object(_client, "_token_endpoint_request", autospec=True) +def test_refresh_grant_retry_with_retry( + mock_token_endpoint_request, mock_parse_expiry, can_retry +): + _client.refresh_grant( + mock.Mock(), + mock.Mock(), + mock.Mock(), + mock.Mock(), + mock.Mock(), + can_retry=can_retry, + ) + mock_token_endpoint_request.assert_called_with( + mock.ANY, mock.ANY, mock.ANY, can_retry=can_retry + ) + + +@pytest.mark.parametrize("can_retry", [True, False]) +def test__token_endpoint_request_no_throw_with_retry(can_retry): + response_data = {"error": "help", "error_description": "I'm alive"} + body = "dummy body" + + mock_response = mock.create_autospec(transport.Response, instance=True) + mock_response.status = http_client.INTERNAL_SERVER_ERROR + mock_response.data = json.dumps(response_data).encode("utf-8") + + mock_request = mock.create_autospec(transport.Request) + mock_request.return_value = mock_response + + _client._token_endpoint_request_no_throw( + mock_request, mock.Mock(), body, mock.Mock(), mock.Mock(), can_retry=can_retry + ) + + if can_retry: + assert mock_request.call_count == 4 + else: + assert mock_request.call_count == 1 diff --git a/tests/oauth2/test_reauth.py b/tests/oauth2/test_reauth.py index ae64be009..df0636b18 100644 --- a/tests/oauth2/test_reauth.py +++ b/tests/oauth2/test_reauth.py @@ -260,7 +260,7 @@ def test_refresh_grant_failed(): with mock.patch( "google.oauth2._client._token_endpoint_request_no_throw" ) as mock_token_request: - mock_token_request.return_value = (False, {"error": "Bad request"}) + mock_token_request.return_value = (False, {"error": "Bad request"}, False) with pytest.raises(exceptions.RefreshError) as excinfo: reauth.refresh_grant( MOCK_REQUEST, @@ -273,6 +273,7 @@ def test_refresh_grant_failed(): enable_reauth_refresh=True, ) assert excinfo.match(r"Bad request") + assert not excinfo.value.retryable mock_token_request.assert_called_with( MOCK_REQUEST, "token_uri", @@ -292,8 +293,8 @@ def test_refresh_grant_success(): "google.oauth2._client._token_endpoint_request_no_throw" ) as mock_token_request: mock_token_request.side_effect = [ - (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}), - (True, {"access_token": "access_token"}), + (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}, True), + (True, {"access_token": "access_token"}, None), ] with mock.patch( "google.oauth2.reauth.get_rapt_token", return_value="new_rapt_token" @@ -319,8 +320,8 @@ def test_refresh_grant_reauth_refresh_disabled(): "google.oauth2._client._token_endpoint_request_no_throw" ) as mock_token_request: mock_token_request.side_effect = [ - (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}), - (True, {"access_token": "access_token"}), + (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}, True), + (True, {"access_token": "access_token"}, None), ] with pytest.raises(exceptions.RefreshError) as excinfo: reauth.refresh_grant( diff --git a/tests/test__exponential_backoff.py b/tests/test__exponential_backoff.py new file mode 100644 index 000000000..06a54527e --- /dev/null +++ b/tests/test__exponential_backoff.py @@ -0,0 +1,41 @@ +# Copyright 2022 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import mock + +from google.auth import _exponential_backoff + + +@mock.patch("time.sleep", return_value=None) +def test_exponential_backoff(mock_time): + eb = _exponential_backoff.ExponentialBackoff() + curr_wait = eb._current_wait_in_seconds + iteration_count = 0 + + for attempt in eb: + backoff_interval = mock_time.call_args[0][0] + jitter = curr_wait * eb._randomization_factor + + assert (curr_wait - jitter) <= backoff_interval <= (curr_wait + jitter) + assert attempt == iteration_count + 1 + assert eb.backoff_count == iteration_count + 1 + assert eb._current_wait_in_seconds == eb._multiplier ** (iteration_count + 1) + + curr_wait = eb._current_wait_in_seconds + iteration_count += 1 + + assert eb.total_attempts == _exponential_backoff._DEFAULT_RETRY_TOTAL_ATTEMPTS + assert eb.backoff_count == _exponential_backoff._DEFAULT_RETRY_TOTAL_ATTEMPTS + assert iteration_count == _exponential_backoff._DEFAULT_RETRY_TOTAL_ATTEMPTS + assert mock_time.call_count == _exponential_backoff._DEFAULT_RETRY_TOTAL_ATTEMPTS diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py new file mode 100644 index 000000000..6f542498f --- /dev/null +++ b/tests/test_exceptions.py @@ -0,0 +1,55 @@ +# Copyright 2022 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest # type: ignore + +from google.auth import exceptions # type:ignore + + +@pytest.fixture( + params=[ + exceptions.GoogleAuthError, + exceptions.TransportError, + exceptions.RefreshError, + exceptions.UserAccessTokenError, + exceptions.DefaultCredentialsError, + exceptions.MutualTLSChannelError, + exceptions.OAuthError, + exceptions.ReauthFailError, + exceptions.ReauthSamlChallengeFailError, + ] +) +def retryable_exception(request): + return request.param + + +@pytest.fixture(params=[exceptions.ClientCertError]) +def non_retryable_exception(request): + return request.param + + +def test_default_retryable_exceptions(retryable_exception): + assert not retryable_exception().retryable + + +@pytest.mark.parametrize("retryable", [True, False]) +def test_retryable_exceptions(retryable_exception, retryable): + retryable_exception = retryable_exception(retryable=retryable) + assert retryable_exception.retryable == retryable + + +@pytest.mark.parametrize("retryable", [True, False]) +def test_non_retryable_exceptions(non_retryable_exception, retryable): + non_retryable_exception = non_retryable_exception(retryable=retryable) + assert not non_retryable_exception.retryable diff --git a/tests_async/oauth2/test__client_async.py b/tests_async/oauth2/test__client_async.py index 91874cdd4..402083672 100644 --- a/tests_async/oauth2/test__client_async.py +++ b/tests_async/oauth2/test__client_async.py @@ -29,10 +29,10 @@ from tests.oauth2 import test__client as test_client -def make_request(response_data, status=http_client.OK): +def make_request(response_data, status=http_client.OK, text=False): response = mock.AsyncMock(spec=["transport.Response"]) response.status = status - data = json.dumps(response_data).encode("utf-8") + data = response_data if text else json.dumps(response_data).encode("utf-8") response.data = mock.AsyncMock(spec=["__call__", "read"]) response.data.read = mock.AsyncMock(spec=["__call__"], return_value=data) response.content = mock.AsyncMock(spec=["__call__"], return_value=data) @@ -62,6 +62,27 @@ async def test__token_endpoint_request(): assert result == {"test": "response"} +@pytest.mark.asyncio +async def test__token_endpoint_request_text(): + + request = make_request("response", text=True) + + result = await _client._token_endpoint_request( + request, "http://example.com", {"test": "params"} + ) + + # Check request call + request.assert_called_with( + method="POST", + url="http://example.com", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + body="test=params".encode("utf-8"), + ) + + # Check result + assert result == "response" + + @pytest.mark.asyncio async def test__token_endpoint_request_json(): @@ -95,8 +116,9 @@ async def test__token_endpoint_request_json(): async def test__token_endpoint_request_error(): request = make_request({}, status=http_client.BAD_REQUEST) - with pytest.raises(exceptions.RefreshError): + with pytest.raises(exceptions.RefreshError) as excinfo: await _client._token_endpoint_request(request, "http://example.com", {}) + assert not excinfo.value.retryable @pytest.mark.asyncio @@ -105,10 +127,11 @@ async def test__token_endpoint_request_internal_failure_error(): {"error_description": "internal_failure"}, status=http_client.BAD_REQUEST ) - with pytest.raises(exceptions.RefreshError): + with pytest.raises(exceptions.RefreshError) as excinfo: await _client._token_endpoint_request( request, "http://example.com", {"error_description": "internal_failure"} ) + assert excinfo.value.retryable request = make_request( {"error": "internal_failure"}, status=http_client.BAD_REQUEST @@ -118,6 +141,61 @@ async def test__token_endpoint_request_internal_failure_error(): await _client._token_endpoint_request( request, "http://example.com", {"error": "internal_failure"} ) + assert excinfo.value.retryable + + +@pytest.mark.asyncio +async def test__token_endpoint_request_internal_failure_and_retry_failure_error(): + retryable_error = mock.AsyncMock(spec=["transport.Response"]) + retryable_error.status = http_client.BAD_REQUEST + data = json.dumps({"error_description": "internal_failure"}).encode("utf-8") + retryable_error.data = mock.AsyncMock(spec=["__call__", "read"]) + retryable_error.data.read = mock.AsyncMock(spec=["__call__"], return_value=data) + retryable_error.content = mock.AsyncMock(spec=["__call__"], return_value=data) + + unretryable_error = mock.AsyncMock(spec=["transport.Response"]) + unretryable_error.status = http_client.BAD_REQUEST + data = json.dumps({"error_description": "invalid_scope"}).encode("utf-8") + unretryable_error.data = mock.AsyncMock(spec=["__call__", "read"]) + unretryable_error.data.read = mock.AsyncMock(spec=["__call__"], return_value=data) + unretryable_error.content = mock.AsyncMock(spec=["__call__"], return_value=data) + + request = mock.AsyncMock(spec=["transport.Request"]) + request.side_effect = [retryable_error, retryable_error, unretryable_error] + + with pytest.raises(exceptions.RefreshError): + await _client._token_endpoint_request( + request, "http://example.com", {"error_description": "invalid_scope"} + ) + # request should be called three times. Two retryable errors and one + # unretryable error to break the retry loop. + assert request.call_count == 3 + + +@pytest.mark.asyncio +async def test__token_endpoint_request_internal_failure_and_retry_succeeds(): + retryable_error = mock.AsyncMock(spec=["transport.Response"]) + retryable_error.status = http_client.BAD_REQUEST + data = json.dumps({"error_description": "internal_failure"}).encode("utf-8") + retryable_error.data = mock.AsyncMock(spec=["__call__", "read"]) + retryable_error.data.read = mock.AsyncMock(spec=["__call__"], return_value=data) + retryable_error.content = mock.AsyncMock(spec=["__call__"], return_value=data) + + response = mock.AsyncMock(spec=["transport.Response"]) + response.status = http_client.OK + data = json.dumps({"hello": "world"}).encode("utf-8") + response.data = mock.AsyncMock(spec=["__call__", "read"]) + response.data.read = mock.AsyncMock(spec=["__call__"], return_value=data) + response.content = mock.AsyncMock(spec=["__call__"], return_value=data) + + request = mock.AsyncMock(spec=["transport.Request"]) + request.side_effect = [retryable_error, response] + + _ = await _client._token_endpoint_request( + request, "http://example.com", {"test": "params"} + ) + + assert request.call_count == 2 def verify_request_params(request, params): @@ -128,8 +206,8 @@ def verify_request_params(request, params): assert request_params[key][0] == value -@mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) @pytest.mark.asyncio +@mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) async def test_jwt_grant(utcnow): request = make_request( {"access_token": "token", "expires_in": 500, "extra": "data"} @@ -161,8 +239,9 @@ async def test_jwt_grant_no_access_token(): } ) - with pytest.raises(exceptions.RefreshError): + with pytest.raises(exceptions.RefreshError) as excinfo: await _client.jwt_grant(request, "http://example.com", "assertion_value") + assert not excinfo.value.retryable @pytest.mark.asyncio @@ -200,14 +279,15 @@ async def test_id_token_jwt_grant_no_access_token(): } ) - with pytest.raises(exceptions.RefreshError): + with pytest.raises(exceptions.RefreshError) as excinfo: await _client.id_token_jwt_grant( request, "http://example.com", "assertion_value" ) + assert not excinfo.value.retryable -@mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) @pytest.mark.asyncio +@mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) async def test_refresh_grant(unused_utcnow): request = make_request( { @@ -246,8 +326,8 @@ async def test_refresh_grant(unused_utcnow): assert extra_data["extra"] == "data" -@mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) @pytest.mark.asyncio +@mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) async def test_refresh_grant_with_scopes(unused_utcnow): request = make_request( { @@ -298,7 +378,121 @@ async def test_refresh_grant_no_access_token(): } ) - with pytest.raises(exceptions.RefreshError): + with pytest.raises(exceptions.RefreshError) as excinfo: await _client.refresh_grant( request, "http://example.com", "refresh_token", "client_id", "client_secret" ) + assert not excinfo.value.retryable + + +@pytest.mark.asyncio +@mock.patch("google.oauth2._client._parse_expiry", return_value=None) +@mock.patch.object(_client, "_token_endpoint_request", autospec=True) +async def test_jwt_grant_retry_default(mock_token_endpoint_request, mock_expiry): + _ = await _client.jwt_grant(mock.Mock(), mock.Mock(), mock.Mock()) + mock_token_endpoint_request.assert_called_with( + mock.ANY, mock.ANY, mock.ANY, can_retry=True + ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("can_retry", [True, False]) +@mock.patch("google.oauth2._client._parse_expiry", return_value=None) +@mock.patch.object(_client, "_token_endpoint_request", autospec=True) +async def test_jwt_grant_retry_with_retry( + mock_token_endpoint_request, mock_expiry, can_retry +): + _ = await _client.jwt_grant( + mock.AsyncMock(), mock.Mock(), mock.Mock(), can_retry=can_retry + ) + mock_token_endpoint_request.assert_called_with( + mock.ANY, mock.ANY, mock.ANY, can_retry=can_retry + ) + + +@pytest.mark.asyncio +@mock.patch("google.auth.jwt.decode", return_value={"exp": 0}) +@mock.patch.object(_client, "_token_endpoint_request", autospec=True) +async def test_id_token_jwt_grant_retry_default( + mock_token_endpoint_request, mock_jwt_decode +): + _ = await _client.id_token_jwt_grant(mock.Mock(), mock.Mock(), mock.Mock()) + mock_token_endpoint_request.assert_called_with( + mock.ANY, mock.ANY, mock.ANY, can_retry=True + ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("can_retry", [True, False]) +@mock.patch("google.auth.jwt.decode", return_value={"exp": 0}) +@mock.patch.object(_client, "_token_endpoint_request", autospec=True) +async def test_id_token_jwt_grant_retry_with_retry( + mock_token_endpoint_request, mock_jwt_decode, can_retry +): + _ = await _client.id_token_jwt_grant( + mock.AsyncMock(), mock.AsyncMock(), mock.AsyncMock(), can_retry=can_retry + ) + mock_token_endpoint_request.assert_called_with( + mock.ANY, mock.ANY, mock.ANY, can_retry=can_retry + ) + + +@pytest.mark.asyncio +@mock.patch("google.oauth2._client._parse_expiry", return_value=None) +@mock.patch.object(_client, "_token_endpoint_request", autospec=True) +async def test_refresh_grant_retry_default( + mock_token_endpoint_request, mock_parse_expiry +): + _ = await _client.refresh_grant( + mock.AsyncMock(), + mock.AsyncMock(), + mock.AsyncMock(), + mock.AsyncMock(), + mock.AsyncMock(), + ) + mock_token_endpoint_request.assert_called_with( + mock.ANY, mock.ANY, mock.ANY, can_retry=True + ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("can_retry", [True, False]) +@mock.patch("google.oauth2._client._parse_expiry", return_value=None) +@mock.patch.object(_client, "_token_endpoint_request", autospec=True) +async def test_refresh_grant_retry_with_retry( + mock_token_endpoint_request, mock_parse_expiry, can_retry +): + _ = await _client.refresh_grant( + mock.AsyncMock(), + mock.AsyncMock(), + mock.AsyncMock(), + mock.AsyncMock(), + mock.AsyncMock(), + can_retry=can_retry, + ) + mock_token_endpoint_request.assert_called_with( + mock.ANY, mock.ANY, mock.ANY, can_retry=can_retry + ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("can_retry", [True, False]) +async def test__token_endpoint_request_no_throw_with_retry(can_retry): + mock_request = make_request( + {"error": "help", "error_description": "I'm alive"}, + http_client.INTERNAL_SERVER_ERROR, + ) + + _ = await _client._token_endpoint_request_no_throw( + mock_request, + mock.AsyncMock(), + "body", + mock.AsyncMock(), + mock.AsyncMock(), + can_retry=can_retry, + ) + + if can_retry: + assert mock_request.call_count == 4 + else: + assert mock_request.call_count == 1 diff --git a/tests_async/oauth2/test_reauth_async.py b/tests_async/oauth2/test_reauth_async.py index 8f51bd3a7..40ca92717 100644 --- a/tests_async/oauth2/test_reauth_async.py +++ b/tests_async/oauth2/test_reauth_async.py @@ -279,7 +279,7 @@ async def test_refresh_grant_failed(): with mock.patch( "google.oauth2._client_async._token_endpoint_request_no_throw" ) as mock_token_request: - mock_token_request.return_value = (False, {"error": "Bad request"}) + mock_token_request.return_value = (False, {"error": "Bad request"}, True) with pytest.raises(exceptions.RefreshError) as excinfo: await _reauth_async.refresh_grant( MOCK_REQUEST, @@ -291,6 +291,7 @@ async def test_refresh_grant_failed(): rapt_token="rapt_token", ) assert excinfo.match(r"Bad request") + assert excinfo.value.retryable mock_token_request.assert_called_with( MOCK_REQUEST, "token_uri", @@ -311,8 +312,8 @@ async def test_refresh_grant_success(): "google.oauth2._client_async._token_endpoint_request_no_throw" ) as mock_token_request: mock_token_request.side_effect = [ - (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}), - (True, {"access_token": "access_token"}), + (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}, True), + (True, {"access_token": "access_token"}, None), ] with mock.patch( "google.oauth2._reauth_async.get_rapt_token", return_value="new_rapt_token" @@ -339,11 +340,16 @@ async def test_refresh_grant_reauth_refresh_disabled(): "google.oauth2._client_async._token_endpoint_request_no_throw" ) as mock_token_request: mock_token_request.side_effect = [ - (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}), - (True, {"access_token": "access_token"}), + ( + False, + {"error": "invalid_grant", "error_subtype": "rapt_required"}, + False, + ), + (True, {"access_token": "access_token"}, None), ] with pytest.raises(exceptions.RefreshError) as excinfo: assert await _reauth_async.refresh_grant( MOCK_REQUEST, "token_uri", "refresh_token", "client_id", "client_secret" ) assert excinfo.match(r"Reauthentication is needed") + assert not excinfo.value.retryable From b4a1ae6f817ef8c3316e0e96428b2f2ef61f5734 Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Thu, 22 Sep 2022 23:41:27 +0000 Subject: [PATCH 33/36] Revert "feat: Retry behavior (#1113)" This reverts commit 06fff40ad9050651024e7b61cbfc5da22fa27f9b. --- google/auth/_exponential_backoff.py | 111 ------------ google/auth/exceptions.py | 17 +- google/auth/transport/__init__.py | 14 +- google/oauth2/_client.py | 171 +++++------------- google/oauth2/_client_async.py | 95 ++++------ google/oauth2/_reauth_async.py | 5 +- google/oauth2/reauth.py | 12 +- tests/compute_engine/test_credentials.py | 2 +- tests/oauth2/test__client.py | 192 ++------------------ tests/oauth2/test_reauth.py | 11 +- tests/test__exponential_backoff.py | 41 ----- tests/test_exceptions.py | 55 ------ tests_async/oauth2/test__client_async.py | 214 ++--------------------- tests_async/oauth2/test_reauth_async.py | 16 +- 14 files changed, 115 insertions(+), 841 deletions(-) delete mode 100644 google/auth/_exponential_backoff.py delete mode 100644 tests/test__exponential_backoff.py delete mode 100644 tests/test_exceptions.py diff --git a/google/auth/_exponential_backoff.py b/google/auth/_exponential_backoff.py deleted file mode 100644 index b5801bec9..000000000 --- a/google/auth/_exponential_backoff.py +++ /dev/null @@ -1,111 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import random -import time - -import six - -# The default amount of retry attempts -_DEFAULT_RETRY_TOTAL_ATTEMPTS = 3 - -# The default initial backoff period (1.0 second). -_DEFAULT_INITIAL_INTERVAL_SECONDS = 1.0 - -# The default randomization factor (0.1 which results in a random period ranging -# between 10% below and 10% above the retry interval). -_DEFAULT_RANDOMIZATION_FACTOR = 0.1 - -# The default multiplier value (2 which is 100% increase per back off). -_DEFAULT_MULTIPLIER = 2.0 - -"""Exponential Backoff Utility - -This is a private module that implements the exponential back off algorithm. -It can be used as a utility for code that needs to retry on failure, for example -an HTTP request. -""" - - -class ExponentialBackoff(six.Iterator): - """An exponential backoff iterator. This can be used in a for loop to - perform requests with exponential backoff. - - Args: - total_attempts Optional[int]: - The maximum amount of retries that should happen. - The default value is 3 attempts. - initial_wait_seconds Optional[int]: - The amount of time to sleep in the first backoff. This parameter - should be in seconds. - The default value is 1 second. - randomization_factor Optional[float]: - The amount of jitter that should be in each backoff. For example, - a value of 0.1 will introduce a jitter range of 10% to the - current backoff period. - The default value is 0.1. - multiplier Optional[float]: - The backoff multipler. This adjusts how much each backoff will - increase. For example a value of 2.0 leads to a 200% backoff - on each attempt. If the initial_wait is 1.0 it would look like - this sequence [1.0, 2.0, 4.0, 8.0]. - The default value is 2.0. - """ - - def __init__( - self, - total_attempts=_DEFAULT_RETRY_TOTAL_ATTEMPTS, - initial_wait_seconds=_DEFAULT_INITIAL_INTERVAL_SECONDS, - randomization_factor=_DEFAULT_RANDOMIZATION_FACTOR, - multiplier=_DEFAULT_MULTIPLIER, - ): - self._total_attempts = total_attempts - self._initial_wait_seconds = initial_wait_seconds - - self._current_wait_in_seconds = self._initial_wait_seconds - - self._randomization_factor = randomization_factor - self._multiplier = multiplier - self._backoff_count = 0 - - def __iter__(self): - self._backoff_count = 0 - self._current_wait_in_seconds = self._initial_wait_seconds - return self - - def __next__(self): - if self._backoff_count >= self._total_attempts: - raise StopIteration - self._backoff_count += 1 - - jitter_variance = self._current_wait_in_seconds * self._randomization_factor - jitter = random.uniform( - self._current_wait_in_seconds - jitter_variance, - self._current_wait_in_seconds + jitter_variance, - ) - - time.sleep(jitter) - - self._current_wait_in_seconds *= self._multiplier - return self._backoff_count - - @property - def total_attempts(self): - """The total amount of backoff attempts that will be made.""" - return self._total_attempts - - @property - def backoff_count(self): - """The current amount of backoff attempts that have been made.""" - return self._backoff_count diff --git a/google/auth/exceptions.py b/google/auth/exceptions.py index 7760c87b8..e9e737780 100644 --- a/google/auth/exceptions.py +++ b/google/auth/exceptions.py @@ -18,15 +18,6 @@ class GoogleAuthError(Exception): """Base class for all google.auth errors.""" - def __init__(self, *args, **kwargs): - super(GoogleAuthError, self).__init__(*args) - retryable = kwargs.get("retryable", False) - self._retryable = retryable - - @property - def retryable(self): - return self._retryable - class TransportError(GoogleAuthError): """Used to indicate an error occurred during an HTTP request.""" @@ -53,10 +44,6 @@ class MutualTLSChannelError(GoogleAuthError): class ClientCertError(GoogleAuthError): """Used to indicate that client certificate is missing or invalid.""" - @property - def retryable(self): - return False - class OAuthError(GoogleAuthError): """Used to indicate an error occurred during an OAuth related HTTP @@ -66,9 +53,9 @@ class OAuthError(GoogleAuthError): class ReauthFailError(RefreshError): """An exception for when reauth failed.""" - def __init__(self, message=None, **kwargs): + def __init__(self, message=None): super(ReauthFailError, self).__init__( - "Reauthentication failed. {0}".format(message), **kwargs + "Reauthentication failed. {0}".format(message) ) diff --git a/google/auth/transport/__init__.py b/google/auth/transport/__init__.py index 8334145a1..374e7b4d7 100644 --- a/google/auth/transport/__init__.py +++ b/google/auth/transport/__init__.py @@ -29,21 +29,9 @@ import six from six.moves import http_client -TOO_MANY_REQUESTS = 429 # Python 2.7 six is missing this status code. - -DEFAULT_RETRYABLE_STATUS_CODES = ( - http_client.INTERNAL_SERVER_ERROR, - http_client.SERVICE_UNAVAILABLE, - http_client.REQUEST_TIMEOUT, - TOO_MANY_REQUESTS, -) -"""Sequence[int]: HTTP status codes indicating a request can be retried. -""" - - DEFAULT_REFRESH_STATUS_CODES = (http_client.UNAUTHORIZED,) """Sequence[int]: Which HTTP status code indicate that credentials should be -refreshed. +refreshed and a request should be retried. """ DEFAULT_MAX_REFRESH_ATTEMPTS = 2 diff --git a/google/oauth2/_client.py b/google/oauth2/_client.py index 7f866d446..847c5db8a 100644 --- a/google/oauth2/_client.py +++ b/google/oauth2/_client.py @@ -30,11 +30,9 @@ from six.moves import http_client from six.moves import urllib -from google.auth import _exponential_backoff from google.auth import _helpers from google.auth import exceptions from google.auth import jwt -from google.auth import transport _URLENCODED_CONTENT_TYPE = "application/x-www-form-urlencoded" _JSON_CONTENT_TYPE = "application/json" @@ -42,22 +40,17 @@ _REFRESH_GRANT_TYPE = "refresh_token" -def _handle_error_response(response_data, retryable_error): +def _handle_error_response(response_data): """Translates an error response into an exception. Args: response_data (Mapping | str): The decoded response data. - retryable_error Optional[bool]: A boolean indicating if an error is retryable. - Defaults to False. Raises: google.auth.exceptions.RefreshError: The errors contained in response_data. """ - - retryable_error = retryable_error if retryable_error else False - if isinstance(response_data, six.string_types): - raise exceptions.RefreshError(response_data, retryable=retryable_error) + raise exceptions.RefreshError(response_data) try: error_details = "{}: {}".format( response_data["error"], response_data.get("error_description") @@ -66,45 +59,7 @@ def _handle_error_response(response_data, retryable_error): except (KeyError, ValueError): error_details = json.dumps(response_data) - raise exceptions.RefreshError( - error_details, response_data, retryable=retryable_error - ) - - -def _can_retry(status_code, response_data): - """Checks if a request can be retried by inspecting the status code - and response body of the request. - - Args: - status_code (int): The response status code. - response_data (Mapping | str): The decoded response data. - - Returns: - bool: True if the response is retryable. False otherwise. - """ - if status_code in transport.DEFAULT_RETRYABLE_STATUS_CODES: - return True - - try: - # For a failed response, response_body could be a string - error_desc = response_data.get("error_description") or "" - error_code = response_data.get("error") or "" - - # Per Oauth 2.0 RFC https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.2.1 - # This is needed because a redirect will not return a 500 status code. - retryable_error_descriptions = { - "internal_failure", - "server_error", - "temporarily_unavailable", - } - - if any(e in retryable_error_descriptions for e in (error_code, error_desc)): - return True - - except AttributeError: - pass - - return False + raise exceptions.RefreshError(error_details, response_data) def _parse_expiry(response_data): @@ -126,13 +81,7 @@ def _parse_expiry(response_data): def _token_endpoint_request_no_throw( - request, - token_uri, - body, - access_token=None, - use_json=False, - can_retry=True, - **kwargs + request, token_uri, body, access_token=None, use_json=False, **kwargs ): """Makes a request to the OAuth 2.0 authorization server's token endpoint. This function doesn't throw on response errors. @@ -146,7 +95,6 @@ def _token_endpoint_request_no_throw( access_token (Optional(str)): The access token needed to make the request. use_json (Optional(bool)): Use urlencoded format or json format for the content type. The default value is False. - can_retry (bool): Enable or disable request retry behavior. kwargs: Additional arguments passed on to the request method. The kwargs will be passed to `requests.request` method, see: https://docs.python-requests.org/en/latest/api/#requests.request. @@ -156,10 +104,8 @@ def _token_endpoint_request_no_throw( side SSL certificate verification. Returns: - Tuple(bool, Mapping[str, str], Optional[bool]): A boolean indicating - if the request is successful, a mapping for the JSON-decoded response - data and in the case of an error a boolean indicating if the error - is retryable. + Tuple(bool, Mapping[str, str]): A boolean indicating if the request is + successful, and a mapping for the JSON-decoded response data. """ if use_json: headers = {"Content-Type": _JSON_CONTENT_TYPE} @@ -171,7 +117,10 @@ def _token_endpoint_request_no_throw( if access_token: headers["Authorization"] = "Bearer {}".format(access_token) - def _perform_request(): + retry = 0 + # retry to fetch token for maximum of two times if any internal failure + # occurs. + while True: response = request( method="POST", url=token_uri, headers=headers, body=body, **kwargs ) @@ -180,44 +129,32 @@ def _perform_request(): if hasattr(response.data, "decode") else response.data ) - response_data = "" - try: - # response_body should be a JSON - response_data = json.loads(response_body) - except ValueError: - response_data = response_body if response.status == http_client.OK: - return True, response_data, None - - retryable_error = _can_retry( - status_code=response.status, response_data=response_data - ) - - return False, response_data, retryable_error - - request_succeeded, response_data, retryable_error = _perform_request() - - if request_succeeded or not retryable_error or not can_retry: - return request_succeeded, response_data, retryable_error - - retries = _exponential_backoff.ExponentialBackoff() - for _ in retries: - request_succeeded, response_data, retryable_error = _perform_request() - if request_succeeded or not retryable_error: - return request_succeeded, response_data, retryable_error - - return False, response_data, retryable_error + # response_body should be a JSON + response_data = json.loads(response_body) + break + else: + # For a failed response, response_body could be a string + try: + response_data = json.loads(response_body) + error_desc = response_data.get("error_description") or "" + error_code = response_data.get("error") or "" + if ( + any(e == "internal_failure" for e in (error_code, error_desc)) + and retry < 1 + ): + retry += 1 + continue + except ValueError: + response_data = response_body + return False, response_data + + return True, response_data def _token_endpoint_request( - request, - token_uri, - body, - access_token=None, - use_json=False, - can_retry=True, - **kwargs + request, token_uri, body, access_token=None, use_json=False, **kwargs ): """Makes a request to the OAuth 2.0 authorization server's token endpoint. @@ -230,7 +167,6 @@ def _token_endpoint_request( access_token (Optional(str)): The access token needed to make the request. use_json (Optional(bool)): Use urlencoded format or json format for the content type. The default value is False. - can_retry (bool): Enable or disable request retry behavior. kwargs: Additional arguments passed on to the request method. The kwargs will be passed to `requests.request` method, see: https://docs.python-requests.org/en/latest/api/#requests.request. @@ -246,22 +182,15 @@ def _token_endpoint_request( google.auth.exceptions.RefreshError: If the token endpoint returned an error. """ - - response_status_ok, response_data, retryable_error = _token_endpoint_request_no_throw( - request, - token_uri, - body, - access_token=access_token, - use_json=use_json, - can_retry=can_retry, - **kwargs + response_status_ok, response_data = _token_endpoint_request_no_throw( + request, token_uri, body, access_token=access_token, use_json=use_json, **kwargs ) if not response_status_ok: - _handle_error_response(response_data, retryable_error) + _handle_error_response(response_data) return response_data -def jwt_grant(request, token_uri, assertion, can_retry=True): +def jwt_grant(request, token_uri, assertion): """Implements the JWT Profile for OAuth 2.0 Authorization Grants. For more details, see `rfc7523 section 4`_. @@ -272,7 +201,6 @@ def jwt_grant(request, token_uri, assertion, can_retry=True): token_uri (str): The OAuth 2.0 authorizations server's token endpoint URI. assertion (str): The OAuth 2.0 assertion. - can_retry (bool): Enable or disable request retry behavior. Returns: Tuple[str, Optional[datetime], Mapping[str, str]]: The access token, @@ -286,16 +214,12 @@ def jwt_grant(request, token_uri, assertion, can_retry=True): """ body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE} - response_data = _token_endpoint_request( - request, token_uri, body, can_retry=can_retry - ) + response_data = _token_endpoint_request(request, token_uri, body) try: access_token = response_data["access_token"] except KeyError as caught_exc: - new_exc = exceptions.RefreshError( - "No access token in response.", response_data, retryable=False - ) + new_exc = exceptions.RefreshError("No access token in response.", response_data) six.raise_from(new_exc, caught_exc) expiry = _parse_expiry(response_data) @@ -303,7 +227,7 @@ def jwt_grant(request, token_uri, assertion, can_retry=True): return access_token, expiry, response_data -def id_token_jwt_grant(request, token_uri, assertion, can_retry=True): +def id_token_jwt_grant(request, token_uri, assertion): """Implements the JWT Profile for OAuth 2.0 Authorization Grants, but requests an OpenID Connect ID Token instead of an access token. @@ -318,7 +242,6 @@ def id_token_jwt_grant(request, token_uri, assertion, can_retry=True): URI. assertion (str): JWT token signed by a service account. The token's payload must include a ``target_audience`` claim. - can_retry (bool): Enable or disable request retry behavior. Returns: Tuple[str, Optional[datetime], Mapping[str, str]]: @@ -331,16 +254,12 @@ def id_token_jwt_grant(request, token_uri, assertion, can_retry=True): """ body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE} - response_data = _token_endpoint_request( - request, token_uri, body, can_retry=can_retry - ) + response_data = _token_endpoint_request(request, token_uri, body) try: id_token = response_data["id_token"] except KeyError as caught_exc: - new_exc = exceptions.RefreshError( - "No ID token in response.", response_data, retryable=False - ) + new_exc = exceptions.RefreshError("No ID token in response.", response_data) six.raise_from(new_exc, caught_exc) payload = jwt.decode(id_token, verify=False) @@ -369,9 +288,7 @@ def _handle_refresh_grant_response(response_data, refresh_token): try: access_token = response_data["access_token"] except KeyError as caught_exc: - new_exc = exceptions.RefreshError( - "No access token in response.", response_data, retryable=False - ) + new_exc = exceptions.RefreshError("No access token in response.", response_data) six.raise_from(new_exc, caught_exc) refresh_token = response_data.get("refresh_token", refresh_token) @@ -388,7 +305,6 @@ def refresh_grant( client_secret, scopes=None, rapt_token=None, - can_retry=True, ): """Implements the OAuth 2.0 refresh token grant. @@ -408,7 +324,6 @@ def refresh_grant( token has a wild card scope (e.g. 'https://www.googleapis.com/auth/any-api'). rapt_token (Optional(str)): The reauth Proof Token. - can_retry (bool): Enable or disable request retry behavior. Returns: Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access @@ -432,7 +347,5 @@ def refresh_grant( if rapt_token: body["rapt"] = rapt_token - response_data = _token_endpoint_request( - request, token_uri, body, can_retry=can_retry - ) + response_data = _token_endpoint_request(request, token_uri, body) return _handle_refresh_grant_response(response_data, refresh_token) diff --git a/google/oauth2/_client_async.py b/google/oauth2/_client_async.py index 428084a70..cf5121137 100644 --- a/google/oauth2/_client_async.py +++ b/google/oauth2/_client_async.py @@ -30,14 +30,13 @@ from six.moves import http_client from six.moves import urllib -from google.auth import _exponential_backoff from google.auth import exceptions from google.auth import jwt from google.oauth2 import _client as client async def _token_endpoint_request_no_throw( - request, token_uri, body, access_token=None, use_json=False, can_retry=True + request, token_uri, body, access_token=None, use_json=False ): """Makes a request to the OAuth 2.0 authorization server's token endpoint. This function doesn't throw on response errors. @@ -51,13 +50,10 @@ async def _token_endpoint_request_no_throw( access_token (Optional(str)): The access token needed to make the request. use_json (Optional(bool)): Use urlencoded format or json format for the content type. The default value is False. - can_retry (bool): Enable or disable request retry behavior. Returns: - Tuple(bool, Mapping[str, str], Optional[bool]): A boolean indicating - if the request is successful, a mapping for the JSON-decoded response - data and in the case of an error a boolean indicating if the error - is retryable. + Tuple(bool, Mapping[str, str]): A boolean indicating if the request is + successful, and a mapping for the JSON-decoded response data. """ if use_json: headers = {"Content-Type": client._JSON_CONTENT_TYPE} @@ -69,7 +65,11 @@ async def _token_endpoint_request_no_throw( if access_token: headers["Authorization"] = "Bearer {}".format(access_token) - async def _perform_request(): + retry = 0 + # retry to fetch token for maximum of two times if any internal failure + # occurs. + while True: + response = await request( method="POST", url=token_uri, headers=headers, body=body ) @@ -83,36 +83,26 @@ async def _perform_request(): else response_body1 ) - try: - response_data = json.loads(response_body) - except ValueError: - response_data = response_body + response_data = json.loads(response_body) if response.status == http_client.OK: - return True, response_data, None - - retryable_error = client._can_retry( - status_code=response.status, response_data=response_data - ) - - return False, response_data, retryable_error - - request_succeeded, response_data, retryable_error = await _perform_request() - - if request_succeeded or not retryable_error or not can_retry: - return request_succeeded, response_data, retryable_error - - retries = _exponential_backoff.ExponentialBackoff() - for _ in retries: - request_succeeded, response_data, retryable_error = await _perform_request() - if request_succeeded or not retryable_error: - return request_succeeded, response_data, retryable_error + break + else: + error_desc = response_data.get("error_description") or "" + error_code = response_data.get("error") or "" + if ( + any(e == "internal_failure" for e in (error_code, error_desc)) + and retry < 1 + ): + retry += 1 + continue + return response.status == http_client.OK, response_data - return False, response_data, retryable_error + return response.status == http_client.OK, response_data async def _token_endpoint_request( - request, token_uri, body, access_token=None, use_json=False, can_retry=True + request, token_uri, body, access_token=None, use_json=False ): """Makes a request to the OAuth 2.0 authorization server's token endpoint. @@ -125,7 +115,6 @@ async def _token_endpoint_request( access_token (Optional(str)): The access token needed to make the request. use_json (Optional(bool)): Use urlencoded format or json format for the content type. The default value is False. - can_retry (bool): Enable or disable request retry behavior. Returns: Mapping[str, str]: The JSON-decoded response data. @@ -134,21 +123,15 @@ async def _token_endpoint_request( google.auth.exceptions.RefreshError: If the token endpoint returned an error. """ - - response_status_ok, response_data, retryable_error = await _token_endpoint_request_no_throw( - request, - token_uri, - body, - access_token=access_token, - use_json=use_json, - can_retry=can_retry, + response_status_ok, response_data = await _token_endpoint_request_no_throw( + request, token_uri, body, access_token=access_token, use_json=use_json ) if not response_status_ok: - client._handle_error_response(response_data, retryable_error) + client._handle_error_response(response_data) return response_data -async def jwt_grant(request, token_uri, assertion, can_retry=True): +async def jwt_grant(request, token_uri, assertion): """Implements the JWT Profile for OAuth 2.0 Authorization Grants. For more details, see `rfc7523 section 4`_. @@ -159,7 +142,6 @@ async def jwt_grant(request, token_uri, assertion, can_retry=True): token_uri (str): The OAuth 2.0 authorizations server's token endpoint URI. assertion (str): The OAuth 2.0 assertion. - can_retry (bool): Enable or disable request retry behavior. Returns: Tuple[str, Optional[datetime], Mapping[str, str]]: The access token, @@ -173,16 +155,12 @@ async def jwt_grant(request, token_uri, assertion, can_retry=True): """ body = {"assertion": assertion, "grant_type": client._JWT_GRANT_TYPE} - response_data = await _token_endpoint_request( - request, token_uri, body, can_retry=can_retry - ) + response_data = await _token_endpoint_request(request, token_uri, body) try: access_token = response_data["access_token"] except KeyError as caught_exc: - new_exc = exceptions.RefreshError( - "No access token in response.", response_data, retryable=False - ) + new_exc = exceptions.RefreshError("No access token in response.", response_data) six.raise_from(new_exc, caught_exc) expiry = client._parse_expiry(response_data) @@ -190,7 +168,7 @@ async def jwt_grant(request, token_uri, assertion, can_retry=True): return access_token, expiry, response_data -async def id_token_jwt_grant(request, token_uri, assertion, can_retry=True): +async def id_token_jwt_grant(request, token_uri, assertion): """Implements the JWT Profile for OAuth 2.0 Authorization Grants, but requests an OpenID Connect ID Token instead of an access token. @@ -205,7 +183,6 @@ async def id_token_jwt_grant(request, token_uri, assertion, can_retry=True): URI. assertion (str): JWT token signed by a service account. The token's payload must include a ``target_audience`` claim. - can_retry (bool): Enable or disable request retry behavior. Returns: Tuple[str, Optional[datetime], Mapping[str, str]]: @@ -218,16 +195,12 @@ async def id_token_jwt_grant(request, token_uri, assertion, can_retry=True): """ body = {"assertion": assertion, "grant_type": client._JWT_GRANT_TYPE} - response_data = await _token_endpoint_request( - request, token_uri, body, can_retry=can_retry - ) + response_data = await _token_endpoint_request(request, token_uri, body) try: id_token = response_data["id_token"] except KeyError as caught_exc: - new_exc = exceptions.RefreshError( - "No ID token in response.", response_data, retryable=False - ) + new_exc = exceptions.RefreshError("No ID token in response.", response_data) six.raise_from(new_exc, caught_exc) payload = jwt.decode(id_token, verify=False) @@ -244,7 +217,6 @@ async def refresh_grant( client_secret, scopes=None, rapt_token=None, - can_retry=True, ): """Implements the OAuth 2.0 refresh token grant. @@ -264,7 +236,6 @@ async def refresh_grant( token has a wild card scope (e.g. 'https://www.googleapis.com/auth/any-api'). rapt_token (Optional(str)): The reauth Proof Token. - can_retry (bool): Enable or disable request retry behavior. Returns: Tuple[str, Optional[str], Optional[datetime], Mapping[str, str]]: The @@ -288,7 +259,5 @@ async def refresh_grant( if rapt_token: body["rapt"] = rapt_token - response_data = await _token_endpoint_request( - request, token_uri, body, can_retry=can_retry - ) + response_data = await _token_endpoint_request(request, token_uri, body) return client._handle_refresh_grant_response(response_data, refresh_token) diff --git a/google/oauth2/_reauth_async.py b/google/oauth2/_reauth_async.py index 6b69c6e67..30b0b0b1e 100644 --- a/google/oauth2/_reauth_async.py +++ b/google/oauth2/_reauth_async.py @@ -292,7 +292,7 @@ async def refresh_grant( if rapt_token: body["rapt"] = rapt_token - response_status_ok, response_data, retryable_error = await _client_async._token_endpoint_request_no_throw( + response_status_ok, response_data = await _client_async._token_endpoint_request_no_throw( request, token_uri, body ) if ( @@ -317,13 +317,12 @@ async def refresh_grant( ( response_status_ok, response_data, - retryable_error, ) = await _client_async._token_endpoint_request_no_throw( request, token_uri, body ) if not response_status_ok: - _client._handle_error_response(response_data, retryable_error) + _client._handle_error_response(response_data) refresh_response = _client._handle_refresh_grant_response( response_data, refresh_token ) diff --git a/google/oauth2/reauth.py b/google/oauth2/reauth.py index ad2ad1b2e..2c32bda2a 100644 --- a/google/oauth2/reauth.py +++ b/google/oauth2/reauth.py @@ -319,7 +319,7 @@ def refresh_grant( if rapt_token: body["rapt"] = rapt_token - response_status_ok, response_data, retryable_error = _client._token_endpoint_request_no_throw( + response_status_ok, response_data = _client._token_endpoint_request_no_throw( request, token_uri, body ) if ( @@ -339,14 +339,12 @@ def refresh_grant( request, client_id, client_secret, refresh_token, token_uri, scopes=scopes ) body["rapt"] = rapt_token - ( - response_status_ok, - response_data, - retryable_error, - ) = _client._token_endpoint_request_no_throw(request, token_uri, body) + (response_status_ok, response_data) = _client._token_endpoint_request_no_throw( + request, token_uri, body + ) if not response_status_ok: - _client._handle_error_response(response_data, retryable_error) + _client._handle_error_response(response_data) return _client._handle_refresh_grant_response(response_data, refresh_token) + ( rapt_token, ) diff --git a/tests/compute_engine/test_credentials.py b/tests/compute_engine/test_credentials.py index ebce176e8..ff01720c4 100644 --- a/tests/compute_engine/test_credentials.py +++ b/tests/compute_engine/test_credentials.py @@ -609,7 +609,7 @@ def test_refresh_error(self, sign, get, utcnow): request = mock.create_autospec(transport.Request, instance=True) response = mock.Mock() response.data = b'{"error": "http error"}' - response.status = 404 # Throw a 404 so the request is not retried. + response.status = 500 request.side_effect = [response] self.credentials = credentials.IDTokenCredentials( diff --git a/tests/oauth2/test__client.py b/tests/oauth2/test__client.py index 13c42dc52..bd4cc5001 100644 --- a/tests/oauth2/test__client.py +++ b/tests/oauth2/test__client.py @@ -47,14 +47,12 @@ ) -@pytest.mark.parametrize("retryable", [True, False]) -def test__handle_error_response(retryable): +def test__handle_error_response(): response_data = {"error": "help", "error_description": "I'm alive"} with pytest.raises(exceptions.RefreshError) as excinfo: - _client._handle_error_response(response_data, retryable) + _client._handle_error_response(response_data) - assert excinfo.value.retryable == retryable assert excinfo.match(r"help: I\'m alive") @@ -62,9 +60,8 @@ def test__handle_error_response_no_error(): response_data = {"foo": "bar"} with pytest.raises(exceptions.RefreshError) as excinfo: - _client._handle_error_response(response_data, False) + _client._handle_error_response(response_data) - assert not excinfo.value.retryable assert excinfo.match(r"{\"foo\": \"bar\"}") @@ -72,33 +69,11 @@ def test__handle_error_response_not_json(): response_data = "this is an error message" with pytest.raises(exceptions.RefreshError) as excinfo: - _client._handle_error_response(response_data, False) + _client._handle_error_response(response_data) - assert not excinfo.value.retryable assert excinfo.match(response_data) -def test__can_retry_retryable(): - retryable_codes = transport.DEFAULT_RETRYABLE_STATUS_CODES - for status_code in range(100, 600): - if status_code in retryable_codes: - assert _client._can_retry(status_code, {"error": "invalid_scope"}) - else: - assert not _client._can_retry(status_code, {"error": "invalid_scope"}) - - -@pytest.mark.parametrize( - "response_data", [{"error": "internal_failure"}, {"error": "server_error"}] -) -def test__can_retry_message(response_data): - assert _client._can_retry(http_client.OK, response_data) - - -@pytest.mark.parametrize("response_data", [{"error": "invalid_scope"}]) -def test__can_retry_no_retry_message(response_data): - assert not _client._can_retry(http_client.OK, response_data) - - @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) def test__parse_expiry(unused_utcnow): result = _client._parse_expiry({"expires_in": 500}) @@ -179,8 +154,8 @@ def test__token_endpoint_request_internal_failure_error(): _client._token_endpoint_request( request, "http://example.com", {"error_description": "internal_failure"} ) - # request should be called once and then with 3 retries - assert request.call_count == 4 + # request should be called twice due to the retry + assert request.call_count == 2 request = make_request( {"error": "internal_failure"}, status=http_client.BAD_REQUEST @@ -190,55 +165,7 @@ def test__token_endpoint_request_internal_failure_error(): _client._token_endpoint_request( request, "http://example.com", {"error": "internal_failure"} ) - # request should be called once and then with 3 retries - assert request.call_count == 4 - - -def test__token_endpoint_request_internal_failure_and_retry_failure_error(): - retryable_error = mock.create_autospec(transport.Response, instance=True) - retryable_error.status = http_client.BAD_REQUEST - retryable_error.data = json.dumps({"error_description": "internal_failure"}).encode( - "utf-8" - ) - - unretryable_error = mock.create_autospec(transport.Response, instance=True) - unretryable_error.status = http_client.BAD_REQUEST - unretryable_error.data = json.dumps({"error_description": "invalid_scope"}).encode( - "utf-8" - ) - - request = mock.create_autospec(transport.Request) - - request.side_effect = [retryable_error, retryable_error, unretryable_error] - - with pytest.raises(exceptions.RefreshError): - _client._token_endpoint_request( - request, "http://example.com", {"error_description": "invalid_scope"} - ) - # request should be called three times. Two retryable errors and one - # unretryable error to break the retry loop. - assert request.call_count == 3 - - -def test__token_endpoint_request_internal_failure_and_retry_succeeds(): - retryable_error = mock.create_autospec(transport.Response, instance=True) - retryable_error.status = http_client.BAD_REQUEST - retryable_error.data = json.dumps({"error_description": "internal_failure"}).encode( - "utf-8" - ) - - response = mock.create_autospec(transport.Response, instance=True) - response.status = http_client.OK - response.data = json.dumps({"hello": "world"}).encode("utf-8") - - request = mock.create_autospec(transport.Request) - - request.side_effect = [retryable_error, response] - - _ = _client._token_endpoint_request( - request, "http://example.com", {"test": "params"} - ) - + # request should be called twice due to the retry assert request.call_count == 2 @@ -292,9 +219,8 @@ def test_jwt_grant_no_access_token(): } ) - with pytest.raises(exceptions.RefreshError) as excinfo: + with pytest.raises(exceptions.RefreshError): _client.jwt_grant(request, "http://example.com", "assertion_value") - assert not excinfo.value.retryable def test_id_token_jwt_grant(): @@ -329,9 +255,8 @@ def test_id_token_jwt_grant_no_access_token(): } ) - with pytest.raises(exceptions.RefreshError) as excinfo: + with pytest.raises(exceptions.RefreshError): _client.id_token_jwt_grant(request, "http://example.com", "assertion_value") - assert not excinfo.value.retryable @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) @@ -423,104 +348,7 @@ def test_refresh_grant_no_access_token(): } ) - with pytest.raises(exceptions.RefreshError) as excinfo: + with pytest.raises(exceptions.RefreshError): _client.refresh_grant( request, "http://example.com", "refresh_token", "client_id", "client_secret" ) - assert not excinfo.value.retryable - - -@mock.patch("google.oauth2._client._parse_expiry", return_value=None) -@mock.patch.object(_client, "_token_endpoint_request", autospec=True) -def test_jwt_grant_retry_default(mock_token_endpoint_request, mock_expiry): - _client.jwt_grant(mock.Mock(), mock.Mock(), mock.Mock()) - mock_token_endpoint_request.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, can_retry=True - ) - - -@pytest.mark.parametrize("can_retry", [True, False]) -@mock.patch("google.oauth2._client._parse_expiry", return_value=None) -@mock.patch.object(_client, "_token_endpoint_request", autospec=True) -def test_jwt_grant_retry_with_retry( - mock_token_endpoint_request, mock_expiry, can_retry -): - _client.jwt_grant(mock.Mock(), mock.Mock(), mock.Mock(), can_retry=can_retry) - mock_token_endpoint_request.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, can_retry=can_retry - ) - - -@mock.patch("google.auth.jwt.decode", return_value={"exp": 0}) -@mock.patch.object(_client, "_token_endpoint_request", autospec=True) -def test_id_token_jwt_grant_retry_default(mock_token_endpoint_request, mock_jwt_decode): - _client.id_token_jwt_grant(mock.Mock(), mock.Mock(), mock.Mock()) - mock_token_endpoint_request.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, can_retry=True - ) - - -@pytest.mark.parametrize("can_retry", [True, False]) -@mock.patch("google.auth.jwt.decode", return_value={"exp": 0}) -@mock.patch.object(_client, "_token_endpoint_request", autospec=True) -def test_id_token_jwt_grant_retry_with_retry( - mock_token_endpoint_request, mock_jwt_decode, can_retry -): - _client.id_token_jwt_grant( - mock.Mock(), mock.Mock(), mock.Mock(), can_retry=can_retry - ) - mock_token_endpoint_request.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, can_retry=can_retry - ) - - -@mock.patch("google.oauth2._client._parse_expiry", return_value=None) -@mock.patch.object(_client, "_token_endpoint_request", autospec=True) -def test_refresh_grant_retry_default(mock_token_endpoint_request, mock_parse_expiry): - _client.refresh_grant( - mock.Mock(), mock.Mock(), mock.Mock(), mock.Mock(), mock.Mock() - ) - mock_token_endpoint_request.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, can_retry=True - ) - - -@pytest.mark.parametrize("can_retry", [True, False]) -@mock.patch("google.oauth2._client._parse_expiry", return_value=None) -@mock.patch.object(_client, "_token_endpoint_request", autospec=True) -def test_refresh_grant_retry_with_retry( - mock_token_endpoint_request, mock_parse_expiry, can_retry -): - _client.refresh_grant( - mock.Mock(), - mock.Mock(), - mock.Mock(), - mock.Mock(), - mock.Mock(), - can_retry=can_retry, - ) - mock_token_endpoint_request.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, can_retry=can_retry - ) - - -@pytest.mark.parametrize("can_retry", [True, False]) -def test__token_endpoint_request_no_throw_with_retry(can_retry): - response_data = {"error": "help", "error_description": "I'm alive"} - body = "dummy body" - - mock_response = mock.create_autospec(transport.Response, instance=True) - mock_response.status = http_client.INTERNAL_SERVER_ERROR - mock_response.data = json.dumps(response_data).encode("utf-8") - - mock_request = mock.create_autospec(transport.Request) - mock_request.return_value = mock_response - - _client._token_endpoint_request_no_throw( - mock_request, mock.Mock(), body, mock.Mock(), mock.Mock(), can_retry=can_retry - ) - - if can_retry: - assert mock_request.call_count == 4 - else: - assert mock_request.call_count == 1 diff --git a/tests/oauth2/test_reauth.py b/tests/oauth2/test_reauth.py index df0636b18..ae64be009 100644 --- a/tests/oauth2/test_reauth.py +++ b/tests/oauth2/test_reauth.py @@ -260,7 +260,7 @@ def test_refresh_grant_failed(): with mock.patch( "google.oauth2._client._token_endpoint_request_no_throw" ) as mock_token_request: - mock_token_request.return_value = (False, {"error": "Bad request"}, False) + mock_token_request.return_value = (False, {"error": "Bad request"}) with pytest.raises(exceptions.RefreshError) as excinfo: reauth.refresh_grant( MOCK_REQUEST, @@ -273,7 +273,6 @@ def test_refresh_grant_failed(): enable_reauth_refresh=True, ) assert excinfo.match(r"Bad request") - assert not excinfo.value.retryable mock_token_request.assert_called_with( MOCK_REQUEST, "token_uri", @@ -293,8 +292,8 @@ def test_refresh_grant_success(): "google.oauth2._client._token_endpoint_request_no_throw" ) as mock_token_request: mock_token_request.side_effect = [ - (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}, True), - (True, {"access_token": "access_token"}, None), + (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}), + (True, {"access_token": "access_token"}), ] with mock.patch( "google.oauth2.reauth.get_rapt_token", return_value="new_rapt_token" @@ -320,8 +319,8 @@ def test_refresh_grant_reauth_refresh_disabled(): "google.oauth2._client._token_endpoint_request_no_throw" ) as mock_token_request: mock_token_request.side_effect = [ - (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}, True), - (True, {"access_token": "access_token"}, None), + (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}), + (True, {"access_token": "access_token"}), ] with pytest.raises(exceptions.RefreshError) as excinfo: reauth.refresh_grant( diff --git a/tests/test__exponential_backoff.py b/tests/test__exponential_backoff.py deleted file mode 100644 index 06a54527e..000000000 --- a/tests/test__exponential_backoff.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2022 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import mock - -from google.auth import _exponential_backoff - - -@mock.patch("time.sleep", return_value=None) -def test_exponential_backoff(mock_time): - eb = _exponential_backoff.ExponentialBackoff() - curr_wait = eb._current_wait_in_seconds - iteration_count = 0 - - for attempt in eb: - backoff_interval = mock_time.call_args[0][0] - jitter = curr_wait * eb._randomization_factor - - assert (curr_wait - jitter) <= backoff_interval <= (curr_wait + jitter) - assert attempt == iteration_count + 1 - assert eb.backoff_count == iteration_count + 1 - assert eb._current_wait_in_seconds == eb._multiplier ** (iteration_count + 1) - - curr_wait = eb._current_wait_in_seconds - iteration_count += 1 - - assert eb.total_attempts == _exponential_backoff._DEFAULT_RETRY_TOTAL_ATTEMPTS - assert eb.backoff_count == _exponential_backoff._DEFAULT_RETRY_TOTAL_ATTEMPTS - assert iteration_count == _exponential_backoff._DEFAULT_RETRY_TOTAL_ATTEMPTS - assert mock_time.call_count == _exponential_backoff._DEFAULT_RETRY_TOTAL_ATTEMPTS diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py deleted file mode 100644 index 6f542498f..000000000 --- a/tests/test_exceptions.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright 2022 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pytest # type: ignore - -from google.auth import exceptions # type:ignore - - -@pytest.fixture( - params=[ - exceptions.GoogleAuthError, - exceptions.TransportError, - exceptions.RefreshError, - exceptions.UserAccessTokenError, - exceptions.DefaultCredentialsError, - exceptions.MutualTLSChannelError, - exceptions.OAuthError, - exceptions.ReauthFailError, - exceptions.ReauthSamlChallengeFailError, - ] -) -def retryable_exception(request): - return request.param - - -@pytest.fixture(params=[exceptions.ClientCertError]) -def non_retryable_exception(request): - return request.param - - -def test_default_retryable_exceptions(retryable_exception): - assert not retryable_exception().retryable - - -@pytest.mark.parametrize("retryable", [True, False]) -def test_retryable_exceptions(retryable_exception, retryable): - retryable_exception = retryable_exception(retryable=retryable) - assert retryable_exception.retryable == retryable - - -@pytest.mark.parametrize("retryable", [True, False]) -def test_non_retryable_exceptions(non_retryable_exception, retryable): - non_retryable_exception = non_retryable_exception(retryable=retryable) - assert not non_retryable_exception.retryable diff --git a/tests_async/oauth2/test__client_async.py b/tests_async/oauth2/test__client_async.py index 402083672..91874cdd4 100644 --- a/tests_async/oauth2/test__client_async.py +++ b/tests_async/oauth2/test__client_async.py @@ -29,10 +29,10 @@ from tests.oauth2 import test__client as test_client -def make_request(response_data, status=http_client.OK, text=False): +def make_request(response_data, status=http_client.OK): response = mock.AsyncMock(spec=["transport.Response"]) response.status = status - data = response_data if text else json.dumps(response_data).encode("utf-8") + data = json.dumps(response_data).encode("utf-8") response.data = mock.AsyncMock(spec=["__call__", "read"]) response.data.read = mock.AsyncMock(spec=["__call__"], return_value=data) response.content = mock.AsyncMock(spec=["__call__"], return_value=data) @@ -62,27 +62,6 @@ async def test__token_endpoint_request(): assert result == {"test": "response"} -@pytest.mark.asyncio -async def test__token_endpoint_request_text(): - - request = make_request("response", text=True) - - result = await _client._token_endpoint_request( - request, "http://example.com", {"test": "params"} - ) - - # Check request call - request.assert_called_with( - method="POST", - url="http://example.com", - headers={"Content-Type": "application/x-www-form-urlencoded"}, - body="test=params".encode("utf-8"), - ) - - # Check result - assert result == "response" - - @pytest.mark.asyncio async def test__token_endpoint_request_json(): @@ -116,9 +95,8 @@ async def test__token_endpoint_request_json(): async def test__token_endpoint_request_error(): request = make_request({}, status=http_client.BAD_REQUEST) - with pytest.raises(exceptions.RefreshError) as excinfo: + with pytest.raises(exceptions.RefreshError): await _client._token_endpoint_request(request, "http://example.com", {}) - assert not excinfo.value.retryable @pytest.mark.asyncio @@ -127,11 +105,10 @@ async def test__token_endpoint_request_internal_failure_error(): {"error_description": "internal_failure"}, status=http_client.BAD_REQUEST ) - with pytest.raises(exceptions.RefreshError) as excinfo: + with pytest.raises(exceptions.RefreshError): await _client._token_endpoint_request( request, "http://example.com", {"error_description": "internal_failure"} ) - assert excinfo.value.retryable request = make_request( {"error": "internal_failure"}, status=http_client.BAD_REQUEST @@ -141,61 +118,6 @@ async def test__token_endpoint_request_internal_failure_error(): await _client._token_endpoint_request( request, "http://example.com", {"error": "internal_failure"} ) - assert excinfo.value.retryable - - -@pytest.mark.asyncio -async def test__token_endpoint_request_internal_failure_and_retry_failure_error(): - retryable_error = mock.AsyncMock(spec=["transport.Response"]) - retryable_error.status = http_client.BAD_REQUEST - data = json.dumps({"error_description": "internal_failure"}).encode("utf-8") - retryable_error.data = mock.AsyncMock(spec=["__call__", "read"]) - retryable_error.data.read = mock.AsyncMock(spec=["__call__"], return_value=data) - retryable_error.content = mock.AsyncMock(spec=["__call__"], return_value=data) - - unretryable_error = mock.AsyncMock(spec=["transport.Response"]) - unretryable_error.status = http_client.BAD_REQUEST - data = json.dumps({"error_description": "invalid_scope"}).encode("utf-8") - unretryable_error.data = mock.AsyncMock(spec=["__call__", "read"]) - unretryable_error.data.read = mock.AsyncMock(spec=["__call__"], return_value=data) - unretryable_error.content = mock.AsyncMock(spec=["__call__"], return_value=data) - - request = mock.AsyncMock(spec=["transport.Request"]) - request.side_effect = [retryable_error, retryable_error, unretryable_error] - - with pytest.raises(exceptions.RefreshError): - await _client._token_endpoint_request( - request, "http://example.com", {"error_description": "invalid_scope"} - ) - # request should be called three times. Two retryable errors and one - # unretryable error to break the retry loop. - assert request.call_count == 3 - - -@pytest.mark.asyncio -async def test__token_endpoint_request_internal_failure_and_retry_succeeds(): - retryable_error = mock.AsyncMock(spec=["transport.Response"]) - retryable_error.status = http_client.BAD_REQUEST - data = json.dumps({"error_description": "internal_failure"}).encode("utf-8") - retryable_error.data = mock.AsyncMock(spec=["__call__", "read"]) - retryable_error.data.read = mock.AsyncMock(spec=["__call__"], return_value=data) - retryable_error.content = mock.AsyncMock(spec=["__call__"], return_value=data) - - response = mock.AsyncMock(spec=["transport.Response"]) - response.status = http_client.OK - data = json.dumps({"hello": "world"}).encode("utf-8") - response.data = mock.AsyncMock(spec=["__call__", "read"]) - response.data.read = mock.AsyncMock(spec=["__call__"], return_value=data) - response.content = mock.AsyncMock(spec=["__call__"], return_value=data) - - request = mock.AsyncMock(spec=["transport.Request"]) - request.side_effect = [retryable_error, response] - - _ = await _client._token_endpoint_request( - request, "http://example.com", {"test": "params"} - ) - - assert request.call_count == 2 def verify_request_params(request, params): @@ -206,8 +128,8 @@ def verify_request_params(request, params): assert request_params[key][0] == value -@pytest.mark.asyncio @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) +@pytest.mark.asyncio async def test_jwt_grant(utcnow): request = make_request( {"access_token": "token", "expires_in": 500, "extra": "data"} @@ -239,9 +161,8 @@ async def test_jwt_grant_no_access_token(): } ) - with pytest.raises(exceptions.RefreshError) as excinfo: + with pytest.raises(exceptions.RefreshError): await _client.jwt_grant(request, "http://example.com", "assertion_value") - assert not excinfo.value.retryable @pytest.mark.asyncio @@ -279,15 +200,14 @@ async def test_id_token_jwt_grant_no_access_token(): } ) - with pytest.raises(exceptions.RefreshError) as excinfo: + with pytest.raises(exceptions.RefreshError): await _client.id_token_jwt_grant( request, "http://example.com", "assertion_value" ) - assert not excinfo.value.retryable -@pytest.mark.asyncio @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) +@pytest.mark.asyncio async def test_refresh_grant(unused_utcnow): request = make_request( { @@ -326,8 +246,8 @@ async def test_refresh_grant(unused_utcnow): assert extra_data["extra"] == "data" -@pytest.mark.asyncio @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) +@pytest.mark.asyncio async def test_refresh_grant_with_scopes(unused_utcnow): request = make_request( { @@ -378,121 +298,7 @@ async def test_refresh_grant_no_access_token(): } ) - with pytest.raises(exceptions.RefreshError) as excinfo: + with pytest.raises(exceptions.RefreshError): await _client.refresh_grant( request, "http://example.com", "refresh_token", "client_id", "client_secret" ) - assert not excinfo.value.retryable - - -@pytest.mark.asyncio -@mock.patch("google.oauth2._client._parse_expiry", return_value=None) -@mock.patch.object(_client, "_token_endpoint_request", autospec=True) -async def test_jwt_grant_retry_default(mock_token_endpoint_request, mock_expiry): - _ = await _client.jwt_grant(mock.Mock(), mock.Mock(), mock.Mock()) - mock_token_endpoint_request.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, can_retry=True - ) - - -@pytest.mark.asyncio -@pytest.mark.parametrize("can_retry", [True, False]) -@mock.patch("google.oauth2._client._parse_expiry", return_value=None) -@mock.patch.object(_client, "_token_endpoint_request", autospec=True) -async def test_jwt_grant_retry_with_retry( - mock_token_endpoint_request, mock_expiry, can_retry -): - _ = await _client.jwt_grant( - mock.AsyncMock(), mock.Mock(), mock.Mock(), can_retry=can_retry - ) - mock_token_endpoint_request.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, can_retry=can_retry - ) - - -@pytest.mark.asyncio -@mock.patch("google.auth.jwt.decode", return_value={"exp": 0}) -@mock.patch.object(_client, "_token_endpoint_request", autospec=True) -async def test_id_token_jwt_grant_retry_default( - mock_token_endpoint_request, mock_jwt_decode -): - _ = await _client.id_token_jwt_grant(mock.Mock(), mock.Mock(), mock.Mock()) - mock_token_endpoint_request.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, can_retry=True - ) - - -@pytest.mark.asyncio -@pytest.mark.parametrize("can_retry", [True, False]) -@mock.patch("google.auth.jwt.decode", return_value={"exp": 0}) -@mock.patch.object(_client, "_token_endpoint_request", autospec=True) -async def test_id_token_jwt_grant_retry_with_retry( - mock_token_endpoint_request, mock_jwt_decode, can_retry -): - _ = await _client.id_token_jwt_grant( - mock.AsyncMock(), mock.AsyncMock(), mock.AsyncMock(), can_retry=can_retry - ) - mock_token_endpoint_request.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, can_retry=can_retry - ) - - -@pytest.mark.asyncio -@mock.patch("google.oauth2._client._parse_expiry", return_value=None) -@mock.patch.object(_client, "_token_endpoint_request", autospec=True) -async def test_refresh_grant_retry_default( - mock_token_endpoint_request, mock_parse_expiry -): - _ = await _client.refresh_grant( - mock.AsyncMock(), - mock.AsyncMock(), - mock.AsyncMock(), - mock.AsyncMock(), - mock.AsyncMock(), - ) - mock_token_endpoint_request.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, can_retry=True - ) - - -@pytest.mark.asyncio -@pytest.mark.parametrize("can_retry", [True, False]) -@mock.patch("google.oauth2._client._parse_expiry", return_value=None) -@mock.patch.object(_client, "_token_endpoint_request", autospec=True) -async def test_refresh_grant_retry_with_retry( - mock_token_endpoint_request, mock_parse_expiry, can_retry -): - _ = await _client.refresh_grant( - mock.AsyncMock(), - mock.AsyncMock(), - mock.AsyncMock(), - mock.AsyncMock(), - mock.AsyncMock(), - can_retry=can_retry, - ) - mock_token_endpoint_request.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, can_retry=can_retry - ) - - -@pytest.mark.asyncio -@pytest.mark.parametrize("can_retry", [True, False]) -async def test__token_endpoint_request_no_throw_with_retry(can_retry): - mock_request = make_request( - {"error": "help", "error_description": "I'm alive"}, - http_client.INTERNAL_SERVER_ERROR, - ) - - _ = await _client._token_endpoint_request_no_throw( - mock_request, - mock.AsyncMock(), - "body", - mock.AsyncMock(), - mock.AsyncMock(), - can_retry=can_retry, - ) - - if can_retry: - assert mock_request.call_count == 4 - else: - assert mock_request.call_count == 1 diff --git a/tests_async/oauth2/test_reauth_async.py b/tests_async/oauth2/test_reauth_async.py index 40ca92717..8f51bd3a7 100644 --- a/tests_async/oauth2/test_reauth_async.py +++ b/tests_async/oauth2/test_reauth_async.py @@ -279,7 +279,7 @@ async def test_refresh_grant_failed(): with mock.patch( "google.oauth2._client_async._token_endpoint_request_no_throw" ) as mock_token_request: - mock_token_request.return_value = (False, {"error": "Bad request"}, True) + mock_token_request.return_value = (False, {"error": "Bad request"}) with pytest.raises(exceptions.RefreshError) as excinfo: await _reauth_async.refresh_grant( MOCK_REQUEST, @@ -291,7 +291,6 @@ async def test_refresh_grant_failed(): rapt_token="rapt_token", ) assert excinfo.match(r"Bad request") - assert excinfo.value.retryable mock_token_request.assert_called_with( MOCK_REQUEST, "token_uri", @@ -312,8 +311,8 @@ async def test_refresh_grant_success(): "google.oauth2._client_async._token_endpoint_request_no_throw" ) as mock_token_request: mock_token_request.side_effect = [ - (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}, True), - (True, {"access_token": "access_token"}, None), + (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}), + (True, {"access_token": "access_token"}), ] with mock.patch( "google.oauth2._reauth_async.get_rapt_token", return_value="new_rapt_token" @@ -340,16 +339,11 @@ async def test_refresh_grant_reauth_refresh_disabled(): "google.oauth2._client_async._token_endpoint_request_no_throw" ) as mock_token_request: mock_token_request.side_effect = [ - ( - False, - {"error": "invalid_grant", "error_subtype": "rapt_required"}, - False, - ), - (True, {"access_token": "access_token"}, None), + (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}), + (True, {"access_token": "access_token"}), ] with pytest.raises(exceptions.RefreshError) as excinfo: assert await _reauth_async.refresh_grant( MOCK_REQUEST, "token_uri", "refresh_token", "client_id", "client_secret" ) assert excinfo.match(r"Reauthentication is needed") - assert not excinfo.value.retryable From 2db7eaf9fc80d36b7f2e193c3863df8c1ef9a40f Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Fri, 23 Sep 2022 17:10:04 +0000 Subject: [PATCH 34/36] chore: update token --- system_tests/secrets.tar.enc | Bin 10324 -> 10324 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc index 6dc14d78e8d934436d54ec752357b030ac692e0d..ae54fc9c36b4ea27d099b07b4798d7cd0f7ffa45 100644 GIT binary patch literal 10324 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

bG)CO=t#_m5-@Z^6sOIvOJZa zH?j}z{nRo>tzJMLlsiciem&V8#SZbVX=;yr;R@s0tuuAQITH`)Q0&OA)!EJ^Vr#IU zRZJrvSoK;#P9c9s~280w`7g7r~VMGJQLF5o#9Z2Jv?xPrhg=8Z4ykVV)#X&M~O?FEnqW0K3hj zz<1M{ceN^DC0vC_>cSI*C_Wex_hN@#p-@K>Z}Z=-{2SSQD~QXp0X_Mv!escPPBBs@ zqJ=`Yz4Tw^@5 z)NNdPx&!kuLlYn9DabMB-To7x5vc4%ta0 z4&VO$KczxdK{-9(5}+V|TY4i_CG1BP_n$ziy56nYO&4VHy|{@KG%S7+Li?^WK58dW z@hR;|9wD^<1Prx8fu?i@9@7{=2DBCfC8Ptw|K(Ev`-uQ^0e_=O31vn8*weZ>Bb3`q zS_Xq3-x?NJ|34SSH)3=adLo8=u-8-jUmAoNh8X>)5|J|z5h&IpDd3lvwT#zMJYM3UR0Hp z#>{KrF?l2LB+>gUR6Jhv#ieYTr&qc%rn-6~@`3Ey-SQ=m724OAgn4U>2Spx~$T8E@ zS390{0B91n&Vrgdkr?j6VmJ>SSNx9O&&ns7)xdjyRg^ybQI3vw_TtqrRYyq8cfU}c zmvt?(Nw;VCG(}7hiG}x2CYILLJn8S0%l|C{pBHMJk_}`aTao;w@fFG7w6sF%pl*8> z@06S?8e(o8)pQPg9reD4HtkuoQGD#?n+|^6K!-x&OEp7!k`tHt`s?sMXw+x+SdUZ8 zJRFsw&D)TU3sOrCc!e&0H zyBZoJruk=ce-$jI=MG0~2HJw(p^^4W-_Ec+|b;SYHd1RrN z#CiyZ?9@0k&;>U}=K4>AWIR>wvtEQe^tR})`d(Z@ZDf`w${1dihC#C81YZ^3_jB;d m-~w>yV0}WrcTx1h@{&x@)E$$T@^!eF%PsG5xg8sr3I?`;rtM<@ literal 10324 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 From a093a266fc4d856a50d69bee98a50357a0997995 Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Mon, 26 Sep 2022 20:38:07 +0000 Subject: [PATCH 35/36] chore: update token --- system_tests/secrets.tar.enc | Bin 10324 -> 10324 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc index ae54fc9c36b4ea27d099b07b4798d7cd0f7ffa45..80517e0a91ac28e439521502ede5f23e5a292342 100644 GIT binary patch literal 10324 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 literal 10324 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

bG)CO=t#_m5-@Z^6sOIvOJZa zH?j}z{nRo>tzJMLlsiciem&V8#SZbVX=;yr;R@s0tuuAQITH`)Q0&OA)!EJ^Vr#IU zRZJrvSoK;#P9c9s~280w`7g7r~VMGJQLF5o#9Z2Jv?xPrhg=8Z4ykVV)#X&M~O?FEnqW0K3hj zz<1M{ceN^DC0vC_>cSI*C_Wex_hN@#p-@K>Z}Z=-{2SSQD~QXp0X_Mv!escPPBBs@ zqJ=`Yz4Tw^@5 z)NNdPx&!kuLlYn9DabMB-To7x5vc4%ta0 z4&VO$KczxdK{-9(5}+V|TY4i_CG1BP_n$ziy56nYO&4VHy|{@KG%S7+Li?^WK58dW z@hR;|9wD^<1Prx8fu?i@9@7{=2DBCfC8Ptw|K(Ev`-uQ^0e_=O31vn8*weZ>Bb3`q zS_Xq3-x?NJ|34SSH)3=adLo8=u-8-jUmAoNh8X>)5|J|z5h&IpDd3lvwT#zMJYM3UR0Hp z#>{KrF?l2LB+>gUR6Jhv#ieYTr&qc%rn-6~@`3Ey-SQ=m724OAgn4U>2Spx~$T8E@ zS390{0B91n&Vrgdkr?j6VmJ>SSNx9O&&ns7)xdjyRg^ybQI3vw_TtqrRYyq8cfU}c zmvt?(Nw;VCG(}7hiG}x2CYILLJn8S0%l|C{pBHMJk_}`aTao;w@fFG7w6sF%pl*8> z@06S?8e(o8)pQPg9reD4HtkuoQGD#?n+|^6K!-x&OEp7!k`tHt`s?sMXw+x+SdUZ8 zJRFsw&D)TU3sOrCc!e&0H zyBZoJruk=ce-$jI=MG0~2HJw(p^^4W-_Ec+|b;SYHd1RrN z#CiyZ?9@0k&;>U}=K4>AWIR>wvtEQe^tR})`d(Z@ZDf`w${1dihC#C81YZ^3_jB;d m-~w>yV0}WrcTx1h@{&x@)E$$T@^!eF%PsG5xg8sr3I?`;rtM<@ From 8af219f1e0921c130aaacd412ca502f10c858833 Mon Sep 17 00:00:00 2001 From: Jin Qin Date: Tue, 27 Sep 2022 22:19:30 +0000 Subject: [PATCH 36/36] chore: update token --- system_tests/secrets.tar.enc | Bin 10324 -> 10324 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc index 80517e0a91ac28e439521502ede5f23e5a292342..2bef7d971ed407a5c67431d403624e65b2c28877 100644 GIT binary patch literal 10324 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