"""Live ACL, authentication, atomic reload and logging regressions.""" import base64 import copy import json import re import time from common import http, write_json DENY = {"default": "deny", "rules": []} ALLOW_LOCAL = {"default": "deny", "rules": [{"network": "127.0.0.1/32", "action": "allow"}]} def run(check, rust, peer): port = rust.reverse_port config = copy.deepcopy(rust.base) config["host_policies"] = [{"hosts": ["fixture.test"], "acl": DENY}] rust.apply(config, "policy-global-host") count = peer.count("GET", "/echo") s, h, b = http(port, "/echo", headers={"Host": "fixture.test"}) check("acl.global_host_without_vhost_acl", s == 403 and peer.count("GET", "/echo") == count, status=s, upstream_requests=peer.count("GET", "/echo") - count) s, h, b = http(port, "/echo", headers={"Host": "localhost"}) check("acl.global_other_host_unchanged", s == 200, status=s) config = copy.deepcopy(rust.base) config["vhosts"][0]["acl"] = DENY rust.apply(config, "policy-vhost-deny") s, h, b = http(port) check("acl.vhost_deny", s == 403, status=s) s, h, b = http(rust.static_port) check("acl.vhost_isolation", s == 200, status=s) config["vhosts"][0]["acl"] = ALLOW_LOCAL rust.apply(config, "policy-vhost-allow") s, h, b = http(port) check("acl.hot_enable_allow", s == 200, status=s) config = copy.deepcopy(rust.base) config["acl_profiles"] = [{"id": "restricted", "name": "Restricted", "acl": DENY}] config["vhosts"][0]["acl_profile_id"] = "restricted" config["vhosts"][0]["acl"] = ALLOW_LOCAL rust.apply(config, "policy-profile-deny") s, h, b = http(port) check("acl.profile_and_vhost_stack", s == 403, status=s) config = copy.deepcopy(rust.base) config["vhosts"][0]["routes"] = [{"id": "denied", "path_prefix": "/echo", "acl": DENY}] rust.apply(config, "policy-location-deny") count = peer.count("GET", "/echo") s, h, b = http(port, "/echo") check("acl.location_before_upstream", s == 403 and peer.count("GET", "/echo") == count, status=s) s, h, b = http(port) check("acl.location_other_path_unchanged", s == 200, status=s) config["vhosts"][0]["acl"] = DENY config["vhosts"][0]["routes"][0]["acl"] = ALLOW_LOCAL rust.apply(config, "policy-location-allow-vhost-deny") s, h, b = http(port, "/echo") check("acl.location_cannot_override_vhost_deny", s == 403, status=s) config = copy.deepcopy(rust.base) config["vhosts"][0]["acl"] = {"default": "deny", "rules": [{"network": "203.0.113.22/32", "action": "allow"}]} rust.apply(config, "policy-untrusted-forwarded") s, h, b = http(port, headers={"X-Forwarded-For": "203.0.113.22", "X-Real-IP": "203.0.113.22"}) check("acl.spoofed_forwarded_ip_denied", s == 403, status=s) config["forwarding"] = {"trusted_proxies": ["127.0.0.1/32"]} rust.apply(config, "policy-trusted-forwarded") s, h, b = http(port, headers={"X-Forwarded-For": "203.0.113.22"}) check("acl.trusted_forwarded_ip_allowed", s == 200, status=s) config = copy.deepcopy(rust.base) config["vhosts"][0]["rate_limit"] = {"country_access": {"mode": "allow", "countries": ["PL"]}} config["vhosts"][0]["cloudflare_client_ip"] = True rust.apply(config, "policy-country") s, h, b = http(port, headers={"CF-IPCountry": "PL", "CF-Connecting-IP": "203.0.113.22"}) check("acl.untrusted_country_header_cannot_allow", s == 403, status=s) config = copy.deepcopy(rust.base) config["vhosts"][0]["method_policy"] = {"mode": "allow", "methods": ["GET", "HEAD"]} rust.apply(config, "policy-method") s, h, b = http(port, "/echo", "POST", body=b"denied method") check("policy.method_allowlist", s in (403, 405), status=s) s, h, b = http(port) check("policy.allowed_method_survives", s == 200, status=s) config = copy.deepcopy(rust.base) config["vhosts"][0]["rate_limit"] = {"enabled": True, "requests": 2, "window_seconds": 60, "burst": 0} rust.apply(config, "policy-rate-limit") statuses = [http(port)[0] for _ in range(3)] check("policy.rate_limit", statuses[:2] == [200, 200] and statuses[2] == 429, statuses=statuses) rust.reset() s, h, b = http(port) check("policy.hot_disable_restores_request", s == 200, status=s) # Every route check uses the real admin socket and no bearer/backdoor token. for path in ["/api/config", "/api/status", "/api/logs", "/api/events", "/api/openapi.json", "/assets/app.js"]: s, h, b = http(rust.admin_port, path) check("admin.private." + path, s in (401, 403), status=s) check("admin.login", rust.login() == 200) s, h, b = rust.admin("/api/config") envelope = json.loads(b) check("admin.authenticated_config", s == 200 and "config" in envelope and "revision" in envelope, status=s) s, h, b = rust.admin("/api/config", "PUT", {"expected_revision": envelope["revision"], "config": rust.base}, csrf=False) check("admin.csrf_required", s == 403, status=s) s, h, b = rust.admin("/api/config", origin="http://foreign.example") check("admin.foreign_origin_denied", s == 403, status=s) # Generate the disposable Basic Auth hash through the existing secured API. s, h, b = rust.admin("/api/basic-auth/hash", "POST", {"username": "fixtureuser", "password": rust.password}) hashed = json.loads(b) check("basic.hash_api", s == 200 and "password_hash" in hashed, status=s) config = copy.deepcopy(rust.base) config["vhosts"][0]["basic_auth"] = {"enabled": True, "require_tls": False, "users": [{"username": "fixtureuser", "password_hash": hashed["password_hash"]}]} rust.apply(config, "policy-basic-auth") s, h, b = http(port, "/echo") check("basic.challenge", s == 401 and "Basic" in h.get("www-authenticate", ""), status=s) credentials = base64.b64encode(("fixtureuser:" + rust.password).encode()).decode() s, h, b = http(port, "/echo", headers={"Authorization": "Basic " + credentials}) check("basic.correct_password_and_strip", s == 200 and "authorization" not in json.loads(b)["headers"], status=s) s, h, b = http(port, "/echo", headers={"Authorization": "Basic Zml4dHVyZXVzZXI6d3Jvbmc="}) check("basic.wrong_password_denied", s == 401, status=s) config["vhosts"][0]["acl"] = DENY rust.apply(config, "policy-acl-before-basic") s, h, b = http(port, "/echo") check("basic.acl_precedes_auth", s == 403, status=s) rust.reset() config = copy.deepcopy(rust.base) config["vhosts"][0]["frontend"]["listener_ids"] = ["does-not-exist"] rust.apply(config, "invalid-config-retains-snapshot", expected_success=False) s, h, b = http(port) check("reload.invalid_document_retains_previous_snapshot", s == 200, status=s) s, h, b = http(rust.static_port) check("reload.invalid_document_keeps_other_vhost", s == 200, status=s) config = copy.deepcopy(rust.base) config["vhosts"][0]["backends"][0]["allow_private"] = False # Explicit private-address rejection is checked through apply, preserving the # active configuration if compile-time validation rejects the backend. rust.apply(config, "ssrf-private-origin-disallowed", expected_success=False) s, h, b = http(port) check("ssrf.rejected_origin_does_not_replace_snapshot", s == 200, status=s) query = "/echo?observability=" + rust.label s, h, b = http(port, query) request_id = json.loads(b)["headers"]["x-request-id"] log = None for _ in range(20): ls, lh, lb = rust.admin("/api/logs?vhost=reverse&limit=512") logs = json.loads(lb) log = next((entry for entry in logs.get("entries", []) if entry.get("request_id") == request_id), None) if log: break time.sleep(.05) check("observability.log_fields_and_body_bytes", log is not None and log.get("status") == 200 and log.get("response_bytes") == len(b) and log.get("request_bytes") == 0 and log.get("cache_effective_enabled") is False and log.get("transform_effective_enabled") is False and bool(re.fullmatch("[0-9a-f]{64}", log.get("request_id", ""))), recorded_status=log.get("status") if log else None, recorded_response_bytes=log.get("response_bytes") if log else None, response_bytes=len(b)) if log: write_json(rust.fixture.results / f"{rust.label}-sample-exchange.json", log) config = copy.deepcopy(rust.base) config["diagnostic_headers"] = {"request_id": True} rust.apply(config, "observability-static-eos") s, h, b = http(rust.static_port) static_request_id = h.get("x-request-id") static_log = None for _ in range(20): ls, lh, lb = rust.admin("/api/logs?vhost=static&limit=64") static_log = next((entry for entry in json.loads(lb).get("entries", []) if entry.get("request_id") == static_request_id), None) if static_log: break time.sleep(.05) check("observability.static_complete_after_content_length", s == 200 and static_log is not None and static_log.get("outcome") == "complete" and static_log.get("response_bytes") == len(b), outcome=static_log.get("outcome") if static_log else None, recorded_response_bytes=static_log.get("response_bytes") if static_log else None, response_bytes=len(b)) if static_log: write_json(rust.fixture.results / f"{rust.label}-sample-static-exchange.json", static_log) s, h, b = rust.admin("/api/status") status = json.loads(b) write_json(rust.fixture.results / f"{rust.label}-runtime-status.json", status) check("observability.status_available", s == 200, status=s)