Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add mtls feature #917

Merged
merged 12 commits into from Jun 2, 2020
99 changes: 90 additions & 9 deletions googleapiclient/discovery.py
@@ -1,4 +1,4 @@
# Copyright 2014 Google Inc. All Rights Reserved.
# Copyright 2020 Google Inc. All Rights Reserved.
arithmetic1728 marked this conversation as resolved.
Show resolved Hide resolved
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down 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.
arithmetic1728 marked this conversation as resolved.
Show resolved Hide resolved

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,
)
)
3 changes: 3 additions & 0 deletions samples/cloudkms/README
@@ -0,0 +1,3 @@
.Cloud KMS mutual TLS example to list key rings in global location.

api: cloudkms
66 changes: 66 additions & 0 deletions samples/cloudkms/main.py
@@ -0,0 +1,66 @@
#!/usr/bin/env python
arithmetic1728 marked this conversation as resolved.
Show resolved Hide resolved
# -*- coding: utf-8 -*-
#
# Copyright 2020 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Mutual TLS example for Cloud KMS.

This application returns the list of key rings in global location. First fill in
the project and adc_cert_key_folder value, then set the GOOGLE_API_USE_MTLS
environment variable to one of the following values based on your need:
(1) "Never": This means we always use regular api endpoint. Since this is the
default value, there is no need to explicitly set it.
(2) "Always": This means we always use the mutual TLS api endpoint.
(3) "Auto": This means we auto switch to mutual TLS api endpoint, if the ADC
client cert and key are detected.
"""
from __future__ import print_function
import google.api_core.client_options
import google.oauth2.credentials

from googleapiclient.discovery import build

project = "FILL IN YOUR PROJECT ID"

# The following are the file paths to save ADC client cert and key. If you don't
# want to use mutual TLS, simply ignore adc_cert_key_folder, and set adc_cert_path
# and adc_key_path to None.
adc_cert_key_folder = "FILL IN THE PATH WHERE YOU WANT TO SAVE ADC CERT AND KEY"
adc_cert_path = adc_cert_key_folder + "cert.pem"
adc_key_path = adc_cert_key_folder + "key.pem"


def main():
cred = google.oauth2.credentials.UserAccessTokenCredentials()

parent = "projects/{project}/locations/{location}".format(
project=project, location="global"
)

# Build a service object for interacting with the API.
service = build(
"cloudkms",
"v1",
credentials=cred,
adc_cert_path=adc_cert_path,
adc_key_path=adc_key_path,
)

# Return the list of key rings in global location.
return service.projects().locations().keyRings().list(parent=parent).execute()


if __name__ == "__main__":
main()
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