1# Copyright 2016 Google LLC
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
17from __future__ import absolute_import
18
19import logging
20
21from google.auth import exceptions
22from google.auth.transport import _mtls_helper
23from google.auth.transport import mtls
24from google.oauth2 import service_account
25
26try:
27 import grpc # type: ignore
28except ImportError as caught_exc: # pragma: NO COVER
29 raise ImportError(
30 "gRPC is not installed from please install the grpcio package to use the gRPC transport."
31 ) from caught_exc
32
33_LOGGER = logging.getLogger(__name__)
34
35
36class AuthMetadataPlugin(grpc.AuthMetadataPlugin):
37 """A `gRPC AuthMetadataPlugin`_ that inserts the credentials into each
38 request.
39
40 .. _gRPC AuthMetadataPlugin:
41 http://www.grpc.io/grpc/python/grpc.html#grpc.AuthMetadataPlugin
42
43 Args:
44 credentials (google.auth.credentials.Credentials): The credentials to
45 add to requests.
46 request (google.auth.transport.Request): A HTTP transport request
47 object used to refresh credentials as needed.
48 default_host (Optional[str]): A host like "pubsub.googleapis.com".
49 This is used when a self-signed JWT is created from service
50 account credentials.
51 """
52
53 def __init__(self, credentials, request, default_host=None):
54 # pylint: disable=no-value-for-parameter
55 # pylint doesn't realize that the super method takes no arguments
56 # because this class is the same name as the superclass.
57 super(AuthMetadataPlugin, self).__init__()
58 self._credentials = credentials
59 self._request = request
60 self._default_host = default_host
61
62 def _get_authorization_headers(self, context):
63 """Gets the authorization headers for a request.
64
65 Returns:
66 Sequence[Tuple[str, str]]: A list of request headers (key, value)
67 to add to the request.
68 """
69 headers = {}
70
71 # https://google.aip.dev/auth/4111
72 # Attempt to use self-signed JWTs when a service account is used.
73 # A default host must be explicitly provided since it cannot always
74 # be determined from the context.service_url.
75 if isinstance(self._credentials, service_account.Credentials):
76 self._credentials._create_self_signed_jwt(
77 "https://{}/".format(self._default_host) if self._default_host else None
78 )
79
80 self._credentials.before_request(
81 self._request, context.method_name, context.service_url, headers
82 )
83
84 return list(headers.items())
85
86 def __call__(self, context, callback):
87 """Passes authorization metadata into the given callback.
88
89 Args:
90 context (grpc.AuthMetadataContext): The RPC context.
91 callback (grpc.AuthMetadataPluginCallback): The callback that will
92 be invoked to pass in the authorization metadata.
93 """
94 callback(self._get_authorization_headers(context), None)
95
96
97def secure_authorized_channel(
98 credentials,
99 request,
100 target,
101 ssl_credentials=None,
102 client_cert_callback=None,
103 **kwargs
104):
105 """Creates a secure authorized gRPC channel.
106
107 This creates a channel with SSL and :class:`AuthMetadataPlugin`. This
108 channel can be used to create a stub that can make authorized requests.
109 Users can configure client certificate or rely on device certificates to
110 establish a mutual TLS channel, if the `GOOGLE_API_USE_CLIENT_CERTIFICATE`
111 variable is explicitly set to `true`.
112
113 Example::
114
115 import google.auth
116 import google.auth.transport.grpc
117 import google.auth.transport.requests
118 from google.cloud.speech.v1 import cloud_speech_pb2
119
120 # Get credentials.
121 credentials, _ = google.auth.default()
122
123 # Get an HTTP request function to refresh credentials.
124 request = google.auth.transport.requests.Request()
125
126 # Create a channel.
127 channel = google.auth.transport.grpc.secure_authorized_channel(
128 credentials, regular_endpoint, request,
129 ssl_credentials=grpc.ssl_channel_credentials())
130
131 # Use the channel to create a stub.
132 cloud_speech.create_Speech_stub(channel)
133
134 Usage:
135
136 There are actually a couple of options to create a channel, depending on if
137 you want to create a regular or mutual TLS channel.
138
139 First let's list the endpoints (regular vs mutual TLS) to choose from::
140
141 regular_endpoint = 'speech.googleapis.com:443'
142 mtls_endpoint = 'speech.mtls.googleapis.com:443'
143
144 Option 1: create a regular (non-mutual) TLS channel by explicitly setting
145 the ssl_credentials::
146
147 regular_ssl_credentials = grpc.ssl_channel_credentials()
148
149 channel = google.auth.transport.grpc.secure_authorized_channel(
150 credentials, request, regular_endpoint,
151 ssl_credentials=regular_ssl_credentials)
152
153 Option 2: create a mutual TLS channel by calling a callback which returns
154 the client side certificate and the key (Note that
155 `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be explicitly
156 set to `true`)::
157
158 def my_client_cert_callback():
159 code_to_load_client_cert_and_key()
160 if loaded:
161 return (pem_cert_bytes, pem_key_bytes)
162 raise MyClientCertFailureException()
163
164 try:
165 channel = google.auth.transport.grpc.secure_authorized_channel(
166 credentials, request, mtls_endpoint,
167 client_cert_callback=my_client_cert_callback)
168 except MyClientCertFailureException:
169 # handle the exception
170
171 Option 3: use application default SSL credentials. It searches and uses
172 the command in a context aware metadata file, which is available on devices
173 with endpoint verification support (Note that
174 `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be explicitly
175 set to `true`).
176 See https://cloud.google.com/endpoint-verification/docs/overview::
177
178 try:
179 default_ssl_credentials = SslCredentials()
180 except:
181 # Exception can be raised if the context aware metadata is malformed.
182 # See :class:`SslCredentials` for the possible exceptions.
183
184 # Choose the endpoint based on the SSL credentials type.
185 if default_ssl_credentials.is_mtls:
186 endpoint_to_use = mtls_endpoint
187 else:
188 endpoint_to_use = regular_endpoint
189 channel = google.auth.transport.grpc.secure_authorized_channel(
190 credentials, request, endpoint_to_use,
191 ssl_credentials=default_ssl_credentials)
192
193 Option 4: not setting ssl_credentials and client_cert_callback. For devices
194 without endpoint verification support or `GOOGLE_API_USE_CLIENT_CERTIFICATE`
195 environment variable is not `true`, a regular TLS channel is created;
196 otherwise, a mutual TLS channel is created, however, the call should be
197 wrapped in a try/except block in case of malformed context aware metadata.
198
199 The following code uses regular_endpoint, it works the same no matter the
200 created channle is regular or mutual TLS. Regular endpoint ignores client
201 certificate and key::
202
203 channel = google.auth.transport.grpc.secure_authorized_channel(
204 credentials, request, regular_endpoint)
205
206 The following code uses mtls_endpoint, if the created channle is regular,
207 and API mtls_endpoint is confgured to require client SSL credentials, API
208 calls using this channel will be rejected::
209
210 channel = google.auth.transport.grpc.secure_authorized_channel(
211 credentials, request, mtls_endpoint)
212
213 Args:
214 credentials (google.auth.credentials.Credentials): The credentials to
215 add to requests.
216 request (google.auth.transport.Request): A HTTP transport request
217 object used to refresh credentials as needed. Even though gRPC
218 is a separate transport, there's no way to refresh the credentials
219 without using a standard http transport.
220 target (str): The host and port of the service.
221 ssl_credentials (grpc.ChannelCredentials): Optional SSL channel
222 credentials. This can be used to specify different certificates.
223 This argument is mutually exclusive with client_cert_callback;
224 providing both will raise an exception.
225 If ssl_credentials and client_cert_callback are None, application
226 default SSL credentials are used if `GOOGLE_API_USE_CLIENT_CERTIFICATE`
227 environment variable is explicitly set to `true`, otherwise one way TLS
228 SSL credentials are used.
229 client_cert_callback (Callable[[], (bytes, bytes)]): Optional
230 callback function to obtain client certicate and key for mutual TLS
231 connection. This argument is mutually exclusive with
232 ssl_credentials; providing both will raise an exception.
233 This argument does nothing unless `GOOGLE_API_USE_CLIENT_CERTIFICATE`
234 environment variable is explicitly set to `true`.
235 kwargs: Additional arguments to pass to :func:`grpc.secure_channel`.
236
237 Returns:
238 grpc.Channel: The created gRPC channel.
239
240 Raises:
241 google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
242 creation failed for any reason.
243 """
244 # Create the metadata plugin for inserting the authorization header.
245 metadata_plugin = AuthMetadataPlugin(credentials, request)
246
247 # Create a set of grpc.CallCredentials using the metadata plugin.
248 google_auth_credentials = grpc.metadata_call_credentials(metadata_plugin)
249
250 if ssl_credentials and client_cert_callback:
251 raise exceptions.MalformedError(
252 "Received both ssl_credentials and client_cert_callback; "
253 "these are mutually exclusive."
254 )
255
256 # If SSL credentials are not explicitly set, try client_cert_callback and ADC.
257 if not ssl_credentials:
258 use_client_cert = _mtls_helper.check_use_client_cert()
259 if use_client_cert and client_cert_callback:
260 # Use the callback if provided.
261 cert, key = client_cert_callback()
262 ssl_credentials = grpc.ssl_channel_credentials(
263 certificate_chain=cert, private_key=key
264 )
265 elif use_client_cert:
266 # Use application default SSL credentials.
267 adc_ssl_credentils = SslCredentials()
268 ssl_credentials = adc_ssl_credentils.ssl_credentials
269 else:
270 ssl_credentials = grpc.ssl_channel_credentials()
271
272 # Combine the ssl credentials and the authorization credentials.
273 composite_credentials = grpc.composite_channel_credentials(
274 ssl_credentials, google_auth_credentials
275 )
276
277 return grpc.secure_channel(target, composite_credentials, **kwargs)
278
279
280class SslCredentials:
281 """Class for application default SSL credentials.
282
283 Mutual TLS (mTLS) is enabled if either:
284
285 1. The `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is explicitly
286 set to `"true"`.
287 2. The `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is unset or empty,
288 but a valid workload certificate configuration is found (e.g., via the
289 `GOOGLE_API_CERTIFICATE_CONFIG` environment variable or the default gcloud config path).
290
291 See https://google.aip.dev/auth/4114 for client certificate discovery details.
292
293 If client certificate usage is enabled, then for devices with endpoint
294 verification support, a device certificate will be automatically loaded and
295 mutual TLS will be established.
296 See https://cloud.google.com/endpoint-verification/docs/overview.
297 """
298
299 def __init__(self):
300 use_client_cert = _mtls_helper.check_use_client_cert()
301 if not use_client_cert:
302 self._is_mtls = False
303 else:
304 self._is_mtls = mtls.has_default_client_cert_source()
305
306 @property
307 def ssl_credentials(self):
308 """Get the created SSL channel credentials.
309
310 For devices with endpoint verification support, if the device certificate
311 loading has any problems, corresponding exceptions will be raised. For
312 a device without endpoint verification support, no exceptions will be
313 raised.
314
315 Returns:
316 grpc.ChannelCredentials: The created grpc channel credentials.
317
318 Raises:
319 google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
320 creation failed for any reason.
321 """
322 if self._is_mtls:
323 try:
324 has_cert, cert, key, _ = _mtls_helper.get_client_ssl_credentials()
325 if has_cert:
326 self._ssl_credentials = grpc.ssl_channel_credentials(
327 certificate_chain=cert, private_key=key
328 )
329 else:
330 self._ssl_credentials = grpc.ssl_channel_credentials()
331 self._is_mtls = False
332 except (exceptions.ClientCertError, OSError) as caught_exc:
333 new_exc = exceptions.MutualTLSChannelError(caught_exc)
334 raise new_exc from caught_exc
335 else:
336 self._ssl_credentials = grpc.ssl_channel_credentials()
337
338 return self._ssl_credentials
339
340 @property
341 def is_mtls(self):
342 """Indicates if the created SSL channel credentials is mutual TLS."""
343 return self._is_mtls