def handle_operation(self, operation, wait_for_operation=False, max_time_seconds=None): """Handles long running operations. Args: operation: The operation to handle. wait_for_operation: Should we allow polling for the operation to complete. If no polling is requested, a locked model will be returned instead. max_time_seconds: The maximum seconds to try polling for operation complete. (None for no limit) Returns: dict: A dictionary of the returned model properties. Raises: TypeError: if the operation is not a dictionary. ValueError: If the operation is malformed. UnknownError: If the server responds with an unexpected response. err: If the operation exceeds polling attempts or stop_time """ if not isinstance(operation, dict): raise TypeError('Operation must be a dictionary.') if operation.get('done'): # Operations which are immediately done don't have an operation name if operation.get('response'): return operation.get('response') if operation.get('error'): raise _utils.handle_operation_error(operation.get('error')) raise exceptions.UnknownError( message='Internal Error: Malformed Operation.') op_name = _validate_operation_name(operation.get('name')) metadata = operation.get('metadata', {}) metadata_type = metadata.get('@type', '') if not metadata_type.endswith('ModelOperationMetadata'): raise TypeError('Unknown type of operation metadata.') _, model_id = _validate_and_parse_name(metadata.get('name')) current_attempt = 0 start_time = datetime.datetime.now() stop_time = (None if max_time_seconds is None else start_time + datetime.timedelta(seconds=max_time_seconds)) while wait_for_operation and not operation.get('done'): # We just got this operation. Wait before getting another # so we don't exceed the GetOperation maximum request rate. self._exponential_backoff(current_attempt, stop_time) operation = self.get_operation(op_name) current_attempt += 1 if operation.get('done'): if operation.get('response'): return operation.get('response') if operation.get('error'): raise _utils.handle_operation_error(operation.get('error')) # If the operation is not complete or timed out, return a (locked) model instead return get_model(model_id).as_dict()
def test_is_associated_auth_id_deleted_without_init_returns_false(self): init_swap = self.swap_to_always_raise( firebase_admin, 'initialize_app', error=firebase_exceptions.UnknownError('could not init')) with init_swap, self.capture_logging() as logs: self.assertFalse( firebase_auth_services .verify_external_auth_associations_are_deleted(self.USER_ID)) self.assert_matches_regexps(logs, ['could not init'])
def test_is_associated_auth_id_deleted_without_init_returns_false(self): init_swap = self.swap_to_always_raise( firebase_admin, 'initialize_app', error=firebase_exceptions.UnknownError('could not init')) with init_swap, self.capture_logging(min_level=logging.ERROR) as logs: self.assertFalse( firebase_auth_services.are_auth_associations_deleted( self.USER_ID)) self.assert_only_item_is_exception(logs, 'could not init')
def test_returns_none_when_firebase_init_fails(self): initialize_app_swap = self.swap_to_always_raise( firebase_admin, 'initialize_app', error=firebase_exceptions.UnknownError('could not init')) request = self.make_request(auth_header='Bearer DUMMY_JWT') with initialize_app_swap, self.capture_logging() as errors: auth_claims = ( firebase_auth_services.get_auth_claims_from_request(request)) self.assertIsNone(auth_claims) self.assert_matches_regexps(errors, ['could not init'])
def test_returns_none_when_firebase_init_fails(self): initialize_app_swap = self.swap_to_always_raise( firebase_admin, 'initialize_app', error=firebase_exceptions.UnknownError('could not init')) request = self.make_request(auth_header='Bearer DUMMY_JWT') with initialize_app_swap, self.capture_logging() as errors: auth_claims = firebase_auth_services.authenticate_request(request) self.assertIsNone(auth_claims) self.assertEqual(len(errors), 1) self.assertIn('could not init', errors[0])
def handle_googleapiclient_error(error, message=None, code=None, http_response=None): """Constructs a ``FirebaseError`` from the given googleapiclient error. This method is agnostic of the remote service that produced the error, whether it is a GCP service or otherwise. Therefore, this method does not attempt to parse the error response in any way. Args: error: An error raised by the googleapiclient module while making an HTTP call. message: A message to be included in the resulting ``FirebaseError`` (optional). If not specified the string representation of the ``error`` argument is used as the message. code: A GCP error code that will be used to determine the resulting error type (optional). If not specified the HTTP status code on the error response is used to determine a suitable error code. http_response: A requests HTTP response object to associate with the exception (optional). If not specified, one will be created from the ``error``. Returns: FirebaseError: A ``FirebaseError`` that can be raised to the user code. """ if isinstance(error, socket.timeout) or (isinstance(error, socket.error) and 'timed out' in str(error)): return exceptions.DeadlineExceededError( message='Timed out while making an API call: {0}'.format(error), cause=error) elif isinstance(error, httplib2.ServerNotFoundError): return exceptions.UnavailableError( message='Failed to establish a connection: {0}'.format(error), cause=error) elif not isinstance(error, googleapiclient.errors.HttpError): return exceptions.UnknownError( message='Unknown error while making a remote service call: {0}'. format(error), cause=error) if not code: code = _http_status_to_error_code(error.resp.status) if not message: message = str(error) if not http_response: http_response = _http_response_from_googleapiclient_error(error) err_type = _error_code_to_exception_type(code) return err_type(message=message, cause=error, http_response=http_response)
def _poll_app_creation(self, operation_name): """Polls the Long-Running Operation repeatedly until it is done with exponential backoff.""" for current_attempt in range(_ProjectManagementService.MAXIMUM_POLLING_ATTEMPTS): delay_factor = pow( _ProjectManagementService.POLL_EXPONENTIAL_BACKOFF_FACTOR, current_attempt) wait_time_seconds = delay_factor * _ProjectManagementService.POLL_BASE_WAIT_TIME_SECONDS time.sleep(wait_time_seconds) path = '/v1/{0}'.format(operation_name) poll_response, http_response = self._body_and_response('get', path) done = poll_response.get('done') if done: response = poll_response.get('response') if response: return response raise exceptions.UnknownError( 'Polling finished, but the operation terminated in an error.', http_response=http_response) raise exceptions.DeadlineExceededError('Polling deadline exceeded.')
def test_disable_association_warns_when_firebase_fails_to_init(self): firebase_admin.auth.create_user(uid='aid') firebase_auth_services.associate_auth_id_with_user_id( auth_domain.AuthIdUserIdPair('aid', 'uid')) init_swap = self.swap_to_always_raise( firebase_admin, 'initialize_app', error=firebase_exceptions.UnknownError('could not init')) self.assertIsNotNone( auth_models.UserIdByFirebaseAuthIdModel.get('aid', strict=False)) self.assertFalse(firebase_admin.auth.get_user('aid').disabled) with init_swap, self.capture_logging() as logs: firebase_auth_services.mark_user_for_deletion('uid') self.assert_matches_regexps(logs, ['could not init']) self.assertIsNone( auth_models.UserIdByFirebaseAuthIdModel.get('aid', strict=False)) self.assertFalse(firebase_admin.auth.get_user('aid').disabled)
def handle_operation_error(error): """Constructs a ``FirebaseError`` from the given operation error. Args: error: An error returned by a long running operation. Returns: FirebaseError: A ``FirebaseError`` that can be raised to the user code. """ if not isinstance(error, dict): return exceptions.UnknownError( message='Unknown error while making a remote service call: {0}'.format(error), cause=error) rpc_code = error.get('code') message = error.get('message') error_code = _rpc_code_to_error_code(rpc_code) err_type = _error_code_to_exception_type(error_code) return err_type(message=message)
def test_disable_association_warns_when_firebase_fails_to_update_user(self): self.firebase_sdk_stub.create_user('aid') firebase_auth_services.associate_auth_id_with_user_id( auth_domain.AuthIdUserIdPair('aid', 'uid')) update_user_swap = self.swap_to_always_raise( firebase_admin.auth, 'update_user', error=firebase_exceptions.UnknownError('could not update')) log_capturing_context = self.capture_logging() self.assertIsNotNone( auth_models.UserIdByFirebaseAuthIdModel.get('aid', strict=False)) self.assertFalse(firebase_admin.auth.get_user('aid').disabled) with update_user_swap, log_capturing_context as logs: firebase_auth_services.mark_user_for_deletion('uid') self.assert_matches_regexps(logs, ['could not update']) self.assertIsNone( auth_models.UserIdByFirebaseAuthIdModel.get('aid', strict=False)) self.assertFalse(firebase_admin.auth.get_user('aid').disabled)
def handle_requests_error(error, message=None, code=None): """Constructs a ``FirebaseError`` from the given requests error. This method is agnostic of the remote service that produced the error, whether it is a GCP service or otherwise. Therefore, this method does not attempt to parse the error response in any way. Args: error: An error raised by the requests module while making an HTTP call. message: A message to be included in the resulting ``FirebaseError`` (optional). If not specified the string representation of the ``error`` argument is used as the message. code: A GCP error code that will be used to determine the resulting error type (optional). If not specified the HTTP status code on the error response is used to determine a suitable error code. Returns: FirebaseError: A ``FirebaseError`` that can be raised to the user code. """ if isinstance(error, requests.exceptions.Timeout): return exceptions.DeadlineExceededError( message='Timed out while making an API call: {0}'.format(error), cause=error) elif isinstance(error, requests.exceptions.ConnectionError): return exceptions.UnavailableError( message='Failed to establish a connection: {0}'.format(error), cause=error) elif error.response is None: return exceptions.UnknownError( message='Unknown error while making a remote service call: {0}'. format(error), cause=error) if not code: code = _http_status_to_error_code(error.response.status_code) if not message: message = str(error) err_type = _error_code_to_exception_type(code) return err_type(message=message, cause=error, http_response=error.response)
class DeleteAuthAssociationsTests(FirebaseAuthServicesTestBase): EMAIL = '*****@*****.**' USERNAME = '******' AUTH_ID = 'authid' UNKNOWN_ERROR = firebase_exceptions.UnknownError('error') def setUp(self): super(DeleteAuthAssociationsTests, self).setUp() self.firebase_sdk_stub.create_user(self.AUTH_ID) user_settings = user_services.create_new_user(self.AUTH_ID, self.EMAIL) self.user_id = user_settings.user_id firebase_auth_services.associate_auth_id_with_user_id( auth_domain.AuthIdUserIdPair(self.AUTH_ID, self.user_id)) def delete_external_auth_associations(self): """Runs delete_external_auth_associations on the test user.""" firebase_auth_services.delete_external_auth_associations(self.user_id) def assert_firebase_account_is_deleted(self): """Asserts that the Firebase account has been deleted.""" self.assertRaisesRegexp( firebase_admin.auth.UserNotFoundError, 'not found', lambda: firebase_admin.auth.get_user(self.AUTH_ID)) def assert_firebase_account_is_not_deleted(self): """Asserts that the Firebase account still exists.""" user = firebase_admin.auth.get_user(self.AUTH_ID) self.assertIsNotNone(user) self.assertEqual(user.uid, self.AUTH_ID) def swap_initialize_sdk_to_always_fail(self): """Swaps the initialize_app function so that it always fails.""" return self.swap_to_always_raise(firebase_admin, 'initialize_app', error=self.UNKNOWN_ERROR) def swap_get_user_to_always_fail(self): """Swaps the get_user function so that it always fails.""" return self.swap_to_always_raise(firebase_admin.auth, 'get_user', error=self.UNKNOWN_ERROR) def swap_delete_user_to_always_fail(self): """Swaps the delete_user function so that it always fails.""" return self.swap_to_always_raise(firebase_admin.auth, 'delete_user', error=self.UNKNOWN_ERROR) def test_delete_external_auth_associations_happy_path(self): self.delete_external_auth_associations() self.assert_firebase_account_is_deleted() self.assertTrue( firebase_auth_services. verify_external_auth_associations_are_deleted(self.user_id)) def test_delete_external_auth_associations_after_failed_attempt(self): with self.swap_initialize_sdk_to_always_fail(): self.delete_external_auth_associations() self.assert_firebase_account_is_not_deleted() self.assertFalse( firebase_auth_services. verify_external_auth_associations_are_deleted(self.user_id)) self.delete_external_auth_associations() self.assert_firebase_account_is_deleted() self.assertTrue( firebase_auth_services. verify_external_auth_associations_are_deleted(self.user_id)) def test_verify_delete_external_auth_associations_after_failed_attempt( self): self.delete_external_auth_associations() self.assert_firebase_account_is_deleted() with self.swap_initialize_sdk_to_always_fail(): self.assertFalse( firebase_auth_services. verify_external_auth_associations_are_deleted(self.user_id)) self.delete_external_auth_associations() self.assertTrue( firebase_auth_services. verify_external_auth_associations_are_deleted(self.user_id)) def test_delete_external_auth_associations_when_delete_user_fails(self): with self.swap_delete_user_to_always_fail(): self.delete_external_auth_associations() self.assert_firebase_account_is_not_deleted() self.assertFalse( firebase_auth_services. verify_external_auth_associations_are_deleted(self.user_id)) def test_delete_external_auth_associations_when_get_user_fails(self): self.delete_external_auth_associations() self.assert_firebase_account_is_deleted() with self.swap_get_user_to_always_fail(): self.assertFalse( firebase_auth_services. verify_external_auth_associations_are_deleted(self.user_id)) self.assertTrue( firebase_auth_services. verify_external_auth_associations_are_deleted(self.user_id)) def test_delete_external_auth_associations_when_init_fails_during_delete( self): with self.swap_initialize_sdk_to_always_fail(): self.delete_external_auth_associations() self.assert_firebase_account_is_not_deleted() self.assertFalse( firebase_auth_services. verify_external_auth_associations_are_deleted(self.user_id)) def test_delete_external_auth_associations_when_init_fails_during_verify( self): self.delete_external_auth_associations() self.assert_firebase_account_is_deleted() with self.swap_initialize_sdk_to_always_fail(): self.assertFalse( firebase_auth_services. verify_external_auth_associations_are_deleted(self.user_id)) self.assertTrue( firebase_auth_services. verify_external_auth_associations_are_deleted(self.user_id))
def mock_delete_app_error(self): """Returns a context in which `delete_app` raises an exception.""" return self._test.swap_to_always_raise( firebase_admin, 'delete_app', error=firebase_exceptions.UnknownError('could not delete app'))
class FirebaseAccountWipeoutTests(test_utils.GenericTestBase): """Tests for wipeout_service that is specific to Firebase authentication.""" EMAIL = '*****@*****.**' USERNAME = '******' AUTH_ID = 'authid' UNKNOWN_ERROR = firebase_exceptions.UnknownError('error') def setUp(self): super(FirebaseAccountWipeoutTests, self).setUp() self._uninstall_stub = FirebaseAdminSdkStub.install(self) firebase_admin.auth.create_user(uid=self.AUTH_ID) self.signup(self.EMAIL, self.USERNAME) self.user_id = self.get_user_id_from_email(self.EMAIL) firebase_auth_services.associate_auth_id_to_user_id( auth_domain.AuthIdUserIdPair(self.AUTH_ID, self.user_id)) wipeout_service.pre_delete_user(self.user_id) def tearDown(self): self._uninstall_stub() super(FirebaseAccountWipeoutTests, self).tearDown() def wipeout(self): """Runs wipeout on the user created by this test.""" wipeout_service.delete_user( wipeout_service.get_pending_deletion_request(self.user_id)) def assert_wipeout_is_verified(self): """Asserts that the wipeout has been acknowledged as complete.""" self.assertTrue(wipeout_service.verify_user_deleted(self.user_id)) def assert_wipeout_is_not_verified(self): """Asserts that the wipeout has been acknowledged as incomplete.""" self.assertFalse(wipeout_service.verify_user_deleted(self.user_id)) def assert_firebase_account_is_deleted(self): """Asserts that the Firebase account has been deleted.""" self.assertRaisesRegexp( firebase_admin.auth.UserNotFoundError, 'not found', lambda: firebase_admin.auth.get_user(self.AUTH_ID)) def assert_firebase_account_is_not_deleted(self): """Asserts that the Firebase account still exists.""" user = firebase_admin.auth.get_user(self.AUTH_ID) self.assertIsNotNone(user) self.assertEqual(user.uid, self.AUTH_ID) def swap_initialize_sdk_to_always_fail(self): """Swaps the initialize_app function so that it always fails.""" return self.swap_to_always_raise( firebase_admin, 'initialize_app', error=self.UNKNOWN_ERROR) def swap_get_user_to_always_fail(self): """Swaps the get_user function so that it always fails.""" return self.swap_to_always_raise( firebase_admin.auth, 'get_user', error=self.UNKNOWN_ERROR) def swap_delete_user_to_always_fail(self): """Swaps the delete_user function so that it always fails.""" return self.swap_to_always_raise( firebase_admin.auth, 'delete_user', error=self.UNKNOWN_ERROR) def test_wipeout_happy_path(self): self.wipeout() self.assert_firebase_account_is_deleted() self.assert_wipeout_is_verified() def test_wipeout_retry_after_failed_attempt(self): with self.swap_initialize_sdk_to_always_fail(): self.wipeout() self.assert_firebase_account_is_not_deleted() self.assert_wipeout_is_not_verified() self.wipeout() self.assert_firebase_account_is_deleted() self.assert_wipeout_is_verified() def test_wipeout_retry_after_unverified_successful_attempt(self): self.wipeout() self.assert_firebase_account_is_deleted() with self.swap_initialize_sdk_to_always_fail(): self.assert_wipeout_is_not_verified() self.wipeout() self.assert_wipeout_is_verified() def test_wipeout_when_delete_user_fails(self): with self.swap_delete_user_to_always_fail(): self.wipeout() self.assert_firebase_account_is_not_deleted() self.assert_wipeout_is_not_verified() def test_wipeout_when_get_user_fails(self): self.wipeout() self.assert_firebase_account_is_deleted() with self.swap_get_user_to_always_fail(): self.assert_wipeout_is_not_verified() self.assert_wipeout_is_verified() def test_wipeout_when_init_fails_during_delete(self): with self.swap_initialize_sdk_to_always_fail(): self.wipeout() self.assert_firebase_account_is_not_deleted() self.assert_wipeout_is_not_verified() def test_wipeout_when_init_fails_during_verify(self): self.wipeout() self.assert_firebase_account_is_deleted() with self.swap_initialize_sdk_to_always_fail(): self.assert_wipeout_is_not_verified() self.assert_wipeout_is_verified()