7fe0e27fec
- 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.
134 lines
3.2 KiB
Python
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"
|
|
)
|