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

feat: allow default_host and default_scopes to be passed to create_channel #134

Merged
merged 12 commits into from
Feb 5, 2021
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,
)
busunkim96 marked this conversation as resolved.
Show resolved Hide resolved
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")
busunkim96 marked this conversation as resolved.
Show resolved Hide resolved

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