auth providers
This commit is contained in:
@@ -94,6 +94,8 @@ SOCKETIO_CORS_ALLOWED_ORIGINS = None if not _SOCKETIO_CORS else [item.strip() fo
|
||||
# Note: API origin checks are separate from Socket.IO CORS. When unset, reuse the Socket.IO allowlist for operator-friendly reverse proxy setups.
|
||||
_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")}
|
||||
|
||||
TRAFFIC_HISTORY_RETENTION_DAYS = _env_int("PYTORRENT_TRAFFIC_HISTORY_RETENTION_DAYS", 90, 1)
|
||||
JOBS_RETENTION_DAYS = _env_int("PYTORRENT_JOBS_RETENTION_DAYS", 30, 1)
|
||||
|
||||
@@ -6,7 +6,7 @@ import secrets
|
||||
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from flask import abort, g, jsonify, redirect, request, session, url_for
|
||||
from flask import abort, g, has_request_context, jsonify, redirect, request, session, url_for
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
|
||||
from ..config import (
|
||||
@@ -17,6 +17,7 @@ from ..config import (
|
||||
AUTH_PROXY_AUTO_CREATE_ROLE,
|
||||
AUTH_PROXY_USER_HEADER,
|
||||
API_ALLOWED_ORIGINS,
|
||||
AUTH_BYPASS_HOSTS,
|
||||
)
|
||||
from ..db import connect, default_user_id, utcnow
|
||||
|
||||
@@ -67,8 +68,22 @@ def password_hash(password: str) -> str:
|
||||
return generate_password_hash(password or "")
|
||||
|
||||
|
||||
def _host_matches_bypass(host: str) -> bool:
|
||||
clean = str(host or "").strip().lower()
|
||||
if not clean:
|
||||
return False
|
||||
return clean in AUTH_BYPASS_HOSTS or clean.split(":", 1)[0] in AUTH_BYPASS_HOSTS
|
||||
|
||||
|
||||
def auth_bypassed_request() -> bool:
|
||||
# Note: Allows trusted direct-IP access to keep auth enabled for reverse-proxy traffic.
|
||||
if not enabled() or not AUTH_BYPASS_HOSTS or not has_request_context():
|
||||
return False
|
||||
return _host_matches_bypass(request.host)
|
||||
|
||||
|
||||
def current_user_id() -> int:
|
||||
if not enabled():
|
||||
if not enabled() or auth_bypassed_request():
|
||||
return default_user_id()
|
||||
api_user_id = getattr(g, "api_user_id", None)
|
||||
if api_user_id:
|
||||
@@ -361,9 +376,25 @@ def authenticate_external_user() -> dict[str, Any] | None:
|
||||
if not user or not int(user.get("is_active") or 0):
|
||||
return None
|
||||
g.external_user_id = int(user["id"])
|
||||
session["user_id"] = int(user["id"])
|
||||
session["username"] = user.get("username")
|
||||
session["role"] = user.get("role") or "user"
|
||||
return _public_user(user)
|
||||
|
||||
|
||||
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():
|
||||
return default_user_id()
|
||||
uid = current_user_id()
|
||||
if uid:
|
||||
return uid
|
||||
if uses_external_provider():
|
||||
authenticate_external_user()
|
||||
return current_user_id()
|
||||
|
||||
|
||||
def logout_user() -> None:
|
||||
session.clear()
|
||||
|
||||
@@ -595,6 +626,8 @@ def install_guards(app) -> None:
|
||||
if not enabled():
|
||||
return None
|
||||
g.api_token_authenticated = False
|
||||
if auth_bypassed_request():
|
||||
return None
|
||||
if request.path.startswith("/api/"):
|
||||
token_user = authenticate_api_token(_request_api_token())
|
||||
if token_user:
|
||||
|
||||
@@ -217,7 +217,7 @@ def register_socketio_handlers(socketio):
|
||||
@socketio.on("connect")
|
||||
def handle_connect():
|
||||
ensure_poller_started()
|
||||
if auth.enabled() and not auth.current_user_id():
|
||||
if auth.enabled() and not auth.ensure_request_user():
|
||||
disconnect()
|
||||
return False
|
||||
profile = active_profile()
|
||||
@@ -234,7 +234,7 @@ def register_socketio_handlers(socketio):
|
||||
|
||||
@socketio.on("select_profile")
|
||||
def handle_select_profile(data):
|
||||
if auth.enabled() and not auth.current_user_id():
|
||||
if auth.enabled() and not auth.ensure_request_user():
|
||||
disconnect()
|
||||
return
|
||||
old_profile = active_profile()
|
||||
|
||||
Reference in New Issue
Block a user