Security values move into the Settings UI
CI & Build / Build now, or wait for Android? (push) Successful in 2s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Failing after 9s
CI & Build / integration (push) Failing after 12s
CI & Build / Build & push image (push) Successful in 32s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m17s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m14s
Desktop (Tauri) / Update manifest (push) Successful in 5s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Failing after 9s
CI & Build / integration (push) Failing after 12s
CI & Build / Build & push image (push) Successful in 32s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m17s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m14s
Desktop (Tauri) / Update manifest (push) Successful in 5s
Operator: *"proxy hops defaults to 1 and should be in the settings UI not in the envs, we need the security values to be in the UI."* Overrules the call I made yesterday, and rule 25 is on your side — I argued deployment-topology, but the operator has to be able to SEE what protects them, and reading a container's environment is not seeing. Six new settings in a **Security** group: trusted proxy hops (default 1), the per-account and per-address sign-in limits with their shared window, and the sign-up limit with its own. `THOUGHTSYNC_TRUSTED_PROXY_HOPS` is gone; the rate limits are no longer hardcoded constants. **The hard part was keeping the throttle cheap.** It consults these BEFORE opening a database connection — deliberately, because a refused attempt is meant to cost nothing, and the hop count is needed to know who is even asking. A query per attempt would undo both. So there is a small cache seeded from the registry defaults (the app works with no database at all, which is what the DB-free unit lane relies on), loaded at boot, and refreshed on every settings save — the same live-update contract `session_ttl_days` already had. `SlidingWindow` now takes its limit and window as SUPPLIERS rather than values, so a saved number applies to the next attempt instead of the next deploy. **Bounds are rejected, not clamped.** A hop count of 99 would trust anything a caller sent; a sign-in limit of 0 would lock every account out permanently. Both now fail validation with a message naming the range, and the number input carries min/max so the browser objects first. Silently storing a different number than the one typed is how somebody ends up believing a protection is set to something it is not. `MAX_BUCKETS` stays a constant on purpose: it protects the limiter from itself rather than the app from a caller, and there is no operator judgment to apply. Two integration tests, because the whole point is the round trip: a dangerous value refused, a legitimate one reaching the cache the throttle reads and persisting; and every Security row reaching the admin payload with bounds and a description that explains itself.
This commit is contained in:
@@ -15,7 +15,7 @@ sent arrives to the LEFT of those.
|
||||
That inverts the intuitive reading. The leftmost entry is nominally "the original
|
||||
client" — and is exactly the one a caller can forge, by sending the header themselves.
|
||||
So we count in from the right by the number of proxies we actually run
|
||||
(`THOUGHTSYNC_TRUSTED_PROXY_HOPS`, default 1), and a forged prefix can never be
|
||||
(the **Trusted proxy hops** setting, default 1), and a forged prefix can never be
|
||||
selected no matter how much of it there is.
|
||||
|
||||
Too HIGH a hop count is the dangerous direction: it starts believing entries no proxy
|
||||
@@ -27,7 +27,7 @@ from __future__ import annotations
|
||||
|
||||
from quart import has_request_context, request
|
||||
|
||||
from .config import Config
|
||||
from .settings import live
|
||||
|
||||
|
||||
def trusted_entry(header: str, hops: int) -> str | None:
|
||||
@@ -54,7 +54,7 @@ def client_address() -> str:
|
||||
return forwarded_for(
|
||||
request.headers.get("X-Forwarded-For", ""),
|
||||
request.remote_addr,
|
||||
Config.trusted_proxy_hops(),
|
||||
live("trusted_proxy_hops"),
|
||||
)
|
||||
|
||||
|
||||
@@ -70,5 +70,5 @@ def is_https() -> bool:
|
||||
return False
|
||||
if request.is_secure:
|
||||
return True
|
||||
entry = trusted_entry(request.headers.get("X-Forwarded-Proto", ""), Config.trusted_proxy_hops())
|
||||
entry = trusted_entry(request.headers.get("X-Forwarded-Proto", ""), live("trusted_proxy_hops"))
|
||||
return (entry or "").lower() == "https"
|
||||
|
||||
Reference in New Issue
Block a user