Files
browser-cli/browser_cli/command_security.py
T
daniel156161 7fe0e27fec
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
feat(auth): add interactive key policy editing
- 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.
2026-06-18 15:02:18 +02:00

134 lines
3.2 KiB
Python

"""Safety policy for generic command execution surfaces.
Dedicated first-party CLI/SDK methods keep their normal behavior. This module
only gates raw surfaces where a single string can trigger arbitrary browser
capabilities: ``browser-cli command``, ``browser-cli script``, and the HTTP
``/command`` endpoint.
"""
from __future__ import annotations
from dataclasses import dataclass
SAFE_COMMANDS = {
"browser-cli.targets",
"clients.list",
"extension.capabilities",
"extension.info",
"group.list",
"group.query",
"group.tabs",
"page.info",
"perf.status",
"tabs.active_in_window",
"tabs.count",
"tabs.filter",
"tabs.list",
"tabs.query",
"tabs.status",
"windows.list",
}
READ_PAGE_COMMANDS = {
"dom.attr",
"dom.exists",
"dom.query",
"dom.text",
"extract.html",
"extract.images",
"extract.json",
"extract.links",
"extract.markdown",
"extract.text",
"tabs.html",
}
CONTROL_PREFIXES = (
"navigate.",
"nav.",
"group.",
"session.",
"tabs.",
"windows.",
)
CONTROL_COMMANDS = {
"dom.check",
"dom.clear",
"dom.click",
"dom.focus",
"dom.hover",
"dom.key",
"dom.poll",
"dom.scroll",
"dom.select",
"dom.submit",
"dom.type",
"dom.uncheck",
"dom.wait_for",
"extension.reload",
}
DANGEROUS_COMMANDS = {
"dom.eval",
"tabs.screenshot",
}
DANGEROUS_PREFIXES = (
"storage.",
)
# Server-side key-management control commands. Gated separately so a key can be
# trusted for browser use without also being able to list or add trusted keys.
KEY_COMMANDS = {
"browser-cli.auth.keys",
"browser-cli.auth.trust",
"browser-cli.auth.policy",
}
@dataclass(frozen=True)
class CommandPolicy:
allow_read_page: bool = False
allow_control: bool = False
allow_dangerous: bool = False
allow_keys: bool = False
@classmethod
def unrestricted(cls) -> "CommandPolicy":
return cls(allow_read_page=True, allow_control=True, allow_dangerous=True, allow_keys=True)
def _is_control(command: str) -> bool:
if command in CONTROL_COMMANDS:
return True
if any(command.startswith(prefix) for prefix in CONTROL_PREFIXES):
return command not in SAFE_COMMANDS and command not in READ_PAGE_COMMANDS and command not in DANGEROUS_COMMANDS
return False
def command_category(command: str) -> str:
name = str(command or "")
if name in KEY_COMMANDS:
return "keys"
if name in DANGEROUS_COMMANDS or any(name.startswith(prefix) for prefix in DANGEROUS_PREFIXES):
return "dangerous"
if name in READ_PAGE_COMMANDS:
return "read-page"
if name in SAFE_COMMANDS:
return "safe"
if _is_control(name):
return "control"
return "unknown"
def assert_command_allowed(command: str, policy: CommandPolicy) -> None:
category = command_category(command)
if category == "safe":
return
if category == "read-page" and policy.allow_read_page:
return
if category == "control" and policy.allow_control:
return
if category == "dangerous" and policy.allow_dangerous:
return
if category == "keys" and policy.allow_keys:
return
raise PermissionError(
f"Raw command '{command}' is {category} and blocked by default; "
"use --allow-read-page, --allow-control, --allow-dangerous, or --allow-keys explicitly"
)