feat(6866): the proxy hop count is an admin setting, and the server shows what each count reads
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 1m47s
CI & Build / integration (push) Successful in 2m1s
CI & Build / Build & push image (push) Successful in 20s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 1m47s
CI & Build / integration (push) Successful in 2m1s
CI & Build / Build & push image (push) Successful in 20s
An admin can save how many reverse proxies stand in front of Grimoire. A saved count wins over TRUSTED_PROXY_HOPS, which stays the starting value. Each worker loads it at startup (under a deadline) and re-reads it every 30s; the worker that saves applies it at once. GET/PUT /api/admin/proxy returns the count, where it came from, and a readout of the admin's own request: the socket peer, the forwarding chains, what counts 0-3 would read, and the count that reads Cloudflare's visitor when Cloudflare's headers are present. Saves are audit-logged. A "too many attempts" reply now says where to look when everyone is being blocked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@ from quart import Blueprint, g, jsonify, redirect, request, session
|
||||
from grimoire.auth import login_required, get_current_user_id
|
||||
from grimoire.config import Config
|
||||
from grimoire.proxy import client_ip
|
||||
from grimoire.rate_limit import is_rate_limited
|
||||
from grimoire.rate_limit import SHARED_ADDRESS_HINT, is_rate_limited
|
||||
from grimoire.services.auth import (
|
||||
authenticate,
|
||||
change_password,
|
||||
@@ -49,7 +49,7 @@ async def register():
|
||||
if not Config.LOCAL_AUTH_ENABLED:
|
||||
return jsonify({"error": "Local authentication is disabled. Please use SSO."}), 403
|
||||
if await is_rate_limited(f"register:{client_ip()}", max_requests=5, window_seconds=300):
|
||||
return jsonify({"error": "Too many registration attempts. Try again later."}), 429
|
||||
return jsonify({"error": f"Too many registration attempts. Try again later. {SHARED_ADDRESS_HINT}"}), 429
|
||||
if not await is_registration_open():
|
||||
return jsonify({"error": "Registration is closed"}), 403
|
||||
|
||||
@@ -79,7 +79,7 @@ async def login():
|
||||
if not Config.LOCAL_AUTH_ENABLED:
|
||||
return jsonify({"error": "Local authentication is disabled. Please use SSO."}), 403
|
||||
if await is_rate_limited(f"login:{client_ip()}", max_requests=10, window_seconds=60):
|
||||
return jsonify({"error": "Too many login attempts. Try again later."}), 429
|
||||
return jsonify({"error": f"Too many login attempts. Try again later. {SHARED_ADDRESS_HINT}"}), 429
|
||||
data = await request.get_json()
|
||||
username = (data.get("username") or "").strip()
|
||||
password = data.get("password") or ""
|
||||
@@ -235,7 +235,7 @@ async def forgot_password():
|
||||
@auth_bp.route("/reset-password", methods=["POST"])
|
||||
async def reset_password():
|
||||
if await is_rate_limited(f"reset:{client_ip()}", max_requests=10, window_seconds=60):
|
||||
return jsonify({"error": "Too many attempts. Try again later."}), 429
|
||||
return jsonify({"error": f"Too many attempts. Try again later. {SHARED_ADDRESS_HINT}"}), 429
|
||||
data = await request.get_json()
|
||||
token = (data.get("token") or "").strip()
|
||||
new_password = data.get("new_password") or ""
|
||||
|
||||
Reference in New Issue
Block a user