From 4381ad503ca3e83510b876281fc768d00d40d499 Mon Sep 17 00:00:00 2001 From: Christopher Wilcox Date: Thu, 22 Jul 2021 10:14:32 -0700 Subject: [PATCH] fix: move to using insecure grpc channels with emulator (#402) * fix: move to using insecure grpc channels with emulator * chore: format * fix: add code to manually inject the id token on an insecure channel * chore: add line for comment * test: use the correct credentials object in mock * chore: black * chore: unused var * always configure the bearer token, even if not available * test: test the path populating an id token * chore: remove unused code and testing of unused code * chore: remove some code repetition * chore: feedback --- google/cloud/firestore_v1/base_client.py | 54 +++++++----------------- tests/unit/v1/test_base_client.py | 38 +++++++++-------- 2 files changed, 35 insertions(+), 57 deletions(-) diff --git a/google/cloud/firestore_v1/base_client.py b/google/cloud/firestore_v1/base_client.py index b2af21e3f..7eb5c26b0 100644 --- a/google/cloud/firestore_v1/base_client.py +++ b/google/cloud/firestore_v1/base_client.py @@ -167,50 +167,26 @@ def _firestore_api_helper(self, transport, client_class, client_module) -> Any: def _emulator_channel(self, transport): """ - Creates a channel using self._credentials in a similar way to grpc.secure_channel but - using grpc.local_channel_credentials() rather than grpc.ssh_channel_credentials() to allow easy connection - to a local firestore emulator. This allows local testing of firestore rules if the credentials have been - created from a signed custom token. + Creates an insecure channel to communicate with the local emulator. + If credentials are provided the token is extracted and added to the + headers. This supports local testing of firestore rules if the credentials + have been created from a signed custom token. :return: grpc.Channel or grpc.aio.Channel """ - # TODO: Implement a special credentials type for emulator and use - # "transport.create_channel" to create gRPC channels once google-auth - # extends it's allowed credentials types. + # Insecure channels are used for the emulator as secure channels + # cannot be used to communicate on some environments. + # https://github.com/googleapis/python-firestore/issues/359 + # Default the token to a non-empty string, in this case "owner". + token = "owner" + if self._credentials is not None and self._credentials.id_token is not None: + token = self._credentials.id_token + options = [("Authorization", f"Bearer {token}")] + if "GrpcAsyncIOTransport" in str(transport.__name__): - return grpc.aio.secure_channel( - self._emulator_host, self._local_composite_credentials() - ) + return grpc.aio.insecure_channel(self._emulator_host, options=options) else: - return grpc.secure_channel( - self._emulator_host, self._local_composite_credentials() - ) - - def _local_composite_credentials(self): - """ - Creates the credentials for the local emulator channel - :return: grpc.ChannelCredentials - """ - credentials = google.auth.credentials.with_scopes_if_required( - self._credentials, None - ) - request = google.auth.transport.requests.Request() - - # Create the metadata plugin for inserting the authorization header. - metadata_plugin = google.auth.transport.grpc.AuthMetadataPlugin( - credentials, request - ) - - # Create a set of grpc.CallCredentials using the metadata plugin. - google_auth_credentials = grpc.metadata_call_credentials(metadata_plugin) - - # Using the local_credentials to allow connection to emulator - local_credentials = grpc.local_channel_credentials() - - # Combine the local credentials and the authorization credentials. - return grpc.composite_channel_credentials( - local_credentials, google_auth_credentials - ) + return grpc.insecure_channel(self._emulator_host, options=options) def _target_helper(self, client_class) -> str: """Return the target (where the API is). diff --git a/tests/unit/v1/test_base_client.py b/tests/unit/v1/test_base_client.py index fd176d760..5de0e4962 100644 --- a/tests/unit/v1/test_base_client.py +++ b/tests/unit/v1/test_base_client.py @@ -146,11 +146,11 @@ def test_emulator_channel(self): ) emulator_host = "localhost:8081" + credentials = _make_credentials() + database = "quanta" with mock.patch("os.getenv") as getenv: getenv.return_value = emulator_host - - credentials = _make_credentials() - database = "quanta" + credentials.id_token = None client = self._make_one( project=self.PROJECT, credentials=credentials, database=database ) @@ -160,21 +160,23 @@ def test_emulator_channel(self): self.assertTrue(isinstance(channel, grpc.Channel)) channel = client._emulator_channel(FirestoreGrpcAsyncIOTransport) self.assertTrue(isinstance(channel, grpc.aio.Channel)) - # checks that the credentials are composite ones using a local channel from grpc - composite_credentials = client._local_composite_credentials() - self.assertTrue(isinstance(composite_credentials, grpc.ChannelCredentials)) - self.assertTrue( - isinstance( - composite_credentials._credentials._call_credentialses[0], - grpc._cython.cygrpc.MetadataPluginCallCredentials, + + # Verify that when credentials are provided with an id token it is used + # for channel construction + # NOTE: On windows, emulation requires an insecure channel. If this is + # altered to use a secure channel, start by verifying that it still + # works as expected on windows. + with mock.patch("os.getenv") as getenv: + getenv.return_value = emulator_host + credentials.id_token = "test" + client = self._make_one( + project=self.PROJECT, credentials=credentials, database=database ) - ) - self.assertTrue( - isinstance( - composite_credentials._credentials._channel_credentials, - grpc._cython.cygrpc.LocalChannelCredentials, + with mock.patch("grpc.insecure_channel") as insecure_channel: + channel = client._emulator_channel(FirestoreGrpcTransport) + insecure_channel.assert_called_once_with( + emulator_host, options=[("Authorization", "Bearer test")] ) - ) def test_field_path(self): klass = self._get_target_class() @@ -392,9 +394,9 @@ def test_paths(self): def _make_credentials(): - import google.auth.credentials + import google.oauth2.credentials - return mock.Mock(spec=google.auth.credentials.Credentials) + return mock.Mock(spec=google.oauth2.credentials.Credentials) def _make_batch_response(**kwargs):