Skip to content
Merged
65 changes: 58 additions & 7 deletions google/api_core/grpc_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,11 @@ def wrap_errors(callable_):
def _create_composite_credentials(
credentials=None,
credentials_file=None,
default_scopes=None,
scopes=None,
ssl_credentials=None,
quota_project_id=None):
quota_project_id=None,
default_host=None):
"""Create the composite credentials for secure channels.

Args:
Expand All @@ -191,12 +193,16 @@ def _create_composite_credentials(
credentials_file (str): A file with credentials that can be loaded with
:func:`google.auth.load_credentials_from_file`. This argument is
mutually exclusive with credentials.
default_scopes (Sequence[str]): A optional list of scopes needed for this
service. These are only used when credentials are not specified and
are passed to :func:`google.auth.default`.
scopes (Sequence[str]): A optional list of scopes needed for this
service. These are only used when credentials are not specified and
are passed to :func:`google.auth.default`.
ssl_credentials (grpc.ChannelCredentials): Optional SSL channel
credentials. This can be used to specify different certificates.
quota_project_id (str): An optional project to use for billing and quota.
default_host (str): The default endpoint. e.g., "pubsub.googleapis.com".

Returns:
grpc.ChannelCredentials: The composed channel credentials object.
Expand All @@ -210,21 +216,59 @@ def _create_composite_credentials(
)

if credentials_file:
credentials, _ = google.auth.load_credentials_from_file(credentials_file, scopes=scopes)
try:
credentials, _ = google.auth.load_credentials_from_file(
credentials_file,
scopes=scopes,
default_scopes=default_scopes
)
# google-auth < x.x.x does not have `default_scopes`
# TODO: remove this try/except once google-auth >= x.x.x is required
except TypeError:
credentials, _ = google.auth.load_credentials_from_file(
credentials_file,
scopes=scopes or default_scopes,
)
elif credentials:
credentials = google.auth.credentials.with_scopes_if_required(credentials, scopes)
try:
credentials = google.auth.credentials.with_scopes_if_required(
credentials,
scopes=scopes,
default_scopes=default_scopes
)
# google-auth < x.x.x does not have `default_scopes`
# TODO: remove this try/except once google-auth >= x.x.x is required
except TypeError:
credentials = google.auth.credentials.with_scopes_if_required(
credentials,
scopes=scopes or default_scopes,
)

else:
credentials, _ = google.auth.default(scopes=scopes)
try:
credentials, _ = google.auth.default(scopes=scopes, default_scopes=default_scopes)
# google-auth < x.x.x does not have `default_scopes`
# TODO: remove this try/except once google-auth >= x.x.x is required
except TypeError:
credentials, _ = google.auth.default(scopes=scopes or default_scopes)

if quota_project_id and isinstance(credentials, google.auth.credentials.CredentialsWithQuotaProject):
credentials = credentials.with_quota_project(quota_project_id)

request = google.auth.transport.requests.Request()

# Create the metadata plugin for inserting the authorization header.
metadata_plugin = google.auth.transport.grpc.AuthMetadataPlugin(
credentials, request
)

# google-auth < x.x.x does not have `default_host`
# TODO: remove this try/except once google-auth >= x.x.x is required
try:
metadata_plugin = google.auth.transport.grpc.AuthMetadataPlugin(
credentials, request, default_host=default_host,
)
except:
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)
Expand All @@ -245,6 +289,8 @@ def create_channel(
ssl_credentials=None,
credentials_file=None,
quota_project_id=None,
default_scopes=None,
default_host=None,
**kwargs):
"""Create a secure channel with credentials.

Expand All @@ -262,6 +308,9 @@ def create_channel(
:func:`google.auth.load_credentials_from_file`. This argument is
mutually exclusive with credentials.
quota_project_id (str): An optional project to use for billing and quota.
default_scopes (Sequence[str]): Default scopes passed by a Google client
library. Use 'scopes' for user-defined scopes.
default_host (str): The default endpoint. e.g., "pubsub.googleapis.com".
kwargs: Additional key-word args passed to
:func:`grpc_gcp.secure_channel` or :func:`grpc.secure_channel`.

Expand All @@ -275,9 +324,11 @@ def create_channel(
composite_credentials = _create_composite_credentials(
credentials=credentials,
credentials_file=credentials_file,
default_scopes=default_scopes,
scopes=scopes,
ssl_credentials=ssl_credentials,
quota_project_id=quota_project_id,
default_host=default_host,
)

if HAS_GRPC_GCP:
Expand Down
7 changes: 7 additions & 0 deletions google/api_core/grpc_helpers_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ def create_channel(
ssl_credentials=None,
credentials_file=None,
quota_project_id=None,
default_scopes=None,
default_host=None,
**kwargs):
"""Create an AsyncIO secure channel with credentials.

Expand All @@ -230,6 +232,9 @@ def create_channel(
:func:`google.auth.load_credentials_from_file`. This argument is
mutually exclusive with credentials.
quota_project_id (str): An optional project to use for billing and quota.
default_scopes (Sequence[str]): Default scopes passed by a Google client
library. Use 'scopes' for user-defined scopes.
default_host (str): The default endpoint. e.g., "pubsub.googleapis.com".
kwargs: Additional key-word args passed to :func:`aio.secure_channel`.

Returns:
Expand All @@ -243,8 +248,10 @@ def create_channel(
credentials=credentials,
credentials_file=credentials_file,
scopes=scopes,
default_scopes=default_scopes,
ssl_credentials=ssl_credentials,
quota_project_id=quota_project_id,
default_host=default_host
)

return aio.secure_channel(target, composite_credentials, **kwargs)
Expand Down
3 changes: 3 additions & 0 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ def default(session):
session.install("mock", "pytest", "pytest-cov", "grpcio >= 1.0.2")
session.install("-e", ".", "-c", constraints_path)

# REMOVE ME: Temporarily install google-auth from a branch
session.install("-e", "git+https://github.com/googleapis/google-auth-library-python.git@self-signed-jwt#egg=google-auth")

pytest_args = [
"python",
"-m",
Expand Down
102 changes: 94 additions & 8 deletions tests/asyncio/test_grpc_helpers_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,31 @@ def test_create_channel_implicit(grpc_secure_channel, default, composite_creds_c
channel = grpc_helpers_async.create_channel(target)

assert channel is grpc_secure_channel.return_value
default.assert_called_once_with(scopes=None)
default.assert_called_once_with(scopes=None, default_scopes=None)
grpc_secure_channel.assert_called_once_with(target, composite_creds)


@mock.patch("google.auth.transport.grpc.AuthMetadataPlugin")
@mock.patch(
"google.auth.transport.requests.Request",
return_value=mock.sentinel.Request
)
@mock.patch("grpc.composite_channel_credentials")
@mock.patch(
"google.auth.default",
return_value=(mock.sentinel.credentials, mock.sentinel.projet),
)
@mock.patch("grpc.experimental.aio.secure_channel")
def test_create_channel_implicit_with_default_host(grpc_secure_channel, default, composite_creds_call, request, auth_metadata_plugin):
target = "example.com:443"
default_host = "example.com"
composite_creds = composite_creds_call.return_value

channel = grpc_helpers_async.create_channel(target, default_host=default_host)

assert channel is grpc_secure_channel.return_value
default.assert_called_once_with(scopes=None, default_scopes=None)
auth_metadata_plugin.assert_called_once_with(mock.sentinel.credentials, mock.sentinel.Request, default_host=default_host)
grpc_secure_channel.assert_called_once_with(target, composite_creds)


Expand All @@ -292,7 +316,7 @@ def test_create_channel_implicit_with_ssl_creds(

grpc_helpers_async.create_channel(target, ssl_credentials=ssl_creds)

default.assert_called_once_with(scopes=None)
default.assert_called_once_with(scopes=None, default_scopes=None)
composite_creds_call.assert_called_once_with(ssl_creds, mock.ANY)
composite_creds = composite_creds_call.return_value
grpc_secure_channel.assert_called_once_with(target, composite_creds)
Expand All @@ -313,7 +337,26 @@ def test_create_channel_implicit_with_scopes(
channel = grpc_helpers_async.create_channel(target, scopes=["one", "two"])

assert channel is grpc_secure_channel.return_value
default.assert_called_once_with(scopes=["one", "two"])
default.assert_called_once_with(scopes=["one", "two"], default_scopes=None)
grpc_secure_channel.assert_called_once_with(target, composite_creds)


@mock.patch("grpc.composite_channel_credentials")
@mock.patch(
"google.auth.default",
return_value=(mock.sentinel.credentials, mock.sentinel.projet),
)
@mock.patch("grpc.experimental.aio.secure_channel")
def test_create_channel_implicit_with_default_scopes(
grpc_secure_channel, default, composite_creds_call
):
target = "example.com:443"
composite_creds = composite_creds_call.return_value

channel = grpc_helpers_async.create_channel(target, scopes=["one", "two"], default_scopes=["three", "four"])

assert channel is grpc_secure_channel.return_value
default.assert_called_once_with(scopes=["one", "two"], default_scopes=["three", "four"])
grpc_secure_channel.assert_called_once_with(target, composite_creds)


Expand All @@ -339,7 +382,7 @@ def test_create_channel_explicit(grpc_secure_channel, auth_creds, composite_cred

channel = grpc_helpers_async.create_channel(target, credentials=mock.sentinel.credentials)

auth_creds.assert_called_once_with(mock.sentinel.credentials, None)
auth_creds.assert_called_once_with(mock.sentinel.credentials, scopes=None, default_scopes=None)
assert channel is grpc_secure_channel.return_value
grpc_secure_channel.assert_called_once_with(target, composite_creds)

Expand All @@ -358,7 +401,27 @@ def test_create_channel_explicit_scoped(grpc_secure_channel, composite_creds_cal
target, credentials=credentials, scopes=scopes
)

credentials.with_scopes.assert_called_once_with(scopes)
credentials.with_scopes.assert_called_once_with(scopes, default_scopes=None)
assert channel is grpc_secure_channel.return_value
grpc_secure_channel.assert_called_once_with(target, composite_creds)


@mock.patch("grpc.composite_channel_credentials")
@mock.patch("grpc.experimental.aio.secure_channel")
def test_create_channel_explicit_default_scopes(grpc_secure_channel, composite_creds_call):
target = "example.com:443"
scopes = ["1", "2"]
default_scopes = ["3", "4"]
composite_creds = composite_creds_call.return_value

credentials = mock.create_autospec(google.auth.credentials.Scoped, instance=True)
credentials.requires_scopes = True

channel = grpc_helpers_async.create_channel(
target, credentials=credentials, scopes=scopes, default_scopes=default_scopes
)

credentials.with_scopes.assert_called_once_with(scopes, default_scopes=default_scopes)
assert channel is grpc_secure_channel.return_value
grpc_secure_channel.assert_called_once_with(target, composite_creds)

Expand Down Expand Up @@ -396,7 +459,7 @@ def test_create_channnel_with_credentials_file(load_credentials_from_file, grpc_
target, credentials_file=credentials_file
)

google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, scopes=None)
google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, scopes=None, default_scopes=None)
assert channel is grpc_secure_channel.return_value
grpc_secure_channel.assert_called_once_with(target, composite_creds)

Expand All @@ -418,7 +481,30 @@ def test_create_channel_with_credentials_file_and_scopes(load_credentials_from_f
target, credentials_file=credentials_file, scopes=scopes
)

google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, scopes=scopes)
google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, scopes=scopes, default_scopes=None)
assert channel is grpc_secure_channel.return_value
grpc_secure_channel.assert_called_once_with(target, composite_creds)


@mock.patch("grpc.composite_channel_credentials")
@mock.patch("grpc.experimental.aio.secure_channel")
@mock.patch(
"google.auth.load_credentials_from_file",
return_value=(mock.sentinel.credentials, mock.sentinel.project)
)
def test_create_channel_with_credentials_file_and_default_scopes(load_credentials_from_file, grpc_secure_channel, composite_creds_call):
target = "example.com:443"
scopes = ["1", "2"]
default_scopes = ["3", "4"]

credentials_file = "/path/to/credentials/file.json"
composite_creds = composite_creds_call.return_value

channel = grpc_helpers_async.create_channel(
target, credentials_file=credentials_file, scopes=scopes, default_scopes=default_scopes
)

google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, scopes=scopes, default_scopes=default_scopes)
assert channel is grpc_secure_channel.return_value
grpc_secure_channel.assert_called_once_with(target, composite_creds)

Expand All @@ -434,7 +520,7 @@ def test_create_channel_without_grpc_gcp(grpc_secure_channel):

grpc_helpers_async.create_channel(target, credentials=credentials, scopes=scopes)
grpc_secure_channel.assert_called()
credentials.with_scopes.assert_called_once_with(scopes)
credentials.with_scopes.assert_called_once_with(scopes, default_scopes=None)


@pytest.mark.asyncio
Expand Down
Loading