Skip to content

Commit 981eadf

Browse files
feat: add mtls feature (#917)
1 parent c482712 commit 981eadf

File tree

11 files changed

+263
-19
lines changed

11 files changed

+263
-19
lines changed

googleapiclient/discovery.py

Lines changed: 89 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@
4747
import httplib2
4848
import uritemplate
4949
import google.api_core.client_options
50+
from google.auth.transport import mtls
51+
from google.auth.exceptions import MutualTLSChannelError
52+
53+
try:
54+
import google_auth_httplib2
55+
except ImportError: # pragma: NO COVER
56+
google_auth_httplib2 = None
5057

5158
# Local imports
5259
from googleapiclient import _auth
@@ -132,7 +139,7 @@ def fix_method_name(name):
132139
133140
Returns:
134141
The name with '_' appended if the name is a reserved word and '$' and '-'
135-
replaced with '_'.
142+
replaced with '_'.
136143
"""
137144
name = name.replace("$", "_").replace("-", "_")
138145
if keyword.iskeyword(name) or name in RESERVED_WORDS:
@@ -178,6 +185,8 @@ def build(
178185
cache_discovery=True,
179186
cache=None,
180187
client_options=None,
188+
adc_cert_path=None,
189+
adc_key_path=None,
181190
):
182191
"""Construct a Resource for interacting with an API.
183192
@@ -206,9 +215,21 @@ def build(
206215
cache object for the discovery documents.
207216
client_options: Dictionary or google.api_core.client_options, Client options to set user
208217
options on the client. API endpoint should be set through client_options.
218+
client_cert_source is not supported, client cert should be provided using
219+
client_encrypted_cert_source instead.
220+
adc_cert_path: str, client certificate file path to save the application
221+
default client certificate for mTLS. This field is required if you want to
222+
use the default client certificate.
223+
adc_key_path: str, client encrypted private key file path to save the
224+
application default client encrypted private key for mTLS. This field is
225+
required if you want to use the default client certificate.
209226
210227
Returns:
211228
A Resource object with methods for interacting with the service.
229+
230+
Raises:
231+
google.auth.exceptions.MutualTLSChannelError: if there are any problems
232+
setting up mutual TLS channel.
212233
"""
213234
params = {"api": serviceName, "apiVersion": version}
214235

@@ -232,7 +253,9 @@ def build(
232253
model=model,
233254
requestBuilder=requestBuilder,
234255
credentials=credentials,
235-
client_options=client_options
256+
client_options=client_options,
257+
adc_cert_path=adc_cert_path,
258+
adc_key_path=adc_key_path,
236259
)
237260
except HttpError as e:
238261
if e.resp.status == http_client.NOT_FOUND:
@@ -309,7 +332,9 @@ def build_from_document(
309332
model=None,
310333
requestBuilder=HttpRequest,
311334
credentials=None,
312-
client_options=None
335+
client_options=None,
336+
adc_cert_path=None,
337+
adc_key_path=None,
313338
):
314339
"""Create a Resource for interacting with an API.
315340
@@ -336,9 +361,21 @@ def build_from_document(
336361
authentication.
337362
client_options: Dictionary or google.api_core.client_options, Client options to set user
338363
options on the client. API endpoint should be set through client_options.
364+
client_cert_source is not supported, client cert should be provided using
365+
client_encrypted_cert_source instead.
366+
adc_cert_path: str, client certificate file path to save the application
367+
default client certificate for mTLS. This field is required if you want to
368+
use the default client certificate.
369+
adc_key_path: str, client encrypted private key file path to save the
370+
application default client encrypted private key for mTLS. This field is
371+
required if you want to use the default client certificate.
339372
340373
Returns:
341374
A Resource object with methods for interacting with the service.
375+
376+
Raises:
377+
google.auth.exceptions.MutualTLSChannelError: if there are any problems
378+
setting up mutual TLS channel.
342379
"""
343380

344381
if http is not None and credentials is not None:
@@ -349,7 +386,7 @@ def build_from_document(
349386
elif isinstance(service, six.binary_type):
350387
service = json.loads(service.decode("utf-8"))
351388

352-
if "rootUrl" not in service and (isinstance(http, (HttpMock, HttpMockSequence))):
389+
if "rootUrl" not in service and isinstance(http, (HttpMock, HttpMockSequence)):
353390
logger.error(
354391
"You are using HttpMock or HttpMockSequence without"
355392
+ "having the service discovery doc in cache. Try calling "
@@ -359,12 +396,10 @@ def build_from_document(
359396
raise InvalidJsonError()
360397

361398
# If an API Endpoint is provided on client options, use that as the base URL
362-
base = urljoin(service['rootUrl'], service["servicePath"])
399+
base = urljoin(service["rootUrl"], service["servicePath"])
363400
if client_options:
364401
if type(client_options) == dict:
365-
client_options = google.api_core.client_options.from_dict(
366-
client_options
367-
)
402+
client_options = google.api_core.client_options.from_dict(client_options)
368403
if client_options.api_endpoint:
369404
base = client_options.api_endpoint
370405

@@ -400,6 +435,52 @@ def build_from_document(
400435
else:
401436
http = build_http()
402437

438+
# Obtain client cert and create mTLS http channel if cert exists.
439+
client_cert_to_use = None
440+
if client_options and client_options.client_cert_source:
441+
raise MutualTLSChannelError(
442+
"ClientOptions.client_cert_source is not supported, please use ClientOptions.client_encrypted_cert_source."
443+
)
444+
if client_options and client_options.client_encrypted_cert_source:
445+
client_cert_to_use = client_options.client_encrypted_cert_source
446+
elif adc_cert_path and adc_key_path and mtls.has_default_client_cert_source():
447+
client_cert_to_use = mtls.default_client_encrypted_cert_source(
448+
adc_cert_path, adc_key_path
449+
)
450+
if client_cert_to_use:
451+
cert_path, key_path, passphrase = client_cert_to_use()
452+
453+
# The http object we built could be google_auth_httplib2.AuthorizedHttp
454+
# or httplib2.Http. In the first case we need to extract the wrapped
455+
# httplib2.Http object from google_auth_httplib2.AuthorizedHttp.
456+
http_channel = (
457+
http.http
458+
if google_auth_httplib2
459+
and isinstance(http, google_auth_httplib2.AuthorizedHttp)
460+
else http
461+
)
462+
http_channel.add_certificate(key_path, cert_path, "", passphrase)
463+
464+
# If user doesn't provide api endpoint via client options, decide which
465+
# api endpoint to use.
466+
if "mtlsRootUrl" in service and (
467+
not client_options or not client_options.api_endpoint
468+
):
469+
mtls_endpoint = urljoin(service["mtlsRootUrl"], service["servicePath"])
470+
use_mtls_env = os.getenv("GOOGLE_API_USE_MTLS", "Never")
471+
472+
if not use_mtls_env in ("Never", "Auto", "Always"):
473+
raise MutualTLSChannelError(
474+
"Unsupported GOOGLE_API_USE_MTLS value. Accepted values: Never, Auto, Always"
475+
)
476+
477+
# Switch to mTLS endpoint, if environment variable is "Always", or
478+
# environment varibable is "Auto" and client cert exists.
479+
if use_mtls_env == "Always" or (
480+
use_mtls_env == "Auto" and client_cert_to_use
481+
):
482+
base = mtls_endpoint
483+
403484
if model is None:
404485
features = service.get("features", [])
405486
model = JsonModel("dataWrapper" in features)

noxfile.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"google-auth",
2020
"google-auth-httplib2",
2121
"mox",
22+
"parameterized",
2223
"pyopenssl",
2324
"pytest",
2425
"pytest-cov",
@@ -54,6 +55,10 @@ def lint(session):
5455
],
5556
)
5657
def unit(session, oauth2client):
58+
session.install(
59+
"-e",
60+
"git+https://github.com/googleapis/python-api-core.git@master#egg=google-api-core",
61+
)
5762
session.install(*test_dependencies)
5863
session.install(oauth2client)
5964
if session.python < "3.0":
@@ -75,4 +80,4 @@ def unit(session, oauth2client):
7580
"--cov-fail-under=85",
7681
"tests",
7782
*session.posargs,
78-
)
83+
)

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
# currently upgrade their httplib2 version.
4040
# Please see https://github.com/googleapis/google-api-python-client/pull/841
4141
"httplib2>=0.9.2,<1dev",
42-
"google-auth>=1.4.1",
42+
"google-auth>=1.16.0",
4343
"google-auth-httplib2>=0.0.3",
4444
"google-api-core>=1.13.0,<2dev",
4545
"six>=1.6.1,<2dev",

tests/data/bigquery.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"baseUrl": "https://www.googleapis.com/bigquery/v2/",
2020
"basePath": "/bigquery/v2/",
2121
"rootUrl": "https://www.googleapis.com/",
22+
"mtlsRootUrl": "https://www.mtls.googleapis.com/",
2223
"servicePath": "bigquery/v2/",
2324
"batchPath": "batch",
2425
"parameters": {

tests/data/drive.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"baseUrl": "https://www.googleapis.com/drive/v3/",
2020
"basePath": "/drive/v3/",
2121
"rootUrl": "https://www.googleapis.com/",
22+
"mtlsRootUrl": "https://www.mtls.googleapis.com/",
2223
"servicePath": "drive/v3/",
2324
"batchPath": "batch",
2425
"parameters": {

tests/data/latitude.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"protocol": "rest",
1515
"basePath": "/latitude/v1/",
1616
"rootUrl": "https://www.googleapis.com/",
17+
"mtlsRootUrl": "https://www.mtls.googleapis.com/",
1718
"servicePath": "latitude/v1/",
1819
"auth": {
1920
"oauth2": {

tests/data/logging.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2086,5 +2086,6 @@
20862086
"ownerName": "Google",
20872087
"version": "v2",
20882088
"rootUrl": "https://logging.googleapis.com/",
2089+
"mtlsRootUrl": "https://logging.mtls.googleapis.com/",
20892090
"kind": "discovery#restDescription"
20902091
}

tests/data/plus.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"protocol": "rest",
1717
"basePath": "/plus/v1/",
1818
"rootUrl": "https://www.googleapis.com/",
19+
"mtlsRootUrl": "https://www.mtls.googleapis.com/",
1920
"servicePath": "plus/v1/",
2021
"parameters": {
2122
"alt": {

tests/data/tasks.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"protocol": "rest",
1717
"basePath": "/tasks/v1/",
1818
"rootUrl": "https://www.googleapis.com/",
19+
"mtlsRootUrl": "https://www.mtls.googleapis.com/",
1920
"servicePath": "tasks/v1/",
2021
"parameters": {
2122
"alt": {

tests/data/zoo.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"basePath": "/zoo/",
77
"batchPath": "batchZoo",
88
"rootUrl": "https://www.googleapis.com/",
9+
"mtlsRootUrl": "https://www.mtls.googleapis.com/",
910
"servicePath": "zoo/v1/",
1011
"rpcPath": "/rpc",
1112
"parameters": {

0 commit comments

Comments
 (0)