Honor AUTH_ENABLED=false in route-level auth gate (#785)

#622 reported "I cant even paste that hash pw and granted So auth_en
=false & localbypass= true But then the host still is showing login
page?" — the operator turned auth off in .env and still gets bounced
to /login on every page load. The flow:

The auth middleware in app.py is correctly gated on AUTH_ENABLED, so
the middleware itself does not install when AUTH_ENABLED=false. The
SPA front-end at static/app.js wraps window.fetch and redirects to
/login on ANY 401 response from any API call. So all it takes for the
operator to see a login page is one route-level 401.

src/auth_helpers.require_user — the shared FastAPI dependency mounted
on ~50 routes (email, contacts, personal, …) — was the source. It is
documented as defense-in-depth in case the middleware was bypassed
unexpectedly (SSRF from a sibling service), but the implementation
treated AUTH_ENABLED=false as one of those unexpected bypasses and
401'd anyway. The loopback fall-through that would have admitted the
operator does not fire under docker compose / a reverse proxy because
the container sees the request arriving from the bridge gateway
(172.x.x.x), not 127.0.0.1.

require_user now short-circuits to "" when AUTH_ENABLED=false so the
explicit operator opt-out reaches the route layer too. While in the
file, also mirror LOCALHOST_BYPASS=true the same way for loopback
callers — the middleware already lets them through, and routes 401'ing
the same caller would produce the same /login bounce. Non-loopback
callers under LOCALHOST_BYPASS are still rejected, matching the
middleware's _is_trusted_loopback check.

Add three focused regression tests in tests/test_security_regressions.py:
docker-bridge caller is admitted under AUTH_ENABLED=false, loopback
caller is admitted under LOCALHOST_BYPASS=true, LAN caller under
LOCALHOST_BYPASS=true is still rejected. The existing
test_require_user_rejects_unauthenticated and
test_require_user_accepts_loopback_when_unconfigured tests continue to
pass because neither sets AUTH_ENABLED, so the AUTH_ENABLED=true
default path is unchanged.

Closes #622.
This commit is contained in:
tanmayraut45
2026-06-02 07:53:47 +05:30
committed by GitHub
parent 55fa223e4d
commit c1df31fda5
2 changed files with 137 additions and 7 deletions

View File

@@ -537,6 +537,104 @@ def test_require_user_accepts_loopback_when_unconfigured(monkeypatch):
assert auth_helpers.require_user(_LoopReq()) == ""
def test_require_user_accepts_anyone_when_auth_disabled(monkeypatch):
"""AUTH_ENABLED=false must let unauthenticated callers through from
any host — including the docker bridge / reverse proxy / LAN — so
the frontend's global 401 redirect doesn't bounce the user to /login
despite the operator turning auth off (issue #622)."""
monkeypatch.setenv("AUTH_ENABLED", "false")
sys.modules.pop("src.auth_helpers", None)
from src import auth_helpers # noqa: WPS433
class _State:
current_user = None
class _AppState:
class _Mgr:
# Even with a prior admin account on disk, AUTH_ENABLED=false
# must take precedence over is_configured=True.
is_configured = True
auth_manager = _Mgr()
class _App:
state = _AppState()
class _DockerClient:
host = "172.18.0.1" # docker bridge gateway, not loopback
class _Req:
state = _State()
app = _App()
client = _DockerClient()
assert auth_helpers.require_user(_Req()) == ""
def test_require_user_localhost_bypass_admits_loopback(monkeypatch):
"""LOCALHOST_BYPASS=true is the dev-only switch that admits loopback
callers without an auth cookie. require_user must mirror the auth
middleware so routes don't 401 a caller the middleware already let
through."""
monkeypatch.setenv("AUTH_ENABLED", "true")
monkeypatch.setenv("LOCALHOST_BYPASS", "true")
sys.modules.pop("src.auth_helpers", None)
from src import auth_helpers # noqa: WPS433
class _State:
current_user = None
class _AppState:
class _Mgr:
is_configured = True
auth_manager = _Mgr()
class _App:
state = _AppState()
class _LoopClient:
host = "127.0.0.1"
class _LoopReq:
state = _State()
app = _App()
client = _LoopClient()
assert auth_helpers.require_user(_LoopReq()) == ""
def test_require_user_localhost_bypass_still_rejects_lan(monkeypatch):
"""LOCALHOST_BYPASS=true must not extend to non-loopback callers —
a LAN visitor still needs to authenticate."""
from fastapi import HTTPException
monkeypatch.setenv("AUTH_ENABLED", "true")
monkeypatch.setenv("LOCALHOST_BYPASS", "true")
sys.modules.pop("src.auth_helpers", None)
from src import auth_helpers # noqa: WPS433
class _State:
current_user = None
class _AppState:
class _Mgr:
is_configured = True
auth_manager = _Mgr()
class _App:
state = _AppState()
class _LanClient:
host = "192.168.1.42"
class _LanReq:
state = _State()
app = _App()
client = _LanClient()
with pytest.raises(HTTPException) as exc:
auth_helpers.require_user(_LanReq())
assert exc.value.status_code == 401
def test_require_admin_rejects_unconfigured_public_api(monkeypatch):
"""First-run API mode must not treat "no users yet" as admin access."""
from fastapi import HTTPException