feat: add support for mtls env variables (#1008)
diff --git a/googleapiclient/discovery.py b/googleapiclient/discovery.py
index 7244c5b..13bdf96 100644
--- a/googleapiclient/discovery.py
+++ b/googleapiclient/discovery.py
@@ -117,6 +117,10 @@
}
_PAGE_TOKEN_NAMES = ("pageToken", "nextPageToken")
+# Parameters controlling mTLS behavior. See https://google.aip.dev/auth/4114.
+GOOGLE_API_USE_CLIENT_CERTIFICATE = "GOOGLE_API_USE_CLIENT_CERTIFICATE"
+GOOGLE_API_USE_MTLS_ENDPOINT = "GOOGLE_API_USE_MTLS_ENDPOINT"
+
# Parameters accepted by the stack, but not visible via discovery.
# TODO(dhermes): Remove 'userip' in 'v2'.
STACK_QUERY_PARAMETERS = frozenset(["trace", "pp", "userip", "strict"])
@@ -215,15 +219,30 @@
cache: googleapiclient.discovery_cache.base.CacheBase, an optional
cache object for the discovery documents.
client_options: Mapping object or google.api_core.client_options, client
- options to set user options on the client. The 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.
+ options to set user options on the client.
+ (1) The API endpoint should be set through client_options. If API endpoint
+ is not set, `GOOGLE_API_USE_MTLS_ENDPOINT` environment variable can be used
+ to control which endpoint to use.
+ (2) client_cert_source is not supported, client cert should be provided using
+ client_encrypted_cert_source instead. In order to use the provided client
+ cert, `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be
+ set to `true`.
+ More details on the environment variables are here:
+ https://google.aip.dev/auth/4114
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.
+ use the default client certificate. `GOOGLE_API_USE_CLIENT_CERTIFICATE`
+ environment variable must be set to `true` in order to use this field,
+ otherwise this field doesn't nothing.
+ More details on the environment variables are here:
+ https://google.aip.dev/auth/4114
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.
+ `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be set to
+ `true` in order to use this field, otherwise this field doesn't nothing.
+ More details on the environment variables are here:
+ https://google.aip.dev/auth/4114
num_retries: Integer, number of times to retry discovery with
randomized exponential backoff in case of intermittent/connection issues.
@@ -392,15 +411,30 @@
google.auth.credentials.Credentials, credentials to be used for
authentication.
client_options: Mapping object or google.api_core.client_options, client
- options to set user options on the client. The 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.
+ options to set user options on the client.
+ (1) The API endpoint should be set through client_options. If API endpoint
+ is not set, `GOOGLE_API_USE_MTLS_ENDPOINT` environment variable can be used
+ to control which endpoint to use.
+ (2) client_cert_source is not supported, client cert should be provided using
+ client_encrypted_cert_source instead. In order to use the provided client
+ cert, `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be
+ set to `true`.
+ More details on the environment variables are here:
+ https://google.aip.dev/auth/4114
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.
+ use the default client certificate. `GOOGLE_API_USE_CLIENT_CERTIFICATE`
+ environment variable must be set to `true` in order to use this field,
+ otherwise this field doesn't nothing.
+ More details on the environment variables are here:
+ https://google.aip.dev/auth/4114
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.
+ `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be set to
+ `true` in order to use this field, otherwise this field doesn't nothing.
+ More details on the environment variables are here:
+ https://google.aip.dev/auth/4114
Returns:
A Resource object with methods for interacting with the service.
@@ -469,20 +503,26 @@
# Obtain client cert and create mTLS http channel if cert exists.
client_cert_to_use = None
+ use_client_cert = os.getenv(GOOGLE_API_USE_CLIENT_CERTIFICATE, "false")
+ if not use_client_cert in ("true", "false"):
+ raise MutualTLSChannelError(
+ "Unsupported GOOGLE_API_USE_CLIENT_CERTIFICATE value. Accepted values: true, false"
+ )
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 hasattr(client_options, "client_encrypted_cert_source")
- 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 use_client_cert == "true":
+ if (
+ client_options
+ and hasattr(client_options, "client_encrypted_cert_source")
+ 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()
@@ -503,17 +543,17 @@
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")
+ use_mtls_endpoint = os.getenv(GOOGLE_API_USE_MTLS_ENDPOINT, "auto")
- if not use_mtls_env in ("never", "auto", "always"):
+ if not use_mtls_endpoint in ("never", "auto", "always"):
raise MutualTLSChannelError(
- "Unsupported GOOGLE_API_USE_MTLS value. Accepted values: never, auto, always"
+ "Unsupported GOOGLE_API_USE_MTLS_ENDPOINT 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
+ if use_mtls_endpoint == "always" or (
+ use_mtls_endpoint == "auto" and client_cert_to_use
):
base = mtls_endpoint
diff --git a/tests/test_discovery.py b/tests/test_discovery.py
index 87cc8ed..7e44a3e 100644
--- a/tests/test_discovery.py
+++ b/tests/test_discovery.py
@@ -595,12 +595,12 @@
ADC_KEY_PATH = "adc_key_path"
ADC_PASSPHRASE = "adc_passphrase"
- def check_http_client_cert(self, resource, has_client_cert=False):
+ def check_http_client_cert(self, resource, has_client_cert="false"):
if isinstance(resource._http, google_auth_httplib2.AuthorizedHttp):
certs = list(resource._http.http.certificates.iter(""))
else:
certs = list(resource._http.certificates.iter(""))
- if has_client_cert:
+ if has_client_cert == "true":
self.assertEqual(len(certs), 1)
self.assertEqual(
certs[0], (self.ADC_KEY_PATH, self.ADC_CERT_PATH, self.ADC_PASSPHRASE)
@@ -611,67 +611,127 @@
def client_encrypted_cert_source(self):
return self.ADC_CERT_PATH, self.ADC_KEY_PATH, self.ADC_PASSPHRASE
- def test_mtls_not_trigger_if_http_provided(self):
+ @parameterized.expand(
+ [
+ ("never", "true"),
+ ("auto", "true"),
+ ("always", "true"),
+ ("never", "false"),
+ ("auto", "false"),
+ ("always", "false"),
+ ]
+ )
+ def test_mtls_not_trigger_if_http_provided(self, use_mtls_env, use_client_cert):
discovery = read_datafile("plus.json")
- plus = build_from_document(discovery, http=httplib2.Http())
- self.assertIsNotNone(plus)
- self.assertEqual(plus._baseUrl, REGULAR_ENDPOINT)
- self.check_http_client_cert(plus, has_client_cert=False)
- def test_exception_with_client_cert_source(self):
- discovery = read_datafile("plus.json")
- with self.assertRaises(MutualTLSChannelError):
- build_from_document(
- discovery,
- credentials=self.MOCK_CREDENTIALS,
- client_options={"client_cert_source": mock.Mock()},
- )
+ with mock.patch.dict(
+ "os.environ", {"GOOGLE_API_USE_MTLS_ENDPOINT": use_mtls_env}
+ ):
+ with mock.patch.dict(
+ "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert}
+ ):
+ plus = build_from_document(discovery, http=httplib2.Http())
+ self.assertIsNotNone(plus)
+ self.assertEqual(plus._baseUrl, REGULAR_ENDPOINT)
+ self.check_http_client_cert(plus, has_client_cert="false")
@parameterized.expand(
[
- ("never", REGULAR_ENDPOINT),
- ("auto", MTLS_ENDPOINT),
- ("always", MTLS_ENDPOINT),
+ ("never", "true"),
+ ("auto", "true"),
+ ("always", "true"),
+ ("never", "false"),
+ ("auto", "false"),
+ ("always", "false"),
]
)
- def test_mtls_with_provided_client_cert(self, use_mtls_env, base_url):
+ def test_exception_with_client_cert_source(self, use_mtls_env, use_client_cert):
+ discovery = read_datafile("plus.json")
+ with mock.patch.dict(
+ "os.environ", {"GOOGLE_API_USE_MTLS_ENDPOINT": use_mtls_env}
+ ):
+ with mock.patch.dict(
+ "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert}
+ ):
+ with self.assertRaises(MutualTLSChannelError):
+ build_from_document(
+ discovery,
+ credentials=self.MOCK_CREDENTIALS,
+ client_options={"client_cert_source": mock.Mock()},
+ )
+
+ @parameterized.expand(
+ [
+ ("never", "true", REGULAR_ENDPOINT),
+ ("auto", "true", MTLS_ENDPOINT),
+ ("always", "true", MTLS_ENDPOINT),
+ ("never", "false", REGULAR_ENDPOINT),
+ ("auto", "false", REGULAR_ENDPOINT),
+ ("always", "false", MTLS_ENDPOINT),
+ ]
+ )
+ def test_mtls_with_provided_client_cert(
+ self, use_mtls_env, use_client_cert, base_url
+ ):
discovery = read_datafile("plus.json")
- with mock.patch.dict("os.environ", {"GOOGLE_API_USE_MTLS": use_mtls_env}):
- plus = build_from_document(
- discovery,
- credentials=self.MOCK_CREDENTIALS,
- client_options={
- "client_encrypted_cert_source": self.client_encrypted_cert_source
- },
- )
- self.assertIsNotNone(plus)
- self.check_http_client_cert(plus, has_client_cert=True)
- self.assertEqual(plus._baseUrl, base_url)
+ with mock.patch.dict(
+ "os.environ", {"GOOGLE_API_USE_MTLS_ENDPOINT": use_mtls_env}
+ ):
+ with mock.patch.dict(
+ "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert}
+ ):
+ plus = build_from_document(
+ discovery,
+ credentials=self.MOCK_CREDENTIALS,
+ client_options={
+ "client_encrypted_cert_source": self.client_encrypted_cert_source
+ },
+ )
+ self.assertIsNotNone(plus)
+ self.check_http_client_cert(plus, has_client_cert=use_client_cert)
+ self.assertEqual(plus._baseUrl, base_url)
- @parameterized.expand(["never", "auto", "always"])
- def test_endpoint_not_switch(self, use_mtls_env):
+ @parameterized.expand(
+ [
+ ("never", "true"),
+ ("auto", "true"),
+ ("always", "true"),
+ ("never", "false"),
+ ("auto", "false"),
+ ("always", "false"),
+ ]
+ )
+ def test_endpoint_not_switch(self, use_mtls_env, use_client_cert):
# Test endpoint is not switched if user provided api endpoint
discovery = read_datafile("plus.json")
- with mock.patch.dict("os.environ", {"GOOGLE_API_USE_MTLS": use_mtls_env}):
- plus = build_from_document(
- discovery,
- credentials=self.MOCK_CREDENTIALS,
- client_options={
- "api_endpoint": "https://foo.googleapis.com",
- "client_encrypted_cert_source": self.client_encrypted_cert_source,
- },
- )
- self.assertIsNotNone(plus)
- self.check_http_client_cert(plus, has_client_cert=True)
- self.assertEqual(plus._baseUrl, "https://foo.googleapis.com")
+ with mock.patch.dict(
+ "os.environ", {"GOOGLE_API_USE_MTLS_ENDPOINT": use_mtls_env}
+ ):
+ with mock.patch.dict(
+ "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert}
+ ):
+ plus = build_from_document(
+ discovery,
+ credentials=self.MOCK_CREDENTIALS,
+ client_options={
+ "api_endpoint": "https://foo.googleapis.com",
+ "client_encrypted_cert_source": self.client_encrypted_cert_source,
+ },
+ )
+ self.assertIsNotNone(plus)
+ self.check_http_client_cert(plus, has_client_cert=use_client_cert)
+ self.assertEqual(plus._baseUrl, "https://foo.googleapis.com")
@parameterized.expand(
[
- ("never", REGULAR_ENDPOINT),
- ("auto", MTLS_ENDPOINT),
- ("always", MTLS_ENDPOINT),
+ ("never", "true", REGULAR_ENDPOINT),
+ ("auto", "true", MTLS_ENDPOINT),
+ ("always", "true", MTLS_ENDPOINT),
+ ("never", "false", REGULAR_ENDPOINT),
+ ("auto", "false", REGULAR_ENDPOINT),
+ ("always", "false", MTLS_ENDPOINT),
]
)
@mock.patch(
@@ -683,6 +743,7 @@
def test_mtls_with_default_client_cert(
self,
use_mtls_env,
+ use_client_cert,
base_url,
default_client_encrypted_cert_source,
has_default_client_cert_source,
@@ -693,43 +754,56 @@
)
discovery = read_datafile("plus.json")
- with mock.patch.dict("os.environ", {"GOOGLE_API_USE_MTLS": use_mtls_env}):
- plus = build_from_document(
- discovery,
- credentials=self.MOCK_CREDENTIALS,
- adc_cert_path=self.ADC_CERT_PATH,
- adc_key_path=self.ADC_KEY_PATH,
- )
- self.assertIsNotNone(plus)
- self.check_http_client_cert(plus, has_client_cert=True)
- self.assertEqual(plus._baseUrl, base_url)
+ with mock.patch.dict(
+ "os.environ", {"GOOGLE_API_USE_MTLS_ENDPOINT": use_mtls_env}
+ ):
+ with mock.patch.dict(
+ "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert}
+ ):
+ plus = build_from_document(
+ discovery,
+ credentials=self.MOCK_CREDENTIALS,
+ adc_cert_path=self.ADC_CERT_PATH,
+ adc_key_path=self.ADC_KEY_PATH,
+ )
+ self.assertIsNotNone(plus)
+ self.check_http_client_cert(plus, has_client_cert=use_client_cert)
+ self.assertEqual(plus._baseUrl, base_url)
@parameterized.expand(
[
- ("never", REGULAR_ENDPOINT),
- ("auto", REGULAR_ENDPOINT),
- ("always", MTLS_ENDPOINT),
+ ("never", "true", REGULAR_ENDPOINT),
+ ("auto", "true", REGULAR_ENDPOINT),
+ ("always", "true", MTLS_ENDPOINT),
+ ("never", "false", REGULAR_ENDPOINT),
+ ("auto", "false", REGULAR_ENDPOINT),
+ ("always", "false", MTLS_ENDPOINT),
]
)
@mock.patch(
"google.auth.transport.mtls.has_default_client_cert_source", autospec=True
)
def test_mtls_with_no_client_cert(
- self, use_mtls_env, base_url, has_default_client_cert_source
+ self, use_mtls_env, use_client_cert, base_url, has_default_client_cert_source
):
has_default_client_cert_source.return_value = False
discovery = read_datafile("plus.json")
- with mock.patch.dict("os.environ", {"GOOGLE_API_USE_MTLS": use_mtls_env}):
- plus = build_from_document(
- discovery,
- credentials=self.MOCK_CREDENTIALS,
- adc_cert_path=self.ADC_CERT_PATH,
- adc_key_path=self.ADC_KEY_PATH,
- )
- self.assertIsNotNone(plus)
- self.check_http_client_cert(plus, has_client_cert=False)
- self.assertEqual(plus._baseUrl, base_url)
+ with mock.patch.dict(
+ "os.environ", {"GOOGLE_API_USE_MTLS_ENDPOINT": use_mtls_env}
+ ):
+ with mock.patch.dict(
+ "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert}
+ ):
+ plus = build_from_document(
+ discovery,
+ credentials=self.MOCK_CREDENTIALS,
+ adc_cert_path=self.ADC_CERT_PATH,
+ adc_key_path=self.ADC_KEY_PATH,
+ )
+ self.assertIsNotNone(plus)
+ self.check_http_client_cert(plus, has_client_cert="false")
+ self.assertEqual(plus._baseUrl, base_url)
class DiscoveryFromHttp(unittest.TestCase):