Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## [Unreleased]

- Add the global `--system-truststore` option (env `ENTROPY_DATA_SYSTEM_TRUSTSTORE`) to verify TLS using the operating system's certificate trust store instead of the bundled CA certificates, for use behind a corporate proxy or with an internal CA.

## [0.3.15]

- Replace `entropy-data organization custom-team-roles ...` and the interim `organization team-roles-mode` command with `entropy-data settings team-roles get|put`. The configuration is now a single aggregate payload — `{mode: default}` or `{mode: custom-team-roles, team-roles: [...]}` — wrapping the new `GET`/`PUT /api/settings/team-roles`. `put` replaces the whole catalog when mode is `custom-team-roles`; an empty catalog or dropping a role still assigned to a member is rejected with 409.
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,24 @@ Values from `.env` are loaded as environment variables and do **not** override a

Resolution precedence: CLI options > environment variables / `.env` > config file.

### TLS behind a corporate proxy or internal CA

By default the CLI verifies TLS certificates against the bundled CA certificates (`certifi`). In a corporate network with a TLS-inspecting proxy or an internal certificate authority, this can fail with `CERTIFICATE_VERIFY_FAILED: unable to get local issuer certificate`, because the root CA is installed in the operating system's trust store but not in the bundled list.

Use the global `--system-truststore` option to verify against the operating system's trust store (macOS Keychain, Windows certificate store, or the system CA certificates on Linux) instead:

```bash
entropy-data --system-truststore datacontracts list
```

You can also enable it for every invocation by exporting an environment variable:

```bash
export ENTROPY_DATA_SYSTEM_TRUSTSTORE=1
```

This keeps certificate verification on while trusting the corporate root CA, and applies to every command that makes HTTPS requests.

## Development

```bash
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ dependencies = [
"tomli_w>=1.0,<2.0",
"pyyaml>=6.0,<7.0",
"python-dotenv>=1.0,<2.0",
"truststore>=0.10,<1.0",
]

[project.urls]
Expand Down
24 changes: 24 additions & 0 deletions src/entropy_data/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,19 @@ def version_callback(value: bool) -> None:
raise typer.Exit()


def inject_system_truststore() -> None:
"""Verify TLS using the operating system's certificate trust store instead of the
bundled CA certificates. This lets the CLI work behind corporate proxies or with
internal CAs whose root certificates are installed in the OS trust store but not in
the certifi bundle that requests uses by default."""
try:
import truststore
except ImportError:
error_console.print("[red]--system-truststore requires the 'truststore' package, which is not installed.[/red]")
raise typer.Exit(code=1)
truststore.inject_into_ssl()


app = typer.Typer(
name="entropy-data",
help="CLI for Entropy Data.",
Expand All @@ -76,6 +89,15 @@ def main(
host: Annotated[Optional[str], typer.Option("--host", help="API host URL (overrides config and env).")] = None,
output: Annotated[OutputFormat, typer.Option("--output", "-o", help="Output format.")] = OutputFormat.table,
debug: Annotated[bool, typer.Option("--debug", help="Enable debug output.")] = False,
system_truststore: Annotated[
bool,
typer.Option(
"--system-truststore",
help="Verify TLS using the operating system's certificate trust store "
"instead of the bundled CA certificates (e.g. behind a corporate proxy or internal CA).",
envvar="ENTROPY_DATA_SYSTEM_TRUSTSTORE",
),
] = False,
) -> None:
"""Entropy Data CLI — manage your data platform from the command line."""
global _connection_name, _cli_api_key, _cli_host, _output_format, _debug
Expand All @@ -87,6 +109,8 @@ def main(
_debug = debug
if debug:
logging.basicConfig(level=logging.DEBUG, stream=sys.stderr)
if system_truststore:
inject_system_truststore()


# Register command groups
Expand Down
43 changes: 43 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Tests for the top-level CLI app and its global options."""

import re

from typer.testing import CliRunner

from entropy_data.cli import app

runner = CliRunner()


def test_system_truststore_option_in_help():
result = runner.invoke(app, ["--help"], env={"COLUMNS": "200"})
assert result.exit_code == 0
plain_output = re.sub(r"\x1b\[[0-9;]*m", "", result.stdout)
assert "--system-truststore" in plain_output


def test_system_truststore_flag_injects(monkeypatch):
calls = []
monkeypatch.setattr("entropy_data.cli.inject_system_truststore", lambda: calls.append(True))
result = runner.invoke(app, ["--system-truststore", "teams", "list"])
# The command fails without a configured connection, but the app callback has run.
assert result.exit_code != 0
assert calls == [True]


def test_system_truststore_env_var_injects(monkeypatch):
calls = []
monkeypatch.setattr("entropy_data.cli.inject_system_truststore", lambda: calls.append(True))
monkeypatch.setenv("ENTROPY_DATA_SYSTEM_TRUSTSTORE", "1")
result = runner.invoke(app, ["teams", "list"])
assert result.exit_code != 0
assert calls == [True]


def test_system_truststore_not_injected_by_default(monkeypatch):
calls = []
monkeypatch.setattr("entropy_data.cli.inject_system_truststore", lambda: calls.append(True))
monkeypatch.delenv("ENTROPY_DATA_SYSTEM_TRUSTSTORE", raising=False)
result = runner.invoke(app, ["teams", "list"])
assert result.exit_code != 0
assert calls == []
13 changes: 12 additions & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading