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
+47 -9
View File
@@ -54,6 +54,9 @@ class ServeControlMixin:
if self.command == "browser-cli.auth.trust":
return await self._handle_trust(msg)
if self.command == "browser-cli.auth.policy":
return await self._handle_policy(msg)
return False
async def _handle_trust(self, msg: dict) -> bool:
@@ -62,7 +65,6 @@ class ServeControlMixin:
log_request(self.addr, self.command, None, "ERROR", "no authorized keys file")
return True
from browser_cli.auth import add_authorized_key
from browser_cli.serve.security import policy_from_categories
args = msg.get("args") or {}
pubkey = str(args.get("pubkey") or "")
name = str(args.get("name") or "")
@@ -71,18 +73,54 @@ class ServeControlMixin:
await self.send_error("invalid pubkey: expected 64 lowercase hex characters")
log_request(self.addr, self.command, None, "ERROR", "invalid pubkey", identity=self.auth_label)
return True
if not await self._validate_categories(categories):
return True
added = add_authorized_key(self.auth_keys_path, pubkey, name, categories)
await self.send_ok({"added": added}, self.command)
log_request(self.addr, self.command, None, "OK" if added else "ALREADY_TRUSTED", identity=self.auth_label)
return True
async def _handle_policy(self, msg: dict) -> bool:
if self.auth_keys_path is None:
await self.send_error("no authorized keys file configured on this server")
log_request(self.addr, self.command, None, "ERROR", "no authorized keys file")
return True
from browser_cli.auth import set_authorized_key_policy
args = msg.get("args") or {}
identifier = str(args.get("identifier") or "")
categories = args.get("allow")
if not identifier.strip():
await self.send_error("missing key identifier")
log_request(self.addr, self.command, None, "ERROR", "missing identifier", identity=self.auth_label)
return True
if not await self._validate_categories(categories):
return True
try:
updated = set_authorized_key_policy(self.auth_keys_path, identifier, categories)
except ValueError as exc:
await self.send_error(str(exc))
log_request(self.addr, self.command, None, "ERROR", "ambiguous key", identity=self.auth_label)
return True
if updated is None:
await self.send_error(f"trusted key not found: {identifier}")
log_request(self.addr, self.command, None, "ERROR", "key not found", identity=self.auth_label)
return True
pubkey, name = updated
await self.send_ok({"updated": True, "pubkey": pubkey, "name": name, "allow": categories}, self.command)
log_request(self.addr, self.command, None, "OK", identity=self.auth_label)
return True
async def _validate_categories(self, categories) -> bool:
if categories is not None and not isinstance(categories, list):
await self.send_error("invalid allow: expected a list of category strings")
log_request(self.addr, self.command, None, "ERROR", "invalid allow", identity=self.auth_label)
return False
if categories is not None:
if not isinstance(categories, list):
await self.send_error("invalid allow: expected a list of category strings")
log_request(self.addr, self.command, None, "ERROR", "invalid allow", identity=self.auth_label)
return True
from browser_cli.serve.security import policy_from_categories
try:
policy_from_categories(categories) # validate before persisting
except ValueError as exc:
await self.send_error(str(exc))
log_request(self.addr, self.command, None, "ERROR", "invalid allow category", identity=self.auth_label)
return True
added = add_authorized_key(self.auth_keys_path, pubkey, name, categories)
await self.send_ok({"added": added}, self.command)
log_request(self.addr, self.command, None, "OK" if added else "ALREADY_TRUSTED", identity=self.auth_label)
return False
return True