feat(discord-bridge): dashboard fara token (DASHBOARD_AUTH=off)

Cerut explicit: tokenul nu se retine. Cu DASHBOARD_AUTH=off nu mai exista login,
/login.html duce inapoi la panou, iar butonul "Iesi" dispare.

Se sprijina pe doua lucruri si nu are sens fara ele: serviciul e legat de
127.0.0.1, deci din retea ajunge la el doar tailscaled; iar tailscale serve il
publica tainet only, unde accesul e deja autentificat de Tailscale.

Compensatie partiala pentru ce se pierde: fiecare start/stop/restart se scrie in
logs/dashboard.log cu identitatea din antetul Tailscale-User-Login pus de
tailscale serve (verificat: ajunge pana la noi). Antetul e DOAR pentru jurnal —
nu decide accesul, fiindca un proces local l-ar putea fabrica.

Implicitul ramane cu token: doar off/none/0/false scot login-ul, orice alta
valoare il pastreaza (are test).

Sase teste noi, 41 pe dashboard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B29CApsP1JkSdjYaGaHpE7
This commit is contained in:
Claude Agent
2026-08-30 14:16:20 +00:00
parent e203ee021d
commit b14e7bc50a
6 changed files with 126 additions and 11 deletions

View File

@@ -90,11 +90,24 @@ def reset_token_cache() -> None:
_TOKEN = None
def auth_disabled() -> bool:
"""`DASHBOARD_AUTH=off` in `~/.claude-discord/env` scoate complet login-ul.
Alegere constienta a operatorului, nu implicit: panoul poate opri un agent cu
`bypassPermissions` si chei SSH catre tot clusterul, deci fara token oricine
ajunge la port il poate folosi. Are sens doar pentru ca serviciul e legat de
`127.0.0.1` si publicat exclusiv in tailnet (`tailscale serve`, tainet only),
unde identitatea o face deja Tailscale.
"""
return (config.get("DASHBOARD_AUTH") or "").strip().lower() in ("off", "none", "0", "false")
def dashboard_token() -> str:
"""Tokenul de acces, din `~/.claude-discord/env` (`DASHBOARD_TOKEN`).
Lipsa lui NU deschide dashboard-ul: se genereaza unul aleator per proces si se
tipareste in log, deci ramane accesibil doar cui poate citi logul.
tipareste in log, deci ramane accesibil doar cui poate citi logul. Ca sa fie
deschis intentionat, se pune `DASHBOARD_AUTH=off`.
"""
global _TOKEN
if _TOKEN is None:
@@ -110,6 +123,13 @@ def dashboard_token() -> str:
return _TOKEN
def tailnet_user(headers) -> str:
"""Cine e, dupa Tailscale. `tailscale serve` pune antetul pe cererile din
tailnet; pe localhost lipseste. Doar pentru jurnal — nu e folosit ca decizie
de acces, fiindca un proces local ar putea sa-l fabrice."""
return (headers.get("Tailscale-User-Login") or "").strip()
# ── systemd ─────────────────────────────────────────────────────────────
def _sysctl(*args: str, timeout: float = 30.0) -> subprocess.CompletedProcess:
return subprocess.run(
@@ -391,6 +411,8 @@ class Handler(SimpleHTTPRequestHandler):
return {}
def authed(self) -> bool:
if auth_disabled():
return True
got = _parse_cookies(self.headers.get("Cookie", "")).get(COOKIE_NAME, "")
return bool(got) and secrets.compare_digest(got, dashboard_token())
@@ -429,6 +451,12 @@ class Handler(SimpleHTTPRequestHandler):
if path in ("/", "/index.html"):
return self.send_html("index.html")
if path == "/login.html":
if auth_disabled():
self.send_response(302)
self.send_header("Location", mount_prefix() + "/")
self.send_header("Content-Length", "0")
self.end_headers()
return None
return self.send_html("login.html")
self.path = path
return super().do_GET()
@@ -474,6 +502,8 @@ class Handler(SimpleHTTPRequestHandler):
"cost": state.get("cost") or {},
"cost_cap": float(config.get("COST_CAP_USD_DAY") or 0),
"pending_approvals": len(pending_approvals()),
"auth": not auth_disabled(),
"user": tailnet_user(self.headers),
"now": time.time(),
})
if path == "/api/logs":
@@ -548,6 +578,9 @@ class Handler(SimpleHTTPRequestHandler):
"hint": "retrimite cu force=true ca sa le intrerupi",
}, 409)
who = tailnet_user(self.headers) or "local"
log_line = f"[actiune] {action} pe {SERVICE}, cerut de {who}"
print(log_line, file=sys.stderr, flush=True)
try:
r = _sysctl(action, SERVICE)
except subprocess.TimeoutExpired: