Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down
1 change: 1 addition & 0 deletions plane/api/base_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 4 additions & 0 deletions plane/client/oauth_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -156,6 +159,7 @@ def __init__(

# Initialize session
self.session = requests.Session()
self.session.verify = verify

def get_authorization_url(
self,
Expand Down
2 changes: 2 additions & 0 deletions plane/client/plane_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions plane/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -38,3 +39,4 @@ def __init__(
self.access_token = access_token
self.timeout = timeout
self.retry = retry
self.verify = verify
66 changes: 66 additions & 0 deletions tests/unit/test_config.py
Original file line number Diff line number Diff line change
@@ -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