diff --git a/.env.example b/.env.example index 8c6db39..b01fa5b 100644 --- a/.env.example +++ b/.env.example @@ -39,6 +39,11 @@ STATION_CODE=ST-DEFAULT # floor the station identity comes from the local install, and measuring against # another station's recipes would silently break traceability. STATION_SWITCH_ENABLED=0 +# Cookie di sessione "Secure": di default segue FLASK_DEBUG (sicuro salvo +# debug). Impostare a false solo per l'installazione on-premise che serve +# HTTP in LAN (#38.4) - un cookie Secure su un'origine non-HTTPS viene +# scartato dal browser in silenzio e il login non regge mai. +# SESSION_COOKIE_SECURE=false # --- Vision --- # Internal address of the vision worker. Never exposed outside tmflow-net. diff --git a/src/frontend/flask_app/config.py b/src/frontend/flask_app/config.py index 7a805c3..c1528a4 100644 --- a/src/frontend/flask_app/config.py +++ b/src/frontend/flask_app/config.py @@ -23,7 +23,18 @@ class Config: # Session SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SAMESITE = "Lax" - SESSION_COOKIE_SECURE = not DEBUG # Only secure cookies in production (HTTPS) + # Default: secure unless FLASK_DEBUG=1 (unchanged). Explicit override via + # SESSION_COOKIE_SECURE=false for the on-premise install (#38.4): nginx + # there listens on plain HTTP inside the LAN, and a cookie marked Secure + # is silently dropped by the browser on a non-HTTPS origin - login looks + # like it works and the session never sticks. + _session_cookie_secure_env = os.getenv("SESSION_COOKIE_SECURE") + if _session_cookie_secure_env is not None: + SESSION_COOKIE_SECURE = _session_cookie_secure_env.strip().lower() not in ( + "false", "0", "no", + ) + else: + SESSION_COOKIE_SECURE = not DEBUG PERMANENT_SESSION_LIFETIME = 28800 # 8 hours # None = token valid for the life of the session (#12): a measurement page # stays open for hours between saves, and a 1h-expired token turned every diff --git a/src/frontend/flask_app/tests/test_config_session_cookie_secure.py b/src/frontend/flask_app/tests/test_config_session_cookie_secure.py new file mode 100644 index 0000000..469d66a --- /dev/null +++ b/src/frontend/flask_app/tests/test_config_session_cookie_secure.py @@ -0,0 +1,50 @@ +"""SESSION_COOKIE_SECURE reads from the environment (#38.4). + +Default unchanged (secure unless FLASK_DEBUG=1); SESSION_COOKIE_SECURE=false +overrides it for the on-premise HTTP-in-LAN install. +""" +import importlib + + +def _reload_config(): + import config + importlib.reload(config) + return config + + +def test_defaults_secure_when_not_debug(monkeypatch): + monkeypatch.delenv("SESSION_COOKIE_SECURE", raising=False) + monkeypatch.delenv("FLASK_DEBUG", raising=False) + config = _reload_config() + assert config.Config.SESSION_COOKIE_SECURE is True + + +def test_defaults_insecure_when_debug(monkeypatch): + monkeypatch.delenv("SESSION_COOKIE_SECURE", raising=False) + monkeypatch.setenv("FLASK_DEBUG", "1") + config = _reload_config() + assert config.Config.SESSION_COOKIE_SECURE is False + + +def test_explicit_false_overrides_default_in_production(monkeypatch): + """On-premise over plain HTTP: FLASK_DEBUG stays unset (real prod), but + the cookie must not be marked Secure or the browser silently drops it.""" + monkeypatch.delenv("FLASK_DEBUG", raising=False) + monkeypatch.setenv("SESSION_COOKIE_SECURE", "false") + config = _reload_config() + assert config.Config.SESSION_COOKIE_SECURE is False + + +def test_explicit_true_keeps_secure(monkeypatch): + monkeypatch.setenv("FLASK_DEBUG", "1") + monkeypatch.setenv("SESSION_COOKIE_SECURE", "true") + config = _reload_config() + assert config.Config.SESSION_COOKIE_SECURE is True + + +def test_falsy_values_are_case_insensitive(monkeypatch): + monkeypatch.delenv("FLASK_DEBUG", raising=False) + for value in ("false", "FALSE", "0", "no", " False "): + monkeypatch.setenv("SESSION_COOKIE_SECURE", value) + config = _reload_config() + assert config.Config.SESSION_COOKIE_SECURE is False, value