Skip to content

Commit

Permalink
feat: add mtls feature (#917)
Browse files Browse the repository at this point in the history
  • Loading branch information
arithmetic1728 committed Jun 2, 2020
1 parent c482712 commit 981eadf
Show file tree
Hide file tree
Showing 11 changed files with 263 additions and 19 deletions.
97 changes: 89 additions & 8 deletions googleapiclient/discovery.py
Expand Up @@ -47,6 +47,13 @@
import httplib2
import uritemplate
import google.api_core.client_options
from google.auth.transport import mtls
from google.auth.exceptions import MutualTLSChannelError

try:
import google_auth_httplib2
except ImportError: # pragma: NO COVER
google_auth_httplib2 = None

# Local imports
from googleapiclient import _auth
Expand Down Expand Up @@ -132,7 +139,7 @@ def fix_method_name(name):
Returns:
The name with '_' appended if the name is a reserved word and '$' and '-'
replaced with '_'.
replaced with '_'.
"""
name = name.replace("$", "_").replace("-", "_")
if keyword.iskeyword(name) or name in RESERVED_WORDS:
Expand Down Expand Up @@ -178,6 +185,8 @@ def build(
cache_discovery=True,
cache=None,
client_options=None,
adc_cert_path=None,
adc_key_path=None,
):
"""Construct a Resource for interacting with an API.
Expand Down Expand Up @@ -206,9 +215,21 @@ def build(
cache object for the discovery documents.
client_options: Dictionary or google.api_core.client_options, Client options to set user
options on the client. API endpoint should be set through client_options.
client_cert_source is not supported, client cert should be provided using
client_encrypted_cert_source instead.
adc_cert_path: str, client certificate file path to save the application
default client certificate for mTLS. This field is required if you want to
use the default client certificate.
adc_key_path: str, client encrypted private key file path to save the
application default client encrypted private key for mTLS. This field is
required if you want to use the default client certificate.
Returns:
A Resource object with methods for interacting with the service.
Raises:
google.auth.exceptions.MutualTLSChannelError: if there are any problems
setting up mutual TLS channel.
"""
params = {"api": serviceName, "apiVersion": version}

Expand All @@ -232,7 +253,9 @@ def build(
model=model,
requestBuilder=requestBuilder,
credentials=credentials,
client_options=client_options
client_options=client_options,
adc_cert_path=adc_cert_path,
adc_key_path=adc_key_path,
)
except HttpError as e:
if e.resp.status == http_client.NOT_FOUND:
Expand Down Expand Up @@ -309,7 +332,9 @@ def build_from_document(
model=None,
requestBuilder=HttpRequest,
credentials=None,
client_options=None
client_options=None,
adc_cert_path=None,
adc_key_path=None,
):
"""Create a Resource for interacting with an API.
Expand All @@ -336,9 +361,21 @@ def build_from_document(
authentication.
client_options: Dictionary or google.api_core.client_options, Client options to set user
options on the client. API endpoint should be set through client_options.
client_cert_source is not supported, client cert should be provided using
client_encrypted_cert_source instead.
adc_cert_path: str, client certificate file path to save the application
default client certificate for mTLS. This field is required if you want to
use the default client certificate.
adc_key_path: str, client encrypted private key file path to save the
application default client encrypted private key for mTLS. This field is
required if you want to use the default client certificate.
Returns:
A Resource object with methods for interacting with the service.
Raises:
google.auth.exceptions.MutualTLSChannelError: if there are any problems
setting up mutual TLS channel.
"""

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

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

# If an API Endpoint is provided on client options, use that as the base URL
base = urljoin(service['rootUrl'], service["servicePath"])
base = urljoin(service["rootUrl"], service["servicePath"])
if client_options:
if type(client_options) == dict:
client_options = google.api_core.client_options.from_dict(
client_options
)
client_options = google.api_core.client_options.from_dict(client_options)
if client_options.api_endpoint:
base = client_options.api_endpoint

Expand Down Expand Up @@ -400,6 +435,52 @@ def build_from_document(
else:
http = build_http()

# Obtain client cert and create mTLS http channel if cert exists.
client_cert_to_use = None
if client_options and client_options.client_cert_source:
raise MutualTLSChannelError(
"ClientOptions.client_cert_source is not supported, please use ClientOptions.client_encrypted_cert_source."
)
if client_options and client_options.client_encrypted_cert_source:
client_cert_to_use = client_options.client_encrypted_cert_source
elif adc_cert_path and adc_key_path and mtls.has_default_client_cert_source():
client_cert_to_use = mtls.default_client_encrypted_cert_source(
adc_cert_path, adc_key_path
)
if client_cert_to_use:
cert_path, key_path, passphrase = client_cert_to_use()

# The http object we built could be google_auth_httplib2.AuthorizedHttp
# or httplib2.Http. In the first case we need to extract the wrapped
# httplib2.Http object from google_auth_httplib2.AuthorizedHttp.
http_channel = (
http.http
if google_auth_httplib2
and isinstance(http, google_auth_httplib2.AuthorizedHttp)
else http
)
http_channel.add_certificate(key_path, cert_path, "", passphrase)

# If user doesn't provide api endpoint via client options, decide which
# api endpoint to use.
if "mtlsRootUrl" in service and (
not client_options or not client_options.api_endpoint
):
mtls_endpoint = urljoin(service["mtlsRootUrl"], service["servicePath"])
use_mtls_env = os.getenv("GOOGLE_API_USE_MTLS", "Never")

if not use_mtls_env in ("Never", "Auto", "Always"):
raise MutualTLSChannelError(
"Unsupported GOOGLE_API_USE_MTLS value. Accepted values: Never, Auto, Always"
)

# Switch to mTLS endpoint, if environment variable is "Always", or
# environment varibable is "Auto" and client cert exists.
if use_mtls_env == "Always" or (
use_mtls_env == "Auto" and client_cert_to_use
):
base = mtls_endpoint

if model is None:
features = service.get("features", [])
model = JsonModel("dataWrapper" in features)
Expand Down
7 changes: 6 additions & 1 deletion noxfile.py
Expand Up @@ -19,6 +19,7 @@
"google-auth",
"google-auth-httplib2",
"mox",
"parameterized",
"pyopenssl",
"pytest",
"pytest-cov",
Expand Down Expand Up @@ -54,6 +55,10 @@ def lint(session):
],
)
def unit(session, oauth2client):
session.install(
"-e",
"git+https://github.com/googleapis/python-api-core.git@master#egg=google-api-core",
)
session.install(*test_dependencies)
session.install(oauth2client)
if session.python < "3.0":
Expand All @@ -75,4 +80,4 @@ def unit(session, oauth2client):
"--cov-fail-under=85",
"tests",
*session.posargs,
)
)
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -39,7 +39,7 @@
# currently upgrade their httplib2 version.
# Please see https://github.com/googleapis/google-api-python-client/pull/841
"httplib2>=0.9.2,<1dev",
"google-auth>=1.4.1",
"google-auth>=1.16.0",
"google-auth-httplib2>=0.0.3",
"google-api-core>=1.13.0,<2dev",
"six>=1.6.1,<2dev",
Expand Down
1 change: 1 addition & 0 deletions tests/data/bigquery.json
Expand Up @@ -19,6 +19,7 @@
"baseUrl": "https://www.googleapis.com/bigquery/v2/",
"basePath": "/bigquery/v2/",
"rootUrl": "https://www.googleapis.com/",
"mtlsRootUrl": "https://www.mtls.googleapis.com/",
"servicePath": "bigquery/v2/",
"batchPath": "batch",
"parameters": {
Expand Down
1 change: 1 addition & 0 deletions tests/data/drive.json
Expand Up @@ -19,6 +19,7 @@
"baseUrl": "https://www.googleapis.com/drive/v3/",
"basePath": "/drive/v3/",
"rootUrl": "https://www.googleapis.com/",
"mtlsRootUrl": "https://www.mtls.googleapis.com/",
"servicePath": "drive/v3/",
"batchPath": "batch",
"parameters": {
Expand Down
1 change: 1 addition & 0 deletions tests/data/latitude.json
Expand Up @@ -14,6 +14,7 @@
"protocol": "rest",
"basePath": "/latitude/v1/",
"rootUrl": "https://www.googleapis.com/",
"mtlsRootUrl": "https://www.mtls.googleapis.com/",
"servicePath": "latitude/v1/",
"auth": {
"oauth2": {
Expand Down
1 change: 1 addition & 0 deletions tests/data/logging.json
Expand Up @@ -2086,5 +2086,6 @@
"ownerName": "Google",
"version": "v2",
"rootUrl": "https://logging.googleapis.com/",
"mtlsRootUrl": "https://logging.mtls.googleapis.com/",
"kind": "discovery#restDescription"
}
1 change: 1 addition & 0 deletions tests/data/plus.json
Expand Up @@ -16,6 +16,7 @@
"protocol": "rest",
"basePath": "/plus/v1/",
"rootUrl": "https://www.googleapis.com/",
"mtlsRootUrl": "https://www.mtls.googleapis.com/",
"servicePath": "plus/v1/",
"parameters": {
"alt": {
Expand Down
1 change: 1 addition & 0 deletions tests/data/tasks.json
Expand Up @@ -16,6 +16,7 @@
"protocol": "rest",
"basePath": "/tasks/v1/",
"rootUrl": "https://www.googleapis.com/",
"mtlsRootUrl": "https://www.mtls.googleapis.com/",
"servicePath": "tasks/v1/",
"parameters": {
"alt": {
Expand Down
1 change: 1 addition & 0 deletions tests/data/zoo.json
Expand Up @@ -6,6 +6,7 @@
"basePath": "/zoo/",
"batchPath": "batchZoo",
"rootUrl": "https://www.googleapis.com/",
"mtlsRootUrl": "https://www.mtls.googleapis.com/",
"servicePath": "zoo/v1/",
"rpcPath": "/rpc",
"parameters": {
Expand Down

0 comments on commit 981eadf

Please sign in to comment.