Skip to content

Commit b9897dc

Browse files
author
Jon Wayne Parrott
authored
Add grpc transport (#67)
1 parent 9779181 commit b9897dc

File tree

10 files changed

+308
-1
lines changed

10 files changed

+308
-1
lines changed

docs/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,3 +373,4 @@
373373
# Autodoc config
374374
autoclass_content = 'both'
375375
autodoc_member_order = 'bysource'
376+
autodoc_mock_imports = ['grpc']
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
google.auth.transport.grpc module
2+
=================================
3+
4+
.. automodule:: google.auth.transport.grpc
5+
:members:
6+
:inherited-members:
7+
:show-inheritance:

docs/reference/google.auth.transport.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Submodules
1111

1212
.. toctree::
1313

14+
google.auth.transport.grpc
1415
google.auth.transport.requests
1516
google.auth.transport.urllib3
1617

docs/requirements-docs.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
sphinx-docstring-typing
22
urllib3
3+
requests

google/auth/transport/grpc.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Copyright 2016 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Authorization support for gRPC."""
16+
17+
from __future__ import absolute_import
18+
19+
import grpc
20+
21+
22+
class AuthMetadataPlugin(grpc.AuthMetadataPlugin):
23+
"""A `gRPC AuthMetadataPlugin`_ that inserts the credentials into each
24+
request.
25+
26+
.. _gRPC AuthMetadataPlugin:
27+
http://www.grpc.io/grpc/python/grpc.html#grpc.AuthMetadataPlugin
28+
29+
Args:
30+
credentials (google.auth.credentials.Credentials): The credentials to
31+
add to requests.
32+
request (google.auth.transport.Request): A HTTP transport request
33+
object used to refresh credentials as needed.
34+
"""
35+
def __init__(self, credentials, request):
36+
self._credentials = credentials
37+
self._request = request
38+
39+
def _get_authorization_headers(self):
40+
"""Gets the authorization headers for a request.
41+
42+
Returns:
43+
Sequence[Tuple[str, str]]: A list of request headers (key, value)
44+
to add to the request.
45+
"""
46+
if self._credentials.expired or not self._credentials.valid:
47+
self._credentials.refresh(self._request)
48+
49+
return [
50+
('authorization', 'Bearer {}'.format(self._credentials.token))
51+
]
52+
53+
def __call__(self, context, callback):
54+
"""Passes authorization metadata into the given callback.
55+
56+
Args:
57+
context (grpc.AuthMetadataContext): The RPC context.
58+
callback (grpc.AuthMetadataPluginCallback): The callback that will
59+
be invoked to pass in the authorization metadata.
60+
"""
61+
callback(self._get_authorization_headers(), None)
62+
63+
64+
def secure_authorized_channel(
65+
credentials, target, request, ssl_credentials=None):
66+
"""Creates a secure authorized gRPC channel.
67+
68+
This creates a channel with SSL and :class:`AuthMetadataPlugin`. This
69+
channel can be used to create a stub that can make authorized requests.
70+
71+
Example::
72+
73+
import google.auth
74+
import google.auth.transport.grpc
75+
import google.auth.transport.requests
76+
from google.cloud.speech.v1 import cloud_speech_pb2
77+
78+
# Get credentials.
79+
credentials, _ = google.auth.default()
80+
81+
# Get an HTTP request function to refresh credentials.
82+
request = google.auth.transport.requests.Request()
83+
84+
# Create a channel.
85+
channel = google.auth.transport.grpc.secure_authorized_channel(
86+
credentials, 'speech.googleapis.com:443', request)
87+
88+
# Use the channel to create a stub.
89+
cloud_speech.create_Speech_stub(channel)
90+
91+
Args:
92+
credentials (google.auth.credentials.Credentials): The credentials to
93+
add to requests.
94+
target (str): The host and port of the service.
95+
request (google.auth.transport.Request): A HTTP transport request
96+
object used to refresh credentials as needed. Even though gRPC
97+
is a separate transport, there's no way to refresh the credentials
98+
without using a standard http transport.
99+
ssl_credentials (grpc.ChannelCredentials): Optional SSL channel
100+
credentials. This can be used to specify different certificates.
101+
102+
Returns:
103+
grpc.Channel: The created gRPC channel.
104+
"""
105+
# Create the metadata plugin for inserting the authorization header.
106+
metadata_plugin = AuthMetadataPlugin(credentials, request)
107+
108+
# Create a set of grpc.CallCredentials using the metadata plugin.
109+
google_auth_credentials = grpc.metadata_call_credentials(metadata_plugin)
110+
111+
if ssl_credentials is None:
112+
ssl_credentials = grpc.ssl_channel_credentials()
113+
114+
# Combine the ssl credentials and the authorization credentials.
115+
composite_credentials = grpc.composite_channel_credentials(
116+
ssl_credentials, google_auth_credentials)
117+
118+
return grpc.secure_channel(target, composite_credentials)

scripts/run_pylint.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,12 @@
7373
'no-self-use',
7474
'redefined-outer-name',
7575
'unused-argument',
76+
'no-name-in-module',
7677
])
7778
_TEST_RC_REPLACEMENTS = copy.deepcopy(_PRODUCTION_RC_REPLACEMENTS)
7879
_TEST_RC_REPLACEMENTS.setdefault('BASIC', {})
7980
_TEST_RC_REPLACEMENTS['BASIC'].update({
80-
'good-names': ['i', 'j', 'k', 'ex', 'Run', '_', 'fh'],
81+
'good-names': ['i', 'j', 'k', 'ex', 'Run', '_', 'fh', 'pytestmark'],
8182
'method-rgx': '[a-z_][a-z0-9_]{2,80}$',
8283
'function-rgx': '[a-z_][a-z0-9_]{2,80}$',
8384
})

system_tests/nox.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,3 +248,9 @@ def session_app_engine(session):
248248
session.env['TEST_APP_URL'] = application_url
249249
session.chdir(HERE)
250250
session.run('pytest', 'test_app_engine.py')
251+
252+
253+
def session_grpc(session):
254+
session.virtualenv = False
255+
session.env[EXPLICIT_CREDENTIALS_ENV] = SERVICE_ACCOUNT_FILE
256+
session.run('pytest', 'test_grpc.py')

system_tests/test_grpc.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Copyright 2016 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import google.auth
16+
import google.auth.credentials
17+
import google.auth.transport.grpc
18+
from google.cloud.gapic.pubsub.v1 import publisher_api
19+
20+
21+
def test_grpc_request(http_request):
22+
credentials, project_id = google.auth.default()
23+
credentials = google.auth.credentials.with_scopes_if_required(
24+
credentials, ['https://www.googleapis.com/auth/pubsub'])
25+
26+
target = '{}:{}'.format(
27+
publisher_api.PublisherApi.SERVICE_ADDRESS,
28+
publisher_api.PublisherApi.DEFAULT_SERVICE_PORT)
29+
30+
channel = google.auth.transport.grpc.secure_authorized_channel(
31+
credentials, target, http_request)
32+
33+
# Create a pub/sub client.
34+
client = publisher_api.PublisherApi(channel=channel)
35+
36+
# list the topics and drain the iterator to test that an authorized API
37+
# call works.
38+
list_topics_iter = client.list_topics(
39+
project='projects/{}'.format(project_id))
40+
list(list_topics_iter)

tests/transport/test_grpc.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# Copyright 2016 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import mock
16+
17+
import pytest
18+
19+
try:
20+
import google.auth.transport.grpc
21+
HAS_GRPC = True
22+
except ImportError: # pragma: NO COVER
23+
HAS_GRPC = False
24+
25+
26+
pytestmark = pytest.mark.skipif(not HAS_GRPC, reason='gRPC is unavailable.')
27+
28+
29+
class MockCredentials(object):
30+
def __init__(self, token='token'):
31+
self.token = token
32+
self.valid = True
33+
self.expired = False
34+
35+
def refresh(self, request):
36+
self.token += '1'
37+
38+
39+
class TestAuthMetadataPlugin(object):
40+
def test_call_no_refresh(self):
41+
credentials = MockCredentials()
42+
request = mock.Mock()
43+
44+
plugin = google.auth.transport.grpc.AuthMetadataPlugin(
45+
credentials, request)
46+
47+
context = mock.Mock()
48+
callback = mock.Mock()
49+
50+
plugin(context, callback)
51+
52+
assert callback.called_once_with(
53+
[('authorization', 'Bearer {}'.format(credentials.token))], None)
54+
55+
def test_call_refresh(self):
56+
credentials = MockCredentials()
57+
credentials.expired = True
58+
request = mock.Mock()
59+
60+
plugin = google.auth.transport.grpc.AuthMetadataPlugin(
61+
credentials, request)
62+
63+
context = mock.Mock()
64+
callback = mock.Mock()
65+
66+
plugin(context, callback)
67+
68+
assert credentials.token == 'token1'
69+
assert callback.called_once_with(
70+
[('authorization', 'Bearer {}'.format(credentials.token))], None)
71+
72+
73+
@mock.patch('grpc.composite_channel_credentials')
74+
@mock.patch('grpc.metadata_call_credentials')
75+
@mock.patch('grpc.ssl_channel_credentials')
76+
@mock.patch('grpc.secure_channel')
77+
def test_secure_authorized_channel(
78+
secure_channel, ssl_channel_credentials, metadata_call_credentials,
79+
composite_channel_credentials):
80+
credentials = mock.Mock()
81+
request = mock.Mock()
82+
target = 'example.com:80'
83+
84+
channel = google.auth.transport.grpc.secure_authorized_channel(
85+
credentials, target, request)
86+
87+
# Check the auth plugin construction.
88+
auth_plugin = metadata_call_credentials.call_args[0][0]
89+
assert isinstance(
90+
auth_plugin, google.auth.transport.grpc.AuthMetadataPlugin)
91+
assert auth_plugin._credentials == credentials
92+
assert auth_plugin._request == request
93+
94+
# Check the ssl channel call.
95+
assert ssl_channel_credentials.called
96+
97+
# Check the composite credentials call.
98+
composite_channel_credentials.assert_called_once_with(
99+
ssl_channel_credentials.return_value,
100+
metadata_call_credentials.return_value)
101+
102+
# Check the channel call.
103+
secure_channel.assert_called_once_with(
104+
target, composite_channel_credentials.return_value)
105+
assert channel == secure_channel.return_value
106+
107+
108+
@mock.patch('grpc.composite_channel_credentials')
109+
@mock.patch('grpc.metadata_call_credentials')
110+
@mock.patch('grpc.ssl_channel_credentials')
111+
@mock.patch('grpc.secure_channel')
112+
def test_secure_authorized_channel_explicit_ssl(
113+
secure_channel, ssl_channel_credentials, metadata_call_credentials,
114+
composite_channel_credentials):
115+
credentials = mock.Mock()
116+
request = mock.Mock()
117+
target = 'example.com:80'
118+
ssl_credentials = mock.Mock()
119+
120+
google.auth.transport.grpc.secure_authorized_channel(
121+
credentials, target, request, ssl_credentials=ssl_credentials)
122+
123+
# Check the ssl channel call.
124+
assert not ssl_channel_credentials.called
125+
126+
# Check the composite credentials call.
127+
composite_channel_credentials.assert_called_once_with(
128+
ssl_credentials,
129+
metadata_call_credentials.return_value)

tox.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ deps =
1111
urllib3
1212
certifi
1313
requests
14+
grpcio; platform_python_implementation != 'PyPy'
1415
commands =
1516
py.test --cov=google.auth --cov=google.oauth2 --cov=tests {posargs:tests}
1617

@@ -30,6 +31,7 @@ commands =
3031
deps =
3132
{[testenv]deps}
3233
nox-automation
34+
gapic-google-pubsub-v1==0.11.1
3335
passenv =
3436
SKIP_APP_ENGINE_SYSTEM_TEST
3537
CLOUD_SDK_ROOT
@@ -42,6 +44,7 @@ commands =
4244
deps =
4345
{[testenv]deps}
4446
nox-automation
47+
gapic-google-pubsub-v1==0.11.1
4548
passenv =
4649
SKIP_APP_ENGINE_SYSTEM_TEST
4750
CLOUD_SDK_ROOT

0 commit comments

Comments
 (0)