Skip to content

Commit

Permalink
fix: move to using insecure grpc channels with emulator (#402)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
crwilcox committed Jul 22, 2021
1 parent 58300d3 commit 4381ad5
Show file tree
Hide file tree
Showing 2 changed files with 35 additions and 57 deletions.
54 changes: 15 additions & 39 deletions google/cloud/firestore_v1/base_client.py
Expand Up @@ -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).
Expand Down
38 changes: 20 additions & 18 deletions tests/unit/v1/test_base_client.py
Expand Up @@ -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
)
Expand All @@ -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()
Expand Down Expand Up @@ -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):
Expand Down

0 comments on commit 4381ad5

Please sign in to comment.