Files
Rohit MahtoandCommandCodeBot e43313b9f4 feat: password-protect file manager and terminal (0.6.1)
- New password_auth module: random token generated on first run, encrypted
  at rest under ~/.champdev (Fernet, cryptography dep), verify + 12h signed
  session cookie. CHAMPDEV_AUTH_PASSWORD env overrides the generated token
  and is what gets sent to telemetry.
- File Manager: write actions (upload/delete/rename/move/copy/mkdir) hidden
  behind an in-UI Unlock dialog and enforced server-side (401). While locked,
  browsing is clamped to the ComfyUI root instead of 403ing, with a lock
  banner. Unlock reveals write actions and allows browsing above the root.
- Terminal: websocket refuses to spawn a shell without auth; lock overlay
  with Unlock dialog, reconnects on success.
- Telemetry: token included in the ingest payload (server column exists).
- Frontend: shared champdev_auth.js in-UI password modal (window.prompt was
  silently blocked in embedded contexts).

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
2026-08-23 19:51:42 +05:30

102 lines
4.4 KiB
Python

import time
import password_auth
def test_ensure_token_creates_and_persists(tmp_path, monkeypatch):
monkeypatch.setattr(password_auth, "BASE_DIR", str(tmp_path))
monkeypatch.setattr(password_auth, "TOKEN_FILE", str(tmp_path / "env"))
monkeypatch.setattr(password_auth, "KEY_FILE", str(tmp_path / "secret_key"))
token = password_auth.ensure_token()
assert token and len(token) >= 20
# A second call returns the same token (persisted, not regenerated).
assert password_auth.ensure_token() == token
assert password_auth.get_token() == token
def test_token_file_does_not_contain_plaintext(tmp_path, monkeypatch):
monkeypatch.setattr(password_auth, "BASE_DIR", str(tmp_path))
monkeypatch.setattr(password_auth, "TOKEN_FILE", str(tmp_path / "env"))
monkeypatch.setattr(password_auth, "KEY_FILE", str(tmp_path / "secret_key"))
token = password_auth.ensure_token()
raw = (tmp_path / "env").read_text(encoding="utf-8")
assert token not in raw
def test_verify_password(tmp_path, monkeypatch):
monkeypatch.setattr(password_auth, "BASE_DIR", str(tmp_path))
monkeypatch.setattr(password_auth, "TOKEN_FILE", str(tmp_path / "env"))
monkeypatch.setattr(password_auth, "KEY_FILE", str(tmp_path / "secret_key"))
token = password_auth.ensure_token()
assert password_auth.verify_password(token) is True
assert password_auth.verify_password("wrong") is False
assert password_auth.verify_password("") is False
assert password_auth.verify_password(None) is False
def test_fails_closed_without_token(tmp_path, monkeypatch):
monkeypatch.setattr(password_auth, "BASE_DIR", str(tmp_path))
monkeypatch.setattr(password_auth, "TOKEN_FILE", str(tmp_path / "env"))
monkeypatch.setattr(password_auth, "KEY_FILE", str(tmp_path / "secret_key"))
assert password_auth.get_token() is None
assert password_auth.verify_password("anything") is False
assert password_auth._session_valid("123.abc") is False
def test_session_cookie_roundtrip(tmp_path, monkeypatch):
monkeypatch.setattr(password_auth, "BASE_DIR", str(tmp_path))
monkeypatch.setattr(password_auth, "TOKEN_FILE", str(tmp_path / "env"))
monkeypatch.setattr(password_auth, "KEY_FILE", str(tmp_path / "secret_key"))
token = password_auth.ensure_token()
cookie = password_auth._mint_session()
assert cookie and "." in cookie
assert password_auth._session_valid(cookie) is True
assert password_auth._session_valid("tampered." + cookie.split(".", 1)[1]) is False
assert password_auth._session_valid(cookie.split(".", 1)[0] + ".badmac") is False
def test_session_cookie_expiry(tmp_path, monkeypatch):
monkeypatch.setattr(password_auth, "BASE_DIR", str(tmp_path))
monkeypatch.setattr(password_auth, "TOKEN_FILE", str(tmp_path / "env"))
monkeypatch.setattr(password_auth, "KEY_FILE", str(tmp_path / "secret_key"))
password_auth.ensure_token()
cookie = password_auth._mint_session()
now = time.time() + password_auth.SESSION_TTL_SEC * 2
assert password_auth._session_valid(cookie, now=now) is False
def test_env_override_is_effective_password(tmp_path, monkeypatch):
monkeypatch.setattr(password_auth, "BASE_DIR", str(tmp_path))
monkeypatch.setattr(password_auth, "TOKEN_FILE", str(tmp_path / "env"))
monkeypatch.setattr(password_auth, "KEY_FILE", str(tmp_path / "secret_key"))
monkeypatch.setenv("CHAMPDEV_AUTH_PASSWORD", "op-secret")
# The override is what ensure_token returns (sent to telemetry)...
assert password_auth.ensure_token() == "op-secret"
# ...no token file is generated...
assert not (tmp_path / "env").exists()
# ...and verification + session signing use it.
assert password_auth.verify_password("op-secret") is True
assert password_auth.verify_password("generated") is False
cookie = password_auth._mint_session()
assert password_auth._session_valid(cookie) is True
def test_env_override_fallback_to_generated(tmp_path, monkeypatch):
monkeypatch.setattr(password_auth, "BASE_DIR", str(tmp_path))
monkeypatch.setattr(password_auth, "TOKEN_FILE", str(tmp_path / "env"))
monkeypatch.setattr(password_auth, "KEY_FILE", str(tmp_path / "secret_key"))
monkeypatch.delenv("CHAMPDEV_AUTH_PASSWORD", raising=False)
token = password_auth.ensure_token()
assert token and len(token) >= 20
assert (tmp_path / "env").exists()
assert password_auth.verify_password(token) is True