diff --git a/README.md b/README.md index 9635e65..d2370c3 100644 --- a/README.md +++ b/README.md @@ -1120,6 +1120,7 @@ client = PlaneClient( | `access_token` | `str` | Optional | Access token for authentication | | `timeout` | `float \| tuple[float, float]` | `30.0` | Request timeout in seconds | | `retry` | `RetryConfig` | None | Retry configuration | +| `verify` | `bool \| str` | `True` | TLS verification: `True` to verify against the system CA store, `False` to disable verification entirely, or a path to a CA bundle file to verify against a custom CA. Passed straight through to `requests.Session.verify` — see [requests: SSL Cert Verification](https://requests.readthedocs.io/en/latest/user/advanced/#ssl-cert-verification). | **Note**: Provide exactly one of `api_key` or `access_token`. diff --git a/plane/api/base_resource.py b/plane/api/base_resource.py index 2f89e1e..6958c00 100644 --- a/plane/api/base_resource.py +++ b/plane/api/base_resource.py @@ -14,6 +14,7 @@ def __init__(self, config: Configuration, base_path: str) -> None: self.config = config self.base_path = base_path.rstrip("/") self.session = requests.Session() + self.session.verify = config.verify if self.config.retry: retry = Retry( diff --git a/plane/client/oauth_client.py b/plane/client/oauth_client.py index 9d264de..f15f2be 100644 --- a/plane/client/oauth_client.py +++ b/plane/client/oauth_client.py @@ -131,6 +131,7 @@ def __init__( client_id: str, client_secret: str, timeout: float | tuple[float, float] | None = 30.0, + verify: bool | str = True, ) -> None: """ Initialize OAuth client. @@ -140,6 +141,8 @@ def __init__( client_id: OAuth client ID client_secret: OAuth client secret timeout: Request timeout in seconds (default: 30.0) + verify: TLS verification, passed through to requests.Session.verify: + True (default), False, or a path to a CA bundle. Raises: ConfigurationError: If required parameters are missing @@ -156,6 +159,7 @@ def __init__( # Initialize session self.session = requests.Session() + self.session.verify = verify def get_authorization_url( self, diff --git a/plane/client/plane_client.py b/plane/client/plane_client.py index 9907cf7..87e291d 100644 --- a/plane/client/plane_client.py +++ b/plane/client/plane_client.py @@ -43,6 +43,7 @@ def __init__( base_url: str, api_key: str | None = None, access_token: str | None = None, + verify: bool | str = True, ) -> None: if not api_key and not access_token: raise ConfigurationError( @@ -57,6 +58,7 @@ def __init__( base_path=base_url, api_key=api_key, access_token=access_token, + verify=verify, ) self.users = Users(self.config) diff --git a/plane/config.py b/plane/config.py index 00136ed..a267eba 100644 --- a/plane/config.py +++ b/plane/config.py @@ -23,6 +23,7 @@ def __init__( access_token: str | None = None, timeout: float | tuple[float, float] | None = 30.0, retry: RetryConfig | None = None, + verify: bool | str = True, ) -> None: if not api_key and not access_token: raise ConfigurationError( @@ -38,3 +39,4 @@ def __init__( self.access_token = access_token self.timeout = timeout self.retry = retry + self.verify = verify diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py new file mode 100644 index 0000000..3e4163b --- /dev/null +++ b/tests/unit/test_config.py @@ -0,0 +1,66 @@ +"""Unit tests for TLS verification config (Configuration.verify / PlaneClient(verify=...)). + +These construct clients/config directly and never make network calls, so they +run without any PLANE_* environment variables. +""" + +from plane.client import PlaneClient +from plane.client.oauth_client import OAuthClient +from plane.config import Configuration + + +def test_verify_defaults_to_true() -> None: + config = Configuration(base_path="https://api.plane.so", api_key="test-key") + assert config.verify is True + + +def test_verify_false_propagates_to_configuration() -> None: + config = Configuration(base_path="https://api.plane.so", api_key="test-key", verify=False) + assert config.verify is False + + +def test_verify_ca_bundle_path_propagates_verbatim() -> None: + ca_path = "/etc/ssl/certs/internal-ca.pem" + config = Configuration(base_path="https://api.plane.so", api_key="test-key", verify=ca_path) + assert config.verify == ca_path + + +def test_plane_client_verify_defaults_to_true() -> None: + client = PlaneClient(base_url="https://api.plane.so", api_key="test-key") + assert client.config.verify is True + assert client.projects.session.verify is True + + +def test_plane_client_verify_false_propagates_to_session() -> None: + client = PlaneClient(base_url="https://api.plane.so", api_key="test-key", verify=False) + assert client.config.verify is False + assert client.projects.session.verify is False + + +def test_plane_client_verify_ca_bundle_propagates_to_all_resources() -> None: + ca_path = "/etc/ssl/certs/internal-ca.pem" + client = PlaneClient(base_url="https://api.plane.so", api_key="test-key", verify=ca_path) + + assert client.projects.session.verify == ca_path + assert client.work_items.session.verify == ca_path + # Sub-resources build their own BaseResource off the same Configuration. + assert client.work_items.comments.session.verify == ca_path + + +def test_oauth_client_verify_defaults_to_true() -> None: + oauth_client = OAuthClient( + base_url="https://api.plane.so", + client_id="client-id", + client_secret="client-secret", + ) + assert oauth_client.session.verify is True + + +def test_oauth_client_verify_false_propagates_to_session() -> None: + oauth_client = OAuthClient( + base_url="https://api.plane.so", + client_id="client-id", + client_secret="client-secret", + verify=False, + ) + assert oauth_client.session.verify is False