feat(auth): add interactive key policy editing
Testing / remote-protocol-compat (0.9.3) (push) Successful in 46s
Testing / remote-protocol-compat (0.9.5) (push) Successful in 47s
Testing / test (push) Successful in 36s

- Add auth policy to update existing authorized_keys allow policies locally or over remote serve.
- Support key lookup by public key or exact name, with safe, all, server-default, and category-based modes.
- Add questionary-powered interactive key selection and checkbox policy editing with current policy preselected.
- Show policy descriptions in auth keys output so each capability is easier to understand.
- Gate the new remote control command behind the existing keys policy category and include protocol routing/compat updates.
- Bump real-browser-cli to 0.16.2 and lock the new questionary dependency.
- Cover local, remote, validation, and policy-category behavior in tests.
This commit is contained in:
2026-06-18 15:02:18 +02:00
parent 6fa931aa36
commit 7fe0e27fec
11 changed files with 454 additions and 16 deletions
+68
View File
@@ -237,9 +237,77 @@ def test_auth_keys_local_shows_policy_column(tmp_path):
result = CliRunner().invoke(main, ["auth", "keys", "--file", str(keys)])
assert result.exit_code == 0
assert "Policy" in result.output
assert "Description" in result.output
assert "read-page" in result.output
assert "all" in result.output
assert "server default" in result.output
assert "read page content" in result.output
assert "Full access" in result.output
def test_auth_policy_updates_existing_key_policy(tmp_path):
keys = tmp_path / "authorized_keys"
pub = "a" * 64
keys.write_text(f"{pub} YubiKey 5C NFC FIPS\n")
result = CliRunner().invoke(main, [
"auth", "policy", pub, "--file", str(keys), "--allow-read-page", "--allow-control",
])
assert result.exit_code == 0
assert keys.read_text().strip() == f"{pub} YubiKey 5C NFC FIPS allow:read-page,control"
assert "Updated policy" in result.output
def test_auth_policy_can_set_safe_and_server_default_by_name(tmp_path):
keys = tmp_path / "authorized_keys"
pub = "b" * 64
keys.write_text(f"{pub} laptop allow:all\n")
safe_result = CliRunner().invoke(main, ["auth", "policy", "laptop", "--file", str(keys), "--safe"])
assert safe_result.exit_code == 0
assert keys.read_text().strip() == f"{pub} laptop allow:"
default_result = CliRunner().invoke(main, ["auth", "policy", "laptop", "--file", str(keys), "--server-default"])
assert default_result.exit_code == 0
assert keys.read_text().strip() == f"{pub} laptop"
def test_auth_policy_requires_policy_mode_when_not_interactive(tmp_path):
keys = tmp_path / "authorized_keys"
keys.write_text(f"{'a' * 64} laptop\n")
result = CliRunner().invoke(main, ["auth", "policy", "laptop", "--file", str(keys)])
assert result.exit_code == 1
assert "Choose a policy mode" in result.output
def test_auth_policy_rejects_conflicting_policy_modes(tmp_path):
keys = tmp_path / "authorized_keys"
keys.write_text(f"{'a' * 64} laptop\n")
result = CliRunner().invoke(main, ["auth", "policy", "laptop", "--file", str(keys), "--safe", "--allow-all"])
assert result.exit_code == 1
assert "Choose exactly one policy mode" in result.output
def test_parse_interactive_policy_selection():
from browser_cli.commands.auth import _parse_checkbox_policy_selection, _parse_policy_selection
assert _parse_policy_selection("1,2") == ["read-page", "control"]
assert _parse_policy_selection("read-page control") == ["read-page", "control"]
assert _parse_policy_selection("all") == ["all"]
assert _parse_policy_selection("safe") == []
assert _parse_policy_selection("default") is None
assert _parse_checkbox_policy_selection(["read-page", "control"]) == ["read-page", "control"]
assert _parse_checkbox_policy_selection(["__all__"]) == ["all"]
assert _parse_checkbox_policy_selection(["__safe__"]) == []
assert _parse_checkbox_policy_selection(["__server_default__"]) is None
def test_auth_policy_without_identifier_requires_interactive_picker():
result = CliRunner().invoke(main, ["auth", "policy", "--allow-all"])
assert result.exit_code == 1
assert "Missing key identifier" in result.output
def test_auth_policy_remote_sends_policy_command():
pub = "c" * 64
with patch("browser_cli.client.send_command", return_value={"pubkey": pub, "name": "remote key", "allow": ["all"]}) as send:
result = CliRunner().invoke(main, ["--remote", "browser-host.example:8765", "auth", "policy", pub, "--allow-all"])
assert result.exit_code == 0
send.assert_called_once()
assert send.call_args.kwargs["args"] == {"identifier": pub, "allow": ["all"]}
assert "Updated policy" in result.output
def test_auth_keys_remote_unreachable_clean_error():
"""`auth keys --remote` on an unreachable host shows a clean error, not a traceback."""