Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: move to using insecure grpc channels with emulator #402

Merged
merged 13 commits into from Jul 22, 2021
53 changes: 14 additions & 39 deletions google/cloud/firestore_v1/base_client.py
Expand Up @@ -167,50 +167,25 @@ 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
crwilcox marked this conversation as resolved.
Show resolved Hide resolved
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
token = "owner"
crwilcox marked this conversation as resolved.
Show resolved Hide resolved
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")]
crwilcox marked this conversation as resolved.
Show resolved Hide resolved
)
)

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