Skip to content

Commit

Permalink
feat: allow credentials files to be passed for channel creation (#50)
Browse files Browse the repository at this point in the history
Co-authored-by: Dov Shlachter <dovs@google.com>
  • Loading branch information
busunkim96 and software-dov committed Jun 18, 2020
1 parent 7531a5e commit ded92d0
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 12 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Expand Up @@ -57,4 +57,7 @@ system_tests/local_test_setup

# Make sure a generated file isn't accidentally committed.
pylintrc
pylintrc.test
pylintrc.test

# pytype
pytype_output
6 changes: 6 additions & 0 deletions google/api_core/exceptions.py
Expand Up @@ -41,6 +41,12 @@ class GoogleAPIError(Exception):
pass


class DuplicateCredentialArgs(GoogleAPIError):
"""Raised when multiple credentials are passed."""

pass


@six.python_2_unicode_compatible
class RetryError(GoogleAPIError):
"""Raised when a function has exhausted all of its available retries.
Expand Down
37 changes: 29 additions & 8 deletions google/api_core/grpc_helpers.py
Expand Up @@ -176,13 +176,16 @@ def wrap_errors(callable_):
return _wrap_unary_errors(callable_)


def _create_composite_credentials(credentials=None, scopes=None, ssl_credentials=None):
def _create_composite_credentials(credentials=None, credentials_file=None, scopes=None, ssl_credentials=None):
"""Create the composite credentials for secure channels.
Args:
credentials (google.auth.credentials.Credentials): The credentials. If
not specified, then this function will attempt to ascertain the
credentials from the environment using :func:`google.auth.default`.
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.
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`.
Expand All @@ -191,14 +194,22 @@ def _create_composite_credentials(credentials=None, scopes=None, ssl_credentials
Returns:
grpc.ChannelCredentials: The composed channel credentials object.
Raises:
google.api_core.DuplicateCredentialArgs: If both a credentials object and credentials_file are passed.
"""
if credentials is None:
credentials, _ = google.auth.default(scopes=scopes)
else:
credentials = google.auth.credentials.with_scopes_if_required(
credentials, scopes
if credentials and credentials_file:
raise exceptions.DuplicateCredentialArgs(
"'credentials' and 'credentials_file' are mutually exclusive."
)

if credentials_file:
credentials, _ = google.auth.load_credentials_from_file(credentials_file, scopes=scopes)
elif credentials:
credentials = google.auth.credentials.with_scopes_if_required(credentials, scopes)
else:
credentials, _ = google.auth.default(scopes=scopes)

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

# Create the metadata plugin for inserting the authorization header.
Expand All @@ -218,7 +229,7 @@ def _create_composite_credentials(credentials=None, scopes=None, ssl_credentials
)


def create_channel(target, credentials=None, scopes=None, ssl_credentials=None, **kwargs):
def create_channel(target, credentials=None, scopes=None, ssl_credentials=None, credentials_file=None, **kwargs):
"""Create a secure channel with credentials.
Args:
Expand All @@ -231,14 +242,24 @@ def create_channel(target, credentials=None, scopes=None, ssl_credentials=None,
are passed to :func:`google.auth.default`.
ssl_credentials (grpc.ChannelCredentials): Optional SSL channel
credentials. This can be used to specify different certificates.
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.
kwargs: Additional key-word args passed to
:func:`grpc_gcp.secure_channel` or :func:`grpc.secure_channel`.
Returns:
grpc.Channel: The created channel.
Raises:
google.api_core.DuplicateCredentialArgs: If both a credentials object and credentials_file are passed.
"""

composite_credentials = _create_composite_credentials(
credentials, scopes, ssl_credentials
credentials=credentials,
credentials_file=credentials_file,
scopes=scopes,
ssl_credentials=ssl_credentials
)

if HAS_GRPC_GCP:
Expand Down
14 changes: 12 additions & 2 deletions google/api_core/grpc_helpers_async.py
Expand Up @@ -206,7 +206,7 @@ def wrap_errors(callable_):
return _wrap_stream_errors(callable_)


def create_channel(target, credentials=None, scopes=None, ssl_credentials=None, **kwargs):
def create_channel(target, credentials=None, scopes=None, ssl_credentials=None, credentials_file=None, **kwargs):
"""Create an AsyncIO secure channel with credentials.
Args:
Expand All @@ -219,13 +219,23 @@ def create_channel(target, credentials=None, scopes=None, ssl_credentials=None,
are passed to :func:`google.auth.default`.
ssl_credentials (grpc.ChannelCredentials): Optional SSL channel
credentials. This can be used to specify different certificates.
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.
kwargs: Additional key-word args passed to :func:`aio.secure_channel`.
Returns:
aio.Channel: The created channel.
Raises:
google.api_core.DuplicateCredentialArgs: If both a credentials object and credentials_file are passed.
"""

composite_credentials = grpc_helpers._create_composite_credentials(
credentials, scopes, ssl_credentials
credentials=credentials,
credentials_file=credentials_file,
scopes=scopes,
ssl_credentials=ssl_credentials
)

return aio.secure_channel(target, composite_credentials, **kwargs)
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -31,7 +31,7 @@
dependencies = [
"googleapis-common-protos >= 1.6.0, < 2.0dev",
"protobuf >= 3.12.0",
"google-auth >= 1.14.0, < 2.0dev",
"google-auth >= 1.18.0, < 2.0dev",
"requests >= 2.18.0, < 3.0.0dev",
"setuptools >= 34.0.0",
"six >= 1.10.0",
Expand Down
56 changes: 56 additions & 0 deletions tests/asyncio/test_grpc_helpers_async.py
Expand Up @@ -317,6 +317,19 @@ def test_create_channel_implicit_with_scopes(
grpc_secure_channel.assert_called_once_with(target, composite_creds)


def test_create_channel_explicit_with_duplicate_credentials():
target = "example:443"

with pytest.raises(exceptions.DuplicateCredentialArgs) as excinfo:
grpc_helpers_async.create_channel(
target,
credentials_file="credentials.json",
credentials=mock.sentinel.credentials
)

assert "mutually exclusive" in str(excinfo.value)


@mock.patch("grpc.composite_channel_credentials")
@mock.patch("google.auth.credentials.with_scopes_if_required")
@mock.patch("grpc.experimental.aio.secure_channel")
Expand Down Expand Up @@ -350,6 +363,49 @@ def test_create_channel_explicit_scoped(grpc_secure_channel, composite_creds_cal
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_channnel_with_credentials_file(load_credentials_from_file, grpc_secure_channel, composite_creds_call):
target = "example.com:443"

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
)

google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, 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_scopes(load_credentials_from_file, grpc_secure_channel, composite_creds_call):
target = "example.com:443"
scopes = ["1", "2"]

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
)

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


@pytest.mark.skipif(grpc_helpers_async.HAS_GRPC_GCP, reason="grpc_gcp module not available")
@mock.patch("grpc.experimental.aio.secure_channel")
def test_create_channel_without_grpc_gcp(grpc_secure_channel):
Expand Down
61 changes: 61 additions & 0 deletions tests/unit/test_grpc_helpers.py
Expand Up @@ -285,6 +285,17 @@ def test_create_channel_implicit_with_scopes(
grpc_secure_channel.assert_called_once_with(target, composite_creds)


def test_create_channel_explicit_with_duplicate_credentials():
target = "example.com:443"

with pytest.raises(exceptions.DuplicateCredentialArgs):
grpc_helpers.create_channel(
target,
credentials_file="credentials.json",
credentials=mock.sentinel.credentials
)


@mock.patch("grpc.composite_channel_credentials")
@mock.patch("google.auth.credentials.with_scopes_if_required")
@mock.patch("grpc.secure_channel")
Expand Down Expand Up @@ -324,6 +335,56 @@ def test_create_channel_explicit_scoped(grpc_secure_channel, composite_creds_cal
grpc_secure_channel.assert_called_once_with(target, composite_creds)


@mock.patch("grpc.composite_channel_credentials")
@mock.patch("grpc.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(load_credentials_from_file, grpc_secure_channel, composite_creds_call):
target = "example.com:443"

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

channel = grpc_helpers.create_channel(
target, credentials_file=credentials_file
)

google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, scopes=None)

assert channel is grpc_secure_channel.return_value
if grpc_helpers.HAS_GRPC_GCP:
grpc_secure_channel.assert_called_once_with(target, composite_creds, None)
else:
grpc_secure_channel.assert_called_once_with(target, composite_creds)


@mock.patch("grpc.composite_channel_credentials")
@mock.patch("grpc.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_scopes(load_credentials_from_file, grpc_secure_channel, composite_creds_call):
target = "example.com:443"
scopes = ["1", "2"]

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

channel = grpc_helpers.create_channel(
target, credentials_file=credentials_file, scopes=scopes
)

google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, scopes=scopes)
assert channel is grpc_secure_channel.return_value
if grpc_helpers.HAS_GRPC_GCP:
grpc_secure_channel.assert_called_once_with(target, composite_creds, None)
else:
grpc_secure_channel.assert_called_once_with(target, composite_creds)


@pytest.mark.skipif(
not grpc_helpers.HAS_GRPC_GCP, reason="grpc_gcp module not available"
)
Expand Down

0 comments on commit ded92d0

Please sign in to comment.