Skip to content

Commit 2c0b579

Browse files
committed
feat(api-core): centralize MTLS fallback functions in gapic_v1 config
1 parent df0541a commit 2c0b579

2 files changed

Lines changed: 145 additions & 0 deletions

File tree

packages/google-api-core/google/api_core/gapic_v1/config.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,14 @@
1919
"""
2020

2121
import collections
22+
import os
23+
from typing import Callable, Optional, Tuple
2224

2325
import grpc
2426

2527
from google.api_core import exceptions, retry, timeout
28+
from google.auth.exceptions import MutualTLSChannelError # type: ignore
29+
from google.auth.transport import mtls # type: ignore
2630

2731
_MILLIS_PER_SECOND = 1000.0
2832

@@ -170,3 +174,51 @@ def parse_method_configs(interface_config, retry_impl=retry.Retry):
170174
method_configs[method_name] = MethodConfig(retry=retry_, timeout=timeout_)
171175

172176
return method_configs
177+
178+
179+
def use_client_cert_effective() -> bool:
180+
"""Returns whether client certificate should be used for mTLS."""
181+
if hasattr(mtls, "should_use_client_cert"):
182+
return mtls.should_use_client_cert()
183+
else:
184+
use_client_cert_str = os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false").lower()
185+
if use_client_cert_str not in ("true", "false"):
186+
raise ValueError(
187+
"Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be"
188+
" either `true` or `false`"
189+
)
190+
return use_client_cert_str == "true"
191+
192+
193+
def get_client_cert_source(
194+
provided_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]],
195+
use_cert_flag: bool,
196+
) -> Optional[Callable[[], Tuple[bytes, bytes]]]:
197+
"""Return the client cert source to be used by the client."""
198+
client_cert_source = None
199+
if use_cert_flag:
200+
if provided_cert_source:
201+
client_cert_source = provided_cert_source
202+
elif (
203+
hasattr(mtls, "has_default_client_cert_source")
204+
and mtls.has_default_client_cert_source()
205+
):
206+
client_cert_source = mtls.default_client_cert_source()
207+
else:
208+
raise ValueError(
209+
"Client certificate is required for mTLS, but no client certificate source was provided or found."
210+
)
211+
return client_cert_source
212+
213+
214+
def read_environment_variables() -> Tuple[bool, str, Optional[str]]:
215+
"""Returns the environment variables used by the client."""
216+
use_client_cert = use_client_cert_effective()
217+
use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto").lower()
218+
universe_domain_env = os.getenv("GOOGLE_CLOUD_UNIVERSE_DOMAIN")
219+
if use_mtls_endpoint not in ("auto", "never", "always"):
220+
raise MutualTLSChannelError(
221+
"Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` "
222+
"must be `never`, `auto` or `always`"
223+
)
224+
return use_client_cert, use_mtls_endpoint, universe_domain_env

packages/google-api-core/tests/unit/gapic/test_config.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import os
16+
from unittest import mock
17+
1518
import pytest
19+
from google.auth.exceptions import MutualTLSChannelError
1620

1721
try:
1822
import grpc # noqa: F401
@@ -91,3 +95,92 @@ def test_create_method_configs():
9195
retry, timeout = method_configs["Plain"]
9296
assert retry is None
9397
assert timeout._timeout == 30.0
98+
99+
100+
def test_use_client_cert_effective_true():
101+
mock_mtls = mock.Mock(spec=["should_use_client_cert"])
102+
mock_mtls.should_use_client_cert.return_value = True
103+
with mock.patch("google.api_core.gapic_v1.config.mtls", mock_mtls):
104+
assert config.use_client_cert_effective() is True
105+
106+
107+
def test_use_client_cert_effective_false():
108+
mock_mtls = mock.Mock(spec=["should_use_client_cert"])
109+
mock_mtls.should_use_client_cert.return_value = False
110+
with mock.patch("google.api_core.gapic_v1.config.mtls", mock_mtls):
111+
assert config.use_client_cert_effective() is False
112+
113+
114+
def test_use_client_cert_effective_fallback_env_true():
115+
mock_mtls = mock.Mock(spec=[])
116+
with mock.patch("google.api_core.gapic_v1.config.mtls", mock_mtls):
117+
with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"}):
118+
assert config.use_client_cert_effective() is True
119+
120+
121+
def test_use_client_cert_effective_fallback_env_false():
122+
mock_mtls = mock.Mock(spec=[])
123+
with mock.patch("google.api_core.gapic_v1.config.mtls", mock_mtls):
124+
with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "false"}):
125+
assert config.use_client_cert_effective() is False
126+
127+
128+
def test_use_client_cert_effective_fallback_env_invalid():
129+
mock_mtls = mock.Mock(spec=[])
130+
with mock.patch("google.api_core.gapic_v1.config.mtls", mock_mtls):
131+
with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "invalid"}):
132+
with pytest.raises(
133+
ValueError,
134+
match="Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`",
135+
):
136+
config.use_client_cert_effective()
137+
138+
139+
def test_get_client_cert_source_provided():
140+
source = mock.Mock()
141+
assert config.get_client_cert_source(source, True) == source
142+
143+
144+
def test_get_client_cert_source_default():
145+
mock_mtls = mock.Mock(spec=["has_default_client_cert_source", "default_client_cert_source"])
146+
mock_mtls.has_default_client_cert_source.return_value = True
147+
mock_source = mock.Mock()
148+
mock_mtls.default_client_cert_source.return_value = mock_source
149+
with mock.patch("google.api_core.gapic_v1.config.mtls", mock_mtls):
150+
assert config.get_client_cert_source(None, True) == mock_source
151+
152+
153+
def test_get_client_cert_source_none():
154+
mock_mtls = mock.Mock(spec=["has_default_client_cert_source", "default_client_cert_source"])
155+
mock_mtls.has_default_client_cert_source.return_value = False
156+
with mock.patch("google.api_core.gapic_v1.config.mtls", mock_mtls):
157+
with pytest.raises(
158+
ValueError,
159+
match="Client certificate is required for mTLS, but no client certificate source was provided or found.",
160+
):
161+
config.get_client_cert_source(None, True)
162+
163+
164+
def test_get_client_cert_source_use_cert_flag_false():
165+
assert config.get_client_cert_source(None, False) is None
166+
source = mock.Mock()
167+
assert config.get_client_cert_source(source, False) is None
168+
169+
170+
def test_read_environment_variables():
171+
with mock.patch("google.api_core.gapic_v1.config.use_client_cert_effective", return_value=True):
172+
with mock.patch.dict(
173+
os.environ,
174+
{"GOOGLE_API_USE_MTLS_ENDPOINT": "always", "GOOGLE_CLOUD_UNIVERSE_DOMAIN": "my-universe.com"}
175+
):
176+
use_cert, use_mtls, universe = config.read_environment_variables()
177+
assert use_cert is True
178+
assert use_mtls == "always"
179+
assert universe == "my-universe.com"
180+
181+
182+
def test_read_environment_variables_invalid_mtls():
183+
with mock.patch("google.api_core.gapic_v1.config.use_client_cert_effective", return_value=True):
184+
with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "invalid"}):
185+
with pytest.raises(MutualTLSChannelError):
186+
config.read_environment_variables()

0 commit comments

Comments
 (0)