diff --git a/README.md b/README.md index d4383d2..ef39414 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,29 @@ Check my stripe balance The agent will use authsome to login into external services and perform the task. +## Proxy Policy + +By default, `authsome run` injects credentials for connected providers and lets requests to unrecognised hosts pass through (`connected_allow`). You can tighten or loosen this behaviour with the proxy-mode setting. + +```bash +# Read current mode +authsome config get proxy-mode + +# Set mode +authsome config set proxy-mode connected_deny +``` + +| Mode | Scope | Unmatched requests | +|------|-------|--------------------| +| `connected_allow` | connected providers only | pass through (**default**) | +| `connected_deny` | connected providers only | blocked with 403 | +| `configured_allow` | all configured providers (including disconnected) | pass through | +| `configured_deny` | all configured providers | blocked with 403 | + +The mode is caller-local — it lives in `~/.authsome/config.json` and applies per `authsome run` invocation. The daemon never sees or acts on it. + +--- + ## Agent Integrations Authsome ships with adapters for the most common agent frameworks and CLIs: diff --git a/src/authsome/cli/client_config.py b/src/authsome/cli/client_config.py index 7b7b397..e1d6c0f 100644 --- a/src/authsome/cli/client_config.py +++ b/src/authsome/cli/client_config.py @@ -24,8 +24,8 @@ class ClientConfig(BaseModel): The proxy_mode field lives here (not in ServerConfig) because the mitmproxy addon runs inside the CLI process per `authsome run` invocation. The daemon never acts on the mode itself; only the - caller-local proxy does. Users can change the mode by editing this - file directly — there is no CLI command for it today (YAGNI). + caller-local proxy does. Use `authsome config get/set proxy-mode` + to read or change the mode without hand-editing the file. """ version: str = __version__ diff --git a/src/authsome/cli/main.py b/src/authsome/cli/main.py index e467196..7dba54e 100644 --- a/src/authsome/cli/main.py +++ b/src/authsome/cli/main.py @@ -517,6 +517,62 @@ async def init(ctx_obj: ContextObj) -> None: ctx_obj.print_json(data) +_VALID_PROXY_MODES = ("connected_allow", "connected_deny", "configured_allow", "configured_deny") + + +@cli.group(name="config") +def config_group() -> None: + """Read and write caller-local configuration.""" + + +@config_group.command(name="get") +@click.argument("key") +@auth_command +def config_get(ctx_obj: ContextObj, key: str) -> None: + """Read a caller-local configuration value. + + Supported keys: proxy-mode + """ + from authsome.cli.client_config import load_client_config + + if key != "proxy-mode": + ctx_obj.print_json({"error": "UnknownConfigKey", "message": f"Unknown key '{key}'. Valid keys: proxy-mode"}) + sys.exit(1) + + home = Path(os.environ.get("AUTHSOME_HOME", str(Path.home() / ".authsome"))) + config = load_client_config(home) + ctx_obj.print_json({"proxy_mode": config.proxy_mode}) + + +@config_group.command(name="set") +@click.argument("key") +@click.argument("value") +@auth_command +def config_set(ctx_obj: ContextObj, key: str, value: str) -> None: + """Write a caller-local configuration value. + + Supported keys: proxy-mode + Valid proxy-mode values: connected_allow, connected_deny, configured_allow, configured_deny + """ + from authsome.cli.client_config import load_client_config, save_client_config + + if key != "proxy-mode": + ctx_obj.print_json({"error": "UnknownConfigKey", "message": f"Unknown key '{key}'. Valid keys: proxy-mode"}) + sys.exit(1) + + if value not in _VALID_PROXY_MODES: + modes_str = ", ".join(_VALID_PROXY_MODES) + ctx_obj.print_json( + {"error": "InvalidProxyMode", "message": f"Invalid proxy mode '{value}'. Valid modes: {modes_str}"} + ) + sys.exit(1) + + home = Path(os.environ.get("AUTHSOME_HOME", str(Path.home() / ".authsome"))) + updated = load_client_config(home).model_copy(update={"proxy_mode": value}) + save_client_config(home, updated) + ctx_obj.print_json({"proxy_mode": updated.proxy_mode, "status": "ok"}) + + @cli.group(name="profile") def profile() -> None: """Manage local profiles backed by identity keys.""" diff --git a/tests/cli/test_config.py b/tests/cli/test_config.py new file mode 100644 index 0000000..8ee6172 --- /dev/null +++ b/tests/cli/test_config.py @@ -0,0 +1,75 @@ +"""Tests for `authsome config get` and `authsome config set`.""" + +import json + +import pytest + +from authsome.cli.main import cli + + +class TestConfigGet: + """Tests for `authsome config get proxy-mode`.""" + + def test_get_returns_default_mode(self, runner) -> None: + result = runner.invoke(cli, ["--log-file", "", "config", "get", "proxy-mode"]) + assert result.exit_code == 0, result.output + data = json.loads(result.output) + assert data["proxy_mode"] == "connected_allow" + + def test_get_reflects_saved_mode(self, runner, tmp_path, monkeypatch) -> None: + monkeypatch.setenv("AUTHSOME_HOME", str(tmp_path)) + from authsome.cli.client_config import ClientConfig, save_client_config + + save_client_config(tmp_path, ClientConfig(proxy_mode="configured_deny")) + + result = runner.invoke(cli, ["--log-file", "", "config", "get", "proxy-mode"]) + assert result.exit_code == 0, result.output + data = json.loads(result.output) + assert data["proxy_mode"] == "configured_deny" + + def test_get_unknown_key_returns_error(self, runner) -> None: + result = runner.invoke(cli, ["--log-file", "", "config", "get", "unknown-key"]) + assert result.exit_code != 0 + data = json.loads(result.output) + assert data["error"] == "UnknownConfigKey" + assert "proxy-mode" in data["message"] + + +class TestConfigSet: + """Tests for `authsome config set proxy-mode `.""" + + @pytest.mark.parametrize( + "mode", + ["connected_allow", "connected_deny", "configured_allow", "configured_deny"], + ) + def test_set_all_valid_modes(self, runner, tmp_path, monkeypatch, mode) -> None: + monkeypatch.setenv("AUTHSOME_HOME", str(tmp_path)) + + result = runner.invoke(cli, ["--log-file", "", "config", "set", "proxy-mode", mode]) + assert result.exit_code == 0, result.output + data = json.loads(result.output) + assert data["status"] == "ok" + assert data["proxy_mode"] == mode + + def test_set_persists_to_disk(self, runner, tmp_path, monkeypatch) -> None: + monkeypatch.setenv("AUTHSOME_HOME", str(tmp_path)) + from authsome.cli.client_config import load_client_config + + runner.invoke(cli, ["--log-file", "", "config", "set", "proxy-mode", "connected_deny"]) + assert load_client_config(tmp_path).proxy_mode == "connected_deny" + + def test_set_invalid_mode_returns_error(self, runner) -> None: + result = runner.invoke(cli, ["--log-file", "", "config", "set", "proxy-mode", "bad_value"]) + assert result.exit_code != 0 + data = json.loads(result.output) + assert data["error"] == "InvalidProxyMode" + assert "bad_value" in data["message"] + for valid in ("connected_allow", "connected_deny", "configured_allow", "configured_deny"): + assert valid in data["message"] + + def test_set_unknown_key_returns_error(self, runner) -> None: + result = runner.invoke(cli, ["--log-file", "", "config", "set", "unknown-key", "value"]) + assert result.exit_code != 0 + data = json.loads(result.output) + assert data["error"] == "UnknownConfigKey" + assert "proxy-mode" in data["message"]