From 109811c0245f912ec6f7c3abe386ffdadc00cc96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 25 May 2026 10:22:14 +0200 Subject: [PATCH] bypass profile select --- .env.example | 1 + auth.md | 11 +++++++++-- pytorrent/config.py | 2 ++ pytorrent/services/auth.py | 24 ++++++++++++++++++++++-- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index 749a235..e747d74 100644 --- a/.env.example +++ b/.env.example @@ -68,3 +68,4 @@ PYTORRENT_SESSION_COOKIE_SECURE=false # bypass auth on specific hosts (ex. local ip) PYTORRENT_AUTH_BYPASS_HOSTS=10.11.1.11:8090,10.11.1.11 +PYTORRENT_AUTH_BYPASS_USER=admin \ No newline at end of file diff --git a/auth.md b/auth.md index be89c46..9747be2 100644 --- a/auth.md +++ b/auth.md @@ -35,6 +35,9 @@ PYTORRENT_AUTH_PROXY_AUTO_CREATE_PERMISSION=rw # Optional: trusted direct-IP/local hosts that should skip pyTorrent auth. # Use this only on private networks, never on public proxy hostnames. PYTORRENT_AUTH_BYPASS_HOSTS=10.11.1.11:8090,10.11.1.11 + +# Existing active user used by bypassed requests. Defaults to admin. +PYTORRENT_AUTH_BYPASS_USER=admin ``` @@ -137,15 +140,19 @@ Example: PYTORRENT_AUTH_ENABLE=true PYTORRENT_AUTH_PROVIDER=tinyauth PYTORRENT_AUTH_BYPASS_HOSTS=10.11.1.11:8090,10.11.1.11 + +# Existing active user used by bypassed requests. Defaults to admin. +PYTORRENT_AUTH_BYPASS_USER=admin ``` Behavior: - requests with `Host: 10.11.1.11:8090` or `Host: 10.11.1.11` use the built-in default admin user; - requests through the reverse proxy still require the configured auth provider; -- profile permissions are ignored for bypassed direct-IP requests because they run as the default admin user; +- `PYTORRENT_AUTH_BYPASS_USER` must point to an existing active user; when unset, pyTorrent uses `admin`; +- if the bypass user is `admin`, profile permissions are ignored because admins can access all profiles; - when no active profile is saved for the bypass user, pyTorrent opens the profile picker instead of silently selecting the first profile; -- after selecting a profile, the choice is saved in the default user's preferences and reused on the next direct-IP visit. +- after selecting a profile, the choice is saved in the bypass user's preferences and reused on the next direct-IP visit. Do not add public domains to this list. diff --git a/pytorrent/config.py b/pytorrent/config.py index fe713c6..b0c35ec 100644 --- a/pytorrent/config.py +++ b/pytorrent/config.py @@ -96,6 +96,8 @@ _API_ALLOWED_ORIGINS = _env_csv("PYTORRENT_API_ALLOWED_ORIGINS") API_ALLOWED_ORIGINS = _API_ALLOWED_ORIGINS or _env_csv("PYTORRENT_SOCKETIO_CORS_ALLOWED_ORIGINS") # Note: Optional auth bypass for trusted direct-IP/local access. Values can be hosts or host:port pairs. AUTH_BYPASS_HOSTS = {item.lower() for item in _env_csv("PYTORRENT_AUTH_BYPASS_HOSTS")} +# Note: Trusted auth-bypass requests act as this existing active user. +AUTH_BYPASS_USER = os.getenv("PYTORRENT_AUTH_BYPASS_USER", "admin").strip() or "admin" TRAFFIC_HISTORY_RETENTION_DAYS = _env_int("PYTORRENT_TRAFFIC_HISTORY_RETENTION_DAYS", 90, 1) JOBS_RETENTION_DAYS = _env_int("PYTORRENT_JOBS_RETENTION_DAYS", 30, 1) diff --git a/pytorrent/services/auth.py b/pytorrent/services/auth.py index 1e26f62..8283127 100644 --- a/pytorrent/services/auth.py +++ b/pytorrent/services/auth.py @@ -18,6 +18,7 @@ from ..config import ( AUTH_PROXY_USER_HEADER, API_ALLOWED_ORIGINS, AUTH_BYPASS_HOSTS, + AUTH_BYPASS_USER, ) from ..db import connect, default_user_id, utcnow @@ -82,9 +83,26 @@ def auth_bypassed_request() -> bool: return _host_matches_bypass(request.host) + +def bypass_user_id() -> int: + """Return the configured active user id used for trusted auth-bypass requests.""" + username = str(AUTH_BYPASS_USER or "admin").strip() or "admin" + with connect() as conn: + row = conn.execute("SELECT id FROM users WHERE username=? AND is_active=1", (username,)).fetchone() + if row: + return int(row["id"]) + # Note: Keep direct-IP access usable after old installs, but never choose an inactive fallback. + row = conn.execute("SELECT id FROM users WHERE username='admin' AND is_active=1").fetchone() + if row: + return int(row["id"]) + row = conn.execute("SELECT id FROM users WHERE id=? AND is_active=1", (default_user_id(),)).fetchone() + return int(row["id"]) if row else 0 + def current_user_id() -> int: - if not enabled() or auth_bypassed_request(): + if not enabled(): return default_user_id() + if auth_bypassed_request(): + return bypass_user_id() api_user_id = getattr(g, "api_user_id", None) if api_user_id: return int(api_user_id) @@ -385,8 +403,10 @@ def authenticate_external_user() -> dict[str, Any] | None: def ensure_request_user() -> int: # Note: Socket.IO events do not go through Flask before_request like normal REST calls, # so external proxy auth must be resolved explicitly during the Socket.IO handshake/events. - if not enabled() or auth_bypassed_request(): + if not enabled(): return default_user_id() + if auth_bypassed_request(): + return bypass_user_id() uid = current_user_id() if uid: return uid