Skip to content

Commit ded92d0

Browse files
feat: allow credentials files to be passed for channel creation (googleapis#50)
Co-authored-by: Dov Shlachter <dovs@google.com>
1 parent 7531a5e commit ded92d0

File tree

7 files changed

+169
-12
lines changed

7 files changed

+169
-12
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,7 @@ system_tests/local_test_setup
5757

5858
# Make sure a generated file isn't accidentally committed.
5959
pylintrc
60-
pylintrc.test
60+
pylintrc.test
61+
62+
# pytype
63+
pytype_output

google/api_core/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ class GoogleAPIError(Exception):
4141
pass
4242

4343

44+
class DuplicateCredentialArgs(GoogleAPIError):
45+
"""Raised when multiple credentials are passed."""
46+
47+
pass
48+
49+
4450
@six.python_2_unicode_compatible
4551
class RetryError(GoogleAPIError):
4652
"""Raised when a function has exhausted all of its available retries.

google/api_core/grpc_helpers.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -176,13 +176,16 @@ def wrap_errors(callable_):
176176
return _wrap_unary_errors(callable_)
177177

178178

179-
def _create_composite_credentials(credentials=None, scopes=None, ssl_credentials=None):
179+
def _create_composite_credentials(credentials=None, credentials_file=None, scopes=None, ssl_credentials=None):
180180
"""Create the composite credentials for secure channels.
181181
182182
Args:
183183
credentials (google.auth.credentials.Credentials): The credentials. If
184184
not specified, then this function will attempt to ascertain the
185185
credentials from the environment using :func:`google.auth.default`.
186+
credentials_file (str): A file with credentials that can be loaded with
187+
:func:`google.auth.load_credentials_from_file`. This argument is
188+
mutually exclusive with credentials.
186189
scopes (Sequence[str]): A optional list of scopes needed for this
187190
service. These are only used when credentials are not specified and
188191
are passed to :func:`google.auth.default`.
@@ -191,14 +194,22 @@ def _create_composite_credentials(credentials=None, scopes=None, ssl_credentials
191194
192195
Returns:
193196
grpc.ChannelCredentials: The composed channel credentials object.
197+
198+
Raises:
199+
google.api_core.DuplicateCredentialArgs: If both a credentials object and credentials_file are passed.
194200
"""
195-
if credentials is None:
196-
credentials, _ = google.auth.default(scopes=scopes)
197-
else:
198-
credentials = google.auth.credentials.with_scopes_if_required(
199-
credentials, scopes
201+
if credentials and credentials_file:
202+
raise exceptions.DuplicateCredentialArgs(
203+
"'credentials' and 'credentials_file' are mutually exclusive."
200204
)
201205

206+
if credentials_file:
207+
credentials, _ = google.auth.load_credentials_from_file(credentials_file, scopes=scopes)
208+
elif credentials:
209+
credentials = google.auth.credentials.with_scopes_if_required(credentials, scopes)
210+
else:
211+
credentials, _ = google.auth.default(scopes=scopes)
212+
202213
request = google.auth.transport.requests.Request()
203214

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

220231

221-
def create_channel(target, credentials=None, scopes=None, ssl_credentials=None, **kwargs):
232+
def create_channel(target, credentials=None, scopes=None, ssl_credentials=None, credentials_file=None, **kwargs):
222233
"""Create a secure channel with credentials.
223234
224235
Args:
@@ -231,14 +242,24 @@ def create_channel(target, credentials=None, scopes=None, ssl_credentials=None,
231242
are passed to :func:`google.auth.default`.
232243
ssl_credentials (grpc.ChannelCredentials): Optional SSL channel
233244
credentials. This can be used to specify different certificates.
245+
credentials_file (str): A file with credentials that can be loaded with
246+
:func:`google.auth.load_credentials_from_file`. This argument is
247+
mutually exclusive with credentials.
234248
kwargs: Additional key-word args passed to
235249
:func:`grpc_gcp.secure_channel` or :func:`grpc.secure_channel`.
236250
237251
Returns:
238252
grpc.Channel: The created channel.
253+
254+
Raises:
255+
google.api_core.DuplicateCredentialArgs: If both a credentials object and credentials_file are passed.
239256
"""
257+
240258
composite_credentials = _create_composite_credentials(
241-
credentials, scopes, ssl_credentials
259+
credentials=credentials,
260+
credentials_file=credentials_file,
261+
scopes=scopes,
262+
ssl_credentials=ssl_credentials
242263
)
243264

244265
if HAS_GRPC_GCP:

google/api_core/grpc_helpers_async.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ def wrap_errors(callable_):
206206
return _wrap_stream_errors(callable_)
207207

208208

209-
def create_channel(target, credentials=None, scopes=None, ssl_credentials=None, **kwargs):
209+
def create_channel(target, credentials=None, scopes=None, ssl_credentials=None, credentials_file=None, **kwargs):
210210
"""Create an AsyncIO secure channel with credentials.
211211
212212
Args:
@@ -219,13 +219,23 @@ def create_channel(target, credentials=None, scopes=None, ssl_credentials=None,
219219
are passed to :func:`google.auth.default`.
220220
ssl_credentials (grpc.ChannelCredentials): Optional SSL channel
221221
credentials. This can be used to specify different certificates.
222+
credentials_file (str): A file with credentials that can be loaded with
223+
:func:`google.auth.load_credentials_from_file`. This argument is
224+
mutually exclusive with credentials.
222225
kwargs: Additional key-word args passed to :func:`aio.secure_channel`.
223226
224227
Returns:
225228
aio.Channel: The created channel.
229+
230+
Raises:
231+
google.api_core.DuplicateCredentialArgs: If both a credentials object and credentials_file are passed.
226232
"""
233+
227234
composite_credentials = grpc_helpers._create_composite_credentials(
228-
credentials, scopes, ssl_credentials
235+
credentials=credentials,
236+
credentials_file=credentials_file,
237+
scopes=scopes,
238+
ssl_credentials=ssl_credentials
229239
)
230240

231241
return aio.secure_channel(target, composite_credentials, **kwargs)

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
dependencies = [
3232
"googleapis-common-protos >= 1.6.0, < 2.0dev",
3333
"protobuf >= 3.12.0",
34-
"google-auth >= 1.14.0, < 2.0dev",
34+
"google-auth >= 1.18.0, < 2.0dev",
3535
"requests >= 2.18.0, < 3.0.0dev",
3636
"setuptools >= 34.0.0",
3737
"six >= 1.10.0",

tests/asyncio/test_grpc_helpers_async.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,19 @@ def test_create_channel_implicit_with_scopes(
317317
grpc_secure_channel.assert_called_once_with(target, composite_creds)
318318

319319

320+
def test_create_channel_explicit_with_duplicate_credentials():
321+
target = "example:443"
322+
323+
with pytest.raises(exceptions.DuplicateCredentialArgs) as excinfo:
324+
grpc_helpers_async.create_channel(
325+
target,
326+
credentials_file="credentials.json",
327+
credentials=mock.sentinel.credentials
328+
)
329+
330+
assert "mutually exclusive" in str(excinfo.value)
331+
332+
320333
@mock.patch("grpc.composite_channel_credentials")
321334
@mock.patch("google.auth.credentials.with_scopes_if_required")
322335
@mock.patch("grpc.experimental.aio.secure_channel")
@@ -350,6 +363,49 @@ def test_create_channel_explicit_scoped(grpc_secure_channel, composite_creds_cal
350363
grpc_secure_channel.assert_called_once_with(target, composite_creds)
351364

352365

366+
@mock.patch("grpc.composite_channel_credentials")
367+
@mock.patch("grpc.experimental.aio.secure_channel")
368+
@mock.patch(
369+
"google.auth.load_credentials_from_file",
370+
return_value=(mock.sentinel.credentials, mock.sentinel.project)
371+
)
372+
def test_create_channnel_with_credentials_file(load_credentials_from_file, grpc_secure_channel, composite_creds_call):
373+
target = "example.com:443"
374+
375+
credentials_file = "/path/to/credentials/file.json"
376+
composite_creds = composite_creds_call.return_value
377+
378+
channel = grpc_helpers_async.create_channel(
379+
target, credentials_file=credentials_file
380+
)
381+
382+
google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, scopes=None)
383+
assert channel is grpc_secure_channel.return_value
384+
grpc_secure_channel.assert_called_once_with(target, composite_creds)
385+
386+
387+
@mock.patch("grpc.composite_channel_credentials")
388+
@mock.patch("grpc.experimental.aio.secure_channel")
389+
@mock.patch(
390+
"google.auth.load_credentials_from_file",
391+
return_value=(mock.sentinel.credentials, mock.sentinel.project)
392+
)
393+
def test_create_channel_with_credentials_file_and_scopes(load_credentials_from_file, grpc_secure_channel, composite_creds_call):
394+
target = "example.com:443"
395+
scopes = ["1", "2"]
396+
397+
credentials_file = "/path/to/credentials/file.json"
398+
composite_creds = composite_creds_call.return_value
399+
400+
channel = grpc_helpers_async.create_channel(
401+
target, credentials_file=credentials_file, scopes=scopes
402+
)
403+
404+
google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, scopes=scopes)
405+
assert channel is grpc_secure_channel.return_value
406+
grpc_secure_channel.assert_called_once_with(target, composite_creds)
407+
408+
353409
@pytest.mark.skipif(grpc_helpers_async.HAS_GRPC_GCP, reason="grpc_gcp module not available")
354410
@mock.patch("grpc.experimental.aio.secure_channel")
355411
def test_create_channel_without_grpc_gcp(grpc_secure_channel):

tests/unit/test_grpc_helpers.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,17 @@ def test_create_channel_implicit_with_scopes(
285285
grpc_secure_channel.assert_called_once_with(target, composite_creds)
286286

287287

288+
def test_create_channel_explicit_with_duplicate_credentials():
289+
target = "example.com:443"
290+
291+
with pytest.raises(exceptions.DuplicateCredentialArgs):
292+
grpc_helpers.create_channel(
293+
target,
294+
credentials_file="credentials.json",
295+
credentials=mock.sentinel.credentials
296+
)
297+
298+
288299
@mock.patch("grpc.composite_channel_credentials")
289300
@mock.patch("google.auth.credentials.with_scopes_if_required")
290301
@mock.patch("grpc.secure_channel")
@@ -324,6 +335,56 @@ def test_create_channel_explicit_scoped(grpc_secure_channel, composite_creds_cal
324335
grpc_secure_channel.assert_called_once_with(target, composite_creds)
325336

326337

338+
@mock.patch("grpc.composite_channel_credentials")
339+
@mock.patch("grpc.secure_channel")
340+
@mock.patch(
341+
"google.auth.load_credentials_from_file",
342+
return_value=(mock.sentinel.credentials, mock.sentinel.project)
343+
)
344+
def test_create_channel_with_credentials_file(load_credentials_from_file, grpc_secure_channel, composite_creds_call):
345+
target = "example.com:443"
346+
347+
credentials_file = "/path/to/credentials/file.json"
348+
composite_creds = composite_creds_call.return_value
349+
350+
channel = grpc_helpers.create_channel(
351+
target, credentials_file=credentials_file
352+
)
353+
354+
google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, scopes=None)
355+
356+
assert channel is grpc_secure_channel.return_value
357+
if grpc_helpers.HAS_GRPC_GCP:
358+
grpc_secure_channel.assert_called_once_with(target, composite_creds, None)
359+
else:
360+
grpc_secure_channel.assert_called_once_with(target, composite_creds)
361+
362+
363+
@mock.patch("grpc.composite_channel_credentials")
364+
@mock.patch("grpc.secure_channel")
365+
@mock.patch(
366+
"google.auth.load_credentials_from_file",
367+
return_value=(mock.sentinel.credentials, mock.sentinel.project)
368+
)
369+
def test_create_channel_with_credentials_file_and_scopes(load_credentials_from_file, grpc_secure_channel, composite_creds_call):
370+
target = "example.com:443"
371+
scopes = ["1", "2"]
372+
373+
credentials_file = "/path/to/credentials/file.json"
374+
composite_creds = composite_creds_call.return_value
375+
376+
channel = grpc_helpers.create_channel(
377+
target, credentials_file=credentials_file, scopes=scopes
378+
)
379+
380+
google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, scopes=scopes)
381+
assert channel is grpc_secure_channel.return_value
382+
if grpc_helpers.HAS_GRPC_GCP:
383+
grpc_secure_channel.assert_called_once_with(target, composite_creds, None)
384+
else:
385+
grpc_secure_channel.assert_called_once_with(target, composite_creds)
386+
387+
327388
@pytest.mark.skipif(
328389
not grpc_helpers.HAS_GRPC_GCP, reason="grpc_gcp module not available"
329390
)

0 commit comments

Comments
 (0)