dodil/erp-brick-tax

DODIL ERP brick: tax

src/brick_tax/domain/fraud.py

"""HMRC's fraud-prevention headers for a web app that calls HMRC through a server (connection method WEB_APP_VIA_SERVER, TAX-31).

The browser supplies what only it knows (user agent, device id, screens, window size, timezone, public IP and port with their
timestamp, multi-factor); this brick adds what only the server knows (the connection method, the user id, the vendor's own
product, version, licence, public IP and the forwarding chain). A header either side cannot supply refuses the submission: a
script has no browser, and HMRC rejects a call without them.
"""
import os
from typing import Optional
from urllib.parse import quote

from brick_tax.domain.errors import unmet

METHOD = "WEB_APP_VIA_SERVER"
CLIENT = ("Gov-Client-Browser-JS-User-Agent", "Gov-Client-Device-ID", "Gov-Client-Multi-Factor", "Gov-Client-Public-IP",
          "Gov-Client-Public-IP-Timestamp", "Gov-Client-Public-Port", "Gov-Client-Screens", "Gov-Client-Timezone",
          "Gov-Client-Window-Size")
SERVER = ("Gov-Client-Connection-Method", "Gov-Client-User-IDs", "Gov-Vendor-Forwarded", "Gov-Vendor-License-IDs",
          "Gov-Vendor-Product-Name", "Gov-Vendor-Public-IP", "Gov-Vendor-Version")
REQUIRED = tuple(sorted(CLIENT + SERVER))


def build(client: Optional[dict], *, user_id: str, version: str) -> dict:
    """The full header set for a call made for a person, or a refusal {gate: fraud_headers_complete} naming the first missing one."""
    given = {str(k).lower(): str(v).strip() for k, v in (client or {}).items() if v is not None}
    vendor_ip = os.environ.get("VENDOR_PUBLIC_IP", "").strip()
    client_ip = given.get("gov-client-public-ip", "")
    headers = {h: given.get(h.lower(), "") for h in CLIENT}
    headers.update({
        "Gov-Client-Connection-Method": METHOD,
        "Gov-Client-User-IDs": f"dodil={quote(str(user_id), safe='')}" if user_id else "",
        "Gov-Vendor-Product-Name": "DODIL%20Tax",
        "Gov-Vendor-Version": f"dodil-tax={version}",
        "Gov-Vendor-License-IDs": os.environ.get("VENDOR_LICENSE_IDS", "").strip(),
        "Gov-Vendor-Public-IP": vendor_ip,
        "Gov-Vendor-Forwarded": f"by={vendor_ip}&for={client_ip}" if vendor_ip and client_ip else "",
    })
    missing = [h for h in REQUIRED if not headers.get(h)]
    if missing:
        raise unmet("fraud_headers_complete", f"HMRC requires {missing[0]}", "submit from the web screen, not a script")
    return headers