"""ProxyFix wiring and the rate-limit principal key. principal_key returns (key, key_type). The key is the rate-limit bucket; key_type ('principal'|'service'|'ip') is for observability (the breach log) only. All key types share the same limits -- they are not given different budgets. """ from abacus_common_logic.constants.constants import DEFAULT_USER_ID from abacus_common_logic.utils.users import get_flask_user_id from flask import Flask, request from werkzeug.middleware.proxy_fix import ProxyFix from core.config import Config def install_proxyfix(app: Flask, config: type[Config]) -> None: """Trust config.PROXYFIX_X_FOR forwarded hops so request.remote_addr is the real client IP. PROXYFIX_X_FOR must equal the number of proxies that actually append X-Forwarded-For (confirm per env; the edge must strip and re-inject the identity, XFF, and Orchard-Requestor-Service headers so a client cannot spoof them -- see the spec security precondition). """ app.wsgi_app = ProxyFix(app.wsgi_app, x_for=config.PROXYFIX_X_FOR, x_proto=1) def principal_key() -> tuple[str, str]: """Resolve the rate-limit key, tiered: authenticated principal, then requestor service, then IP. The principal tier is server-resolved identity and is trustworthy. The service tier reads the client-supplied Orchard-Requestor-Service header, which is spoofable unless the edge strips and re-injects it (same precondition as XFF, see install_proxyfix); until that holds it must not be relied on for enforcement. IP is the last-resort bucket. """ uid = get_flask_user_id() if uid and uid != DEFAULT_USER_ID: return uid, 'principal' svc = request.headers.get('Orchard-Requestor-Service') if svc: return svc, 'service' return request.remote_addr or 'unknown', 'ip'