Canalul web trece de la 100% deschis (hardcodat cont 1) la autentificat si multi-tenant. Un service nou se inregistreaza din browser, primeste o cheie API (o singura data) si o sesiune; contul se creeaza "in asteptare" (active=0) si nu trimite la RAR pana la activarea de catre admin (tools/account.py activate). - users + app/users.py: parole scrypt (salt per-user, eticheta parametri onorata la verify pentru migrare cost), email unic case-insensitive - sesiune: SessionMiddleware (same_site=strict, https_only config) + app/web/session.py (current_account/web_account/require_login->LoginRequired, set_session clear-inainte) - CSRF (app/web/csrf.py) enforce in prod inclusiv pe login/signup + rate-limit in-proces (app/web/ratelimit.py) pe signup si login - signup/login/logout (app/web/auth_routes.py): signup tranzactie atomica, cheie-o-data, log SIGNUP pentru descoperire admin - dashboard + import scoped pe contul sesiunii (regula NULL->cont 1); toate rutele web care ating date sensibile sub require_login; nomenclator ramane global - banner "cont in asteptare" pentru conturi active=0 - gate worker: claim_one LEFT JOIN accounts COALESCE(active,1)=1 (account_id NULL=activ) VERIFY context curat (2 runde): leak cross-account /_fragments/mapari prins+reparat. /code-review high: csrf_token lipsa pe re-randari de eroare, scrypt_params ignorat, login fara rate-limit -- toate reparate. 361 teste pass (de la 313). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
150 lines
5.1 KiB
Python
150 lines
5.1 KiB
Python
"""Teste US-002 (PRD 3.3): SessionMiddleware + helper-e sesiune in app/web/session.py."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
|
|
import pytest
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.responses import JSONResponse
|
|
from starlette.middleware.sessions import SessionMiddleware
|
|
from starlette.testclient import TestClient
|
|
|
|
|
|
# ---- App minimal pentru teste (fara init_db, fara DB) ----
|
|
|
|
def _make_app(web_auth_required: bool = True, session_secret: str = "test-secret-dev") -> FastAPI:
|
|
"""Construieste un app FastAPI minimal cu SessionMiddleware + rute de test."""
|
|
mini = FastAPI()
|
|
mini.add_middleware(
|
|
SessionMiddleware,
|
|
secret_key=session_secret,
|
|
session_cookie="autopass_session",
|
|
https_only=False,
|
|
same_site="strict",
|
|
)
|
|
|
|
@mini.get("/set-session")
|
|
def set_sess(request: Request, account_id: int = 1, user_id: int = 1):
|
|
from app.web.session import set_session
|
|
set_session(request, account_id, user_id)
|
|
return {"ok": True}
|
|
|
|
@mini.get("/get-session")
|
|
def get_sess(request: Request):
|
|
from app.web.session import current_account, current_user_id
|
|
return {
|
|
"account_id": current_account(request),
|
|
"user_id": current_user_id(request),
|
|
}
|
|
|
|
@mini.get("/protected")
|
|
def protected(request: Request):
|
|
from app.web.session import require_login
|
|
aid = require_login(request)
|
|
return {"account_id": aid}
|
|
|
|
@mini.get("/logout")
|
|
def logout_ep(request: Request):
|
|
from app.web.session import clear_session
|
|
clear_session(request)
|
|
return {"ok": True}
|
|
|
|
@mini.get("/web-account")
|
|
def web_account_ep(request: Request):
|
|
from app.web.session import web_account
|
|
return {"account_id": web_account(request)}
|
|
|
|
# Handler pentru LoginRequired
|
|
from app.web.session import LoginRequired
|
|
from starlette.responses import RedirectResponse
|
|
|
|
@mini.exception_handler(LoginRequired)
|
|
async def login_required_handler(request: Request, exc: LoginRequired):
|
|
return RedirectResponse("/login", status_code=303)
|
|
|
|
return mini
|
|
|
|
|
|
@pytest.fixture()
|
|
def client_auth(monkeypatch):
|
|
"""Client cu web_auth_required=True."""
|
|
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "true")
|
|
from app.config import get_settings
|
|
get_settings.cache_clear()
|
|
app = _make_app(web_auth_required=True)
|
|
with TestClient(app, follow_redirects=False) as c:
|
|
yield c
|
|
get_settings.cache_clear()
|
|
|
|
|
|
@pytest.fixture()
|
|
def client_dev(monkeypatch):
|
|
"""Client cu web_auth_required=False (dev bypass)."""
|
|
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false")
|
|
from app.config import get_settings
|
|
get_settings.cache_clear()
|
|
app = _make_app(web_auth_required=False)
|
|
with TestClient(app, follow_redirects=False) as c:
|
|
yield c
|
|
get_settings.cache_clear()
|
|
|
|
|
|
def test_ruta_protejata_redirect_login(client_auth):
|
|
"""Fara sesiune si web_auth_required=True -> 303 redirect catre /login."""
|
|
resp = client_auth.get("/protected")
|
|
assert resp.status_code == 303
|
|
assert resp.headers["location"] == "/login"
|
|
|
|
|
|
def test_sesiune_seteaza_si_citeste_cont(client_auth):
|
|
"""set_session stocheaza account_id si user_id; current_account/current_user_id le citesc."""
|
|
client_auth.get("/set-session?account_id=5&user_id=7")
|
|
resp = client_auth.get("/get-session")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["account_id"] == 5
|
|
assert data["user_id"] == 7
|
|
|
|
|
|
def test_logout_curata_sesiunea(client_auth):
|
|
"""clear_session sterge account_id si user_id din sesiune."""
|
|
client_auth.get("/set-session?account_id=3&user_id=4")
|
|
# Verifica ca sesiunea e setata
|
|
data_before = client_auth.get("/get-session").json()
|
|
assert data_before["account_id"] == 3
|
|
# Logout
|
|
client_auth.get("/logout")
|
|
data_after = client_auth.get("/get-session").json()
|
|
assert data_after["account_id"] is None
|
|
assert data_after["user_id"] is None
|
|
|
|
|
|
def test_dev_bypass_cont_1(client_dev, monkeypatch):
|
|
"""web_auth_required=False -> web_account() returneaza 1 (DEFAULT_ACCOUNT_ID) fara sesiune."""
|
|
resp = client_dev.get("/web-account")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["account_id"] == 1
|
|
|
|
|
|
def test_set_session_curata_sesiunea_anterioara(client_auth):
|
|
"""set_session face clear() inainte de a seta (C3 anti-fixare sesiune)."""
|
|
# Seteaza sesiune initiala cu cont 10
|
|
client_auth.get("/set-session?account_id=10&user_id=10")
|
|
data_initial = client_auth.get("/get-session").json()
|
|
assert data_initial["account_id"] == 10
|
|
# Re-login cu cont nou 20 — sesiunea veche trebuie stearsa inainte
|
|
client_auth.get("/set-session?account_id=20&user_id=20")
|
|
data_nou = client_auth.get("/get-session").json()
|
|
assert data_nou["account_id"] == 20
|
|
assert data_nou["user_id"] == 20
|
|
|
|
|
|
def test_ruta_protejata_cu_sesiune_trece(client_auth):
|
|
"""Cu sesiune setata si web_auth_required=True -> ruta protejata raspunde 200."""
|
|
client_auth.get("/set-session?account_id=5&user_id=5")
|
|
resp = client_auth.get("/protected")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["account_id"] == 5
|