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