Skip to content

Commit b3e6a07

Browse files
authored
feat: Add custom CA file support for verify_ssl (#292)
* Add custom CA file support for `api.verify_ssl` config option * Fix Union types in config docs
1 parent 2cdca9f commit b3e6a07

File tree

4 files changed

+50
-26
lines changed

4 files changed

+50
-26
lines changed

CHANGELOG

+4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
## [Unreleased]
1111

12+
### Added
13+
14+
- Support for custom CA file bundles for Zabbix API connections. The config option `api.verify_ssl` now accepts a path to a custom CA file bundle.
15+
1216
### Fixed
1317

1418
- `create_maintenance_definition` with multiple host groups only including the first group in the maintenance definition for Zabbix >=6.0.

docs/scripts/gen_config_data.py

+24-19
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ def validate_default(cls, value: Any) -> Optional[Any]:
165165
return None
166166
if isinstance(value, SecretStr):
167167
return value.get_secret_value()
168+
if isinstance(value, bool):
169+
return str(value).lower()
168170
return value
169171

170172
@field_validator("type", mode="before")
@@ -175,6 +177,24 @@ def validate_type(cls, value: Any) -> str:
175177

176178
origin = get_origin(value)
177179
args = get_args(value)
180+
181+
def type_to_str(t: type[Any]) -> str:
182+
if lenient_issubclass(value, str):
183+
return "str"
184+
if lenient_issubclass(value, Enum):
185+
# Get the name of the first enum member type
186+
# Will fail if enum has no members
187+
return str(list(value)[0]) # pyright: ignore[reportUnknownArgumentType]
188+
# Types that are represented as strings in config (paths, secrets, etc.)
189+
if typ := TYPE_MAP.get(t):
190+
return typ
191+
# Primitives and built-in generics (str, int, list[str], dict[str, int], etc.)
192+
if origin in TYPE_CAN_STR:
193+
return str(value)
194+
# Fall back on the string representation of the type
195+
return getattr(value, "__name__", str(value))
196+
197+
# Handle generics, literals, etc.
178198
if origin and args:
179199
# Get the name of the first type in the Literal type
180200
# NOTE: we expect that Literal is only used with a single type
@@ -183,26 +203,11 @@ def validate_type(cls, value: Any) -> str:
183203
# Get first non-None type in Union
184204
# NOTE: we expect that the config does not have unions of more than 2 types
185205
elif origin is Union and args:
186-
return next(a.__name__ for a in args if a is not type(None))
187-
188-
if lenient_issubclass(value, str):
189-
return "str"
190-
191-
if lenient_issubclass(value, Enum):
192-
# Get the name of the first enum member type
193-
# Will fail if enum has no members
194-
return str(list(value)[0]) # pyright: ignore[reportUnknownArgumentType]
195-
196-
# Types that are represented as strings in config (paths, secrets, etc.)
197-
if t := TYPE_MAP.get(value):
198-
return t
199-
200-
# Primitives and built-in generics (str, int, list[str], dict[str, int], etc.)
201-
if origin in TYPE_CAN_STR:
202-
return str(value)
206+
# Strip None from the Union
207+
ar = (type_to_str(a) for a in args if a is not type(None))
208+
return " | ".join(ar)
203209

204-
# Fall back on the string representation of the type
205-
return getattr(value, "__name__", str(value))
210+
return type_to_str(value)
206211

207212
@field_validator("description", mode="before")
208213
@classmethod

zabbix_cli/config/model.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,11 @@ class APIConfig(BaseModel):
128128
description="API auth token.",
129129
examples=["API_TOKEN_123"],
130130
)
131-
verify_ssl: bool = Field(
131+
verify_ssl: Union[bool, Path] = Field(
132132
default=True,
133133
# Changed in V3: cert_verify -> verify_ssl
134134
validation_alias=AliasChoices("verify_ssl", "cert_verify"),
135-
description="Verify SSL certificate of the Zabbix API host.",
135+
description="Verify SSL certificate of the Zabbix API host. Can also be a path to a CA bundle.",
136136
)
137137
timeout: Optional[int] = Field(
138138
default=0,

zabbix_cli/pyzabbix/client.py

+20-5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from __future__ import annotations
1616

1717
import logging
18+
import ssl
1819
from collections.abc import MutableMapping
1920
from datetime import datetime
2021
from functools import cached_property
@@ -242,12 +243,12 @@ def __init__(
242243
server: str = "http://localhost/zabbix",
243244
*,
244245
timeout: Optional[int] = None,
245-
verify_ssl: bool = True,
246+
verify_ssl: Union[bool, Path] = True,
246247
) -> None:
247248
"""Parameters:
248249
server: Base URI for zabbix web interface (omitting /api_jsonrpc.php)
249-
session: optional pre-configured requests.Session instance
250-
timeout: optional connect and read timeout in seconds.
250+
timeout: Read and connect timeout for HTTP requests in seconds.
251+
verify_ssl: Verify SSL certificates. Can be a boolean or a path to a CA bundle.
251252
"""
252253
self.timeout = timeout if timeout else None
253254
self.session = self._get_client(verify_ssl=verify_ssl, timeout=timeout)
@@ -279,14 +280,28 @@ def from_config(cls, config: Config) -> ZabbixAPI:
279280
)
280281
return client
281282

283+
def _get_ssl_context(
284+
self, verify_ssl: Union[bool, Path]
285+
) -> Union[ssl.SSLContext, bool]:
286+
if isinstance(verify_ssl, Path):
287+
if not verify_ssl.exists():
288+
raise ValueError(f"CA bundle not found: {verify_ssl}")
289+
if verify_ssl.is_dir():
290+
ctx = ssl.create_default_context(capath=verify_ssl)
291+
else:
292+
ctx = ssl.create_default_context(cafile=verify_ssl)
293+
else:
294+
ctx = verify_ssl
295+
return ctx
296+
282297
def _get_client(
283-
self, *, verify_ssl: bool, timeout: Union[float, int, None] = None
298+
self, *, verify_ssl: Union[bool, Path], timeout: Union[float, int, None] = None
284299
) -> httpx.Client:
285300
kwargs: HTTPXClientKwargs = {}
286301
if timeout is not None:
287302
kwargs["timeout"] = timeout
288303
client = httpx.Client(
289-
verify=verify_ssl,
304+
verify=self._get_ssl_context(verify_ssl),
290305
# Default headers for all requests
291306
headers={
292307
"Content-Type": "application/json-rpc",

0 commit comments

Comments
 (0)