185 lines
9.5 KiB
Python
Executable File
185 lines
9.5 KiB
Python
Executable File
"""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)
|