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.
This commit is contained in:
@@ -24,6 +24,7 @@ from browser_cli.auth.keys import (
|
||||
load_authorized_keys_with_policies,
|
||||
load_private_key,
|
||||
public_key_hex,
|
||||
set_authorized_key_policy,
|
||||
)
|
||||
from browser_cli.auth.pq import (
|
||||
new_nonce,
|
||||
@@ -66,6 +67,7 @@ __all__ = [
|
||||
"pq_kex_server_decapsulate",
|
||||
"pq_kex_server_keypair",
|
||||
"public_key_hex",
|
||||
"set_authorized_key_policy",
|
||||
"sign",
|
||||
"verify",
|
||||
]
|
||||
|
||||
@@ -89,3 +89,37 @@ def add_authorized_key(path: Path, pub_hex: str, name: str = "", categories: lis
|
||||
with open(path, "a", encoding="utf-8") as file:
|
||||
file.write(line)
|
||||
return True
|
||||
|
||||
def set_authorized_key_policy(path: Path, identifier: str, categories: list[str] | None) -> tuple[str, str] | None:
|
||||
"""Update the per-key policy for a trusted key.
|
||||
|
||||
``identifier`` may be the full public key or an exact key name. ``categories``
|
||||
is written as the ``allow:`` token; ``None`` removes the token so the key uses
|
||||
the server default. Returns ``(pubkey, name)`` for the updated key, ``None`` if
|
||||
no key matched, and raises ``ValueError`` for ambiguous names.
|
||||
"""
|
||||
if not path.exists():
|
||||
return None
|
||||
|
||||
wanted = identifier.strip()
|
||||
lines = path.read_text(encoding="utf-8").splitlines(keepends=True)
|
||||
matches: list[tuple[int, str, str, str]] = []
|
||||
|
||||
for index, line in enumerate(lines):
|
||||
parsed = _parse_authorized_line(line)
|
||||
if parsed is None:
|
||||
continue
|
||||
pubkey, name, _cats = parsed
|
||||
if pubkey.lower() == wanted.lower() or (name and name == wanted):
|
||||
newline = "\n" if line.endswith("\n") else ""
|
||||
matches.append((index, pubkey, name, newline))
|
||||
|
||||
if not matches:
|
||||
return None
|
||||
if len(matches) > 1:
|
||||
raise ValueError(f"ambiguous key name: {identifier!r} matches {len(matches)} keys")
|
||||
|
||||
index, pubkey, name, newline = matches[0]
|
||||
lines[index] = format_authorized_line(pubkey, name, categories) + newline
|
||||
path.write_text("".join(lines), encoding="utf-8")
|
||||
return pubkey, name
|
||||
|
||||
Reference in New Issue
Block a user