mirror of
https://github.com/taylanbakircioglu/haproxy-openmanager.git
synced 2026-10-04 20:41:33 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c79391cd13 |
+23
-45
@@ -456,26 +456,17 @@ class FrontendConfig(BaseModel):
|
||||
if any(dangerous in rule.lower() for dangerous in ['$(', '`']):
|
||||
raise ValueError(f'ACL rule contains potentially dangerous content: "{rule}"')
|
||||
|
||||
# Phase K Phase D follow-up (Bulgu #12 round 3) — reject
|
||||
# the HAProxy `-f <file>` pattern-file flag here too so the
|
||||
# manual Frontend API mirrors the wizard's parity rule.
|
||||
# HAProxy OpenManager does not provision pattern files
|
||||
# onto the HAProxy node filesystem, so any `-f /path/...`
|
||||
# reference will fail HAProxy's `-c` parse at apply time
|
||||
# with "failed to open pattern file". Reject up-front so
|
||||
# operators get the same actionable error from both the
|
||||
# manual page and the wizard.
|
||||
if re.search(r"(^|\s)-f(\s|$)", rule):
|
||||
raise ValueError(
|
||||
f'ACL rule "{rule}" uses the HAProxy `-f <file>` '
|
||||
"pattern-file flag, which is not supported in "
|
||||
"HAProxy OpenManager: the product does not "
|
||||
"provision pattern files onto the HAProxy node "
|
||||
"filesystem, so the reference would fail at "
|
||||
"reload time. Use inline values instead "
|
||||
"(e.g. `src 10.0.0.0/24` rather than "
|
||||
"`src -f /etc/haproxy/admins.lst`)."
|
||||
)
|
||||
# Issue #38 follow-up — the `-f <file>` pattern-file flag
|
||||
# is ACCEPTED here (the Bulgu #12 hard reject was removed).
|
||||
# Pattern files are operator-managed host files, exactly
|
||||
# like the SPOE `filter ... config <path>` reference this
|
||||
# release started preserving: bulk import always accepted
|
||||
# `-f`, the free-form fields (request_headers,
|
||||
# tcp_request_rules) always accepted it, and the agent
|
||||
# runs `haproxy -c` before every reload so a missing file
|
||||
# fails safely (previous config keeps running). The route
|
||||
# handlers surface a non-blocking warning listing the
|
||||
# referenced pattern files instead.
|
||||
|
||||
validated_rules.append(rule)
|
||||
|
||||
@@ -515,20 +506,12 @@ class FrontendConfig(BaseModel):
|
||||
if not any(rule.startswith(redirect_type) for redirect_type in valid_redirects):
|
||||
raise ValueError(f'Invalid redirect rule: "{rule}". Must start with: location, prefix, or scheme.')
|
||||
|
||||
# Phase K Phase D follow-up (Bulgu #12 round 3) —
|
||||
# mirror the wizard's `-f <file>` guard here. The
|
||||
# `X !X` contradiction check used to live alongside
|
||||
# this guard, but Bulgu #62 (round-22 audit) moved
|
||||
# it into the route handler so updates can grandfather
|
||||
# legacy rules created before the contradiction guard
|
||||
# landed. See `routers/frontend.py::_collect_routing_rule_contradictions`.
|
||||
if re.search(r"(^|\s)-f(\s|$)", rule):
|
||||
raise ValueError(
|
||||
f'Redirect rule "{rule}" uses the HAProxy `-f <file>` '
|
||||
"pattern-file flag, which is not supported in "
|
||||
"HAProxy OpenManager: the product does not provision "
|
||||
"pattern files onto the HAProxy node filesystem."
|
||||
)
|
||||
# Issue #38 follow-up — `-f <file>` pattern-file references
|
||||
# are ACCEPTED (Bulgu #12 hard reject removed; see
|
||||
# validate_acl_rules for the full rationale). The `X !X`
|
||||
# contradiction check lives in the route handler
|
||||
# (`routers/frontend.py::_collect_routing_rule_contradictions`,
|
||||
# Bulgu #62) and is unchanged.
|
||||
|
||||
validated_rules.append(rule)
|
||||
|
||||
@@ -536,9 +519,12 @@ class FrontendConfig(BaseModel):
|
||||
|
||||
@validator('use_backend_rules')
|
||||
def validate_use_backend_rules_syntax(cls, v):
|
||||
"""Phase K Phase D follow-up (Bulgu #12 round 3) — manual
|
||||
Frontend API parity guard: reject `-f <file>` references
|
||||
and dangerous shell patterns.
|
||||
"""Manual Frontend API guard for dangerous shell patterns.
|
||||
|
||||
Issue #38 follow-up — the Bulgu #12 `-f <file>` hard reject
|
||||
was removed (see validate_acl_rules for the rationale);
|
||||
pattern-file references are operator-managed host files and
|
||||
are surfaced as non-blocking warnings by the route handlers.
|
||||
|
||||
Bulgu #62 (round-22 audit) — the `X !X` contradiction check
|
||||
previously lived here but moved into the route handler so
|
||||
@@ -568,13 +554,5 @@ class FrontendConfig(BaseModel):
|
||||
f'use_backend rule contains potentially dangerous '
|
||||
f'content: "{rule}"'
|
||||
)
|
||||
if re.search(r"(^|\s)-f(\s|$)", rule):
|
||||
raise ValueError(
|
||||
f'use_backend rule "{rule}" uses the HAProxy '
|
||||
"`-f <file>` pattern-file flag, which is not "
|
||||
"supported in HAProxy OpenManager: the product "
|
||||
"does not provision pattern files onto the HAProxy "
|
||||
"node filesystem."
|
||||
)
|
||||
validated_rules.append(rule)
|
||||
return validated_rules
|
||||
@@ -179,35 +179,17 @@ _MAX_RULE_STRING_LEN = 4096
|
||||
# attempts.
|
||||
_DANGEROUS_RULE_PATTERNS = ("$(", "`")
|
||||
|
||||
# Phase K Phase D follow-up (Bulgu #12 round 3) — the HAProxy `-f
|
||||
# <file>` ACL/condition flag instructs HAProxy to load match patterns
|
||||
# from a server-side file at parse time. HAProxy OpenManager is a
|
||||
# fully-managed product: we do NOT provision pattern files onto the
|
||||
# HAProxy node's filesystem, and operators have no UI to upload one.
|
||||
# A `-f /some/path` reference therefore ALWAYS resolves to
|
||||
# "file not found" when HAProxy's real `-c` parse runs at apply
|
||||
# time, producing exactly the operator-reported failure mode:
|
||||
# [ALERT] parsing ACL 'acl1' : failed to open pattern file </path>.
|
||||
# [ALERT] parsing switching rule : no such ACL : 'acl1'.
|
||||
#
|
||||
# Surface this BEFORE persist by rejecting `-f` in any rule string
|
||||
# that comes through the wizard / manual frontend API. Reject ALL
|
||||
# variants (` -f `, leading `-f `, trailing `... -f`) defensively so
|
||||
# operators cannot slip the flag through with creative spacing.
|
||||
# The check is anchored to ACL/condition rule strings only; raw
|
||||
# HAProxy snippet fields (tcp_request_rules, request_headers, ...)
|
||||
# are NOT touched because those are inherently free-form and
|
||||
# advanced operators may legitimately reference pre-provisioned
|
||||
# pattern files there.
|
||||
_ACL_FILE_FLAG_PATTERN = re.compile(r"(^|\s)-f(\s|$)")
|
||||
_ACL_FILE_FLAG_MESSAGE = (
|
||||
"pattern-file references with '-f <file>' are not supported in ACL / "
|
||||
"use_backend / redirect rules: HAProxy OpenManager does not provision "
|
||||
"pattern files onto the HAProxy node's filesystem, so the reference "
|
||||
"would always fail at HAProxy reload time. Use inline values "
|
||||
"instead (e.g. `acl is_admin src 10.0.0.0/24` rather than "
|
||||
"`acl is_admin src -f /etc/haproxy/admins.lst`)."
|
||||
)
|
||||
# Issue #38 follow-up — the HAProxy `-f <file>` ACL/condition flag
|
||||
# loads match patterns from a file on the HAProxy host. The Bulgu #12
|
||||
# hard reject (`_ACL_FILE_FLAG_PATTERN`/`_ACL_FILE_FLAG_MESSAGE`) was
|
||||
# removed: pattern files are operator-managed host files (exactly like
|
||||
# the SPOE `filter ... config <path>` reference preserved since
|
||||
# v1.8.8), bulk import and the free-form fields (tcp_request_rules,
|
||||
# request_headers) always accepted them, and the agent runs
|
||||
# `haproxy -c` before every reload so a missing file fails safely
|
||||
# (the previous config keeps running). The manual frontend route
|
||||
# handlers emit a non-blocking warning listing referenced pattern
|
||||
# files (`routers/frontend.py::_pattern_file_warnings`).
|
||||
|
||||
# Phase K Phase D follow-up (Bulgu #13) — detect a routing /
|
||||
# redirect rule whose condition references the SAME ACL in both
|
||||
@@ -305,13 +287,10 @@ def _validate_haproxy_directive_string(
|
||||
f"{field_label} entry contains potentially dangerous content: "
|
||||
f"{pattern!r}"
|
||||
)
|
||||
# Phase K Phase D follow-up (Bulgu #12 round 3) — reject the
|
||||
# HAProxy `-f <file>` pattern-file flag because OpenManager does
|
||||
# not manage the HAProxy node filesystem. See the module-level
|
||||
# `_ACL_FILE_FLAG_PATTERN` docstring for the full operator-
|
||||
# reported failure mode this guards against.
|
||||
if _ACL_FILE_FLAG_PATTERN.search(stripped):
|
||||
raise ValueError(f"{field_label}: {_ACL_FILE_FLAG_MESSAGE}")
|
||||
# Issue #38 follow-up — `-f <file>` pattern-file references are
|
||||
# ACCEPTED (Bulgu #12 hard reject removed; see the module-level
|
||||
# `_ACL_FILE_FLAG_PATTERN` comment). The route handlers surface
|
||||
# a non-blocking pattern-file warning instead.
|
||||
# Phase K Phase D follow-up (Bulgu #13) — for routing /
|
||||
# redirect rules (not ACL definitions themselves), reject a
|
||||
# condition that contains the same ACL in both positive and
|
||||
@@ -1083,21 +1062,12 @@ class FrontendStep(BaseModel):
|
||||
normalised: List[Union[str, dict]] = []
|
||||
for el in v:
|
||||
if isinstance(el, dict):
|
||||
# Phase K Phase D follow-up (Bulgu #12 round 3
|
||||
# extension) — dict-shaped redirect rules emit their
|
||||
# `condition` / `target` fields VERBATIM into the
|
||||
# rendered HAProxy directive. A dict with
|
||||
# `condition: "if { src -f /etc/haproxy/x.lst }"`
|
||||
# would slip past the string-only validator above
|
||||
# and trigger the same operator-reported "failed to
|
||||
# open pattern file" rejection at apply time. Reject
|
||||
# `-f` in any string-shaped value the dict carries.
|
||||
for field_name in ("condition", "target", "type"):
|
||||
val = el.get(field_name)
|
||||
if isinstance(val, str) and _ACL_FILE_FLAG_PATTERN.search(val):
|
||||
raise ValueError(
|
||||
f"redirect_rules.{field_name}: {_ACL_FILE_FLAG_MESSAGE}"
|
||||
)
|
||||
# Issue #38 follow-up — dict-shaped redirect rules may
|
||||
# carry `-f <file>` pattern-file references in their
|
||||
# `condition`/`target` values; these are ACCEPTED now
|
||||
# (Bulgu #12 hard reject removed — operator-managed
|
||||
# host files, fail-safe apply; see module-level
|
||||
# `_ACL_FILE_FLAG_PATTERN` comment).
|
||||
# Bulgu #13 extension — same contradiction guard
|
||||
# for dict-shaped redirect conditions.
|
||||
cond_val = el.get("condition")
|
||||
|
||||
@@ -1133,6 +1133,25 @@ async def parse_bulk_config(
|
||||
f"{_cfg_dir}). HAProxy OpenManager preserves the filter directive but "
|
||||
f"does not provision these files; otherwise 'haproxy -c' fails at apply."
|
||||
)
|
||||
# Issue #38 follow-up: ACL `-f <file>` pattern-file advisory.
|
||||
# Scan only the structured rule fields (acl/use_backend) —
|
||||
# request_headers/tcp_request_rules were always free-form and
|
||||
# warning on them now would add new noise for existing users.
|
||||
_pattern_paths = []
|
||||
for _rule in (_fe.get("acl_rules") or []) + (_fe.get("use_backend_rules") or []):
|
||||
if isinstance(_rule, str):
|
||||
_pattern_paths.extend(
|
||||
re.findall(r"(?:^|\s)-f\s+(\S+)", _rule))
|
||||
if _pattern_paths:
|
||||
_uniq = sorted(set(_pattern_paths))
|
||||
enhanced_warnings.append(
|
||||
f"ℹ️ Frontend '{_fe['name']}': ACL/routing rules reference pattern "
|
||||
f"file(s) {', '.join(_uniq)}. Each file must exist at that exact path "
|
||||
f"on every HAProxy host in the cluster (cluster config dir: {_cfg_dir}) "
|
||||
f"— HAProxy OpenManager does not create or distribute pattern files. "
|
||||
f"A missing file fails safely at 'haproxy -c' (previous config keeps "
|
||||
f"running)."
|
||||
)
|
||||
except Exception as _spoe_adv_err:
|
||||
logger.warning(f"SPOE advisory generation skipped: {_spoe_adv_err}")
|
||||
|
||||
|
||||
@@ -117,6 +117,41 @@ def _rule_contradiction_text(rule: Any) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def _pattern_file_warnings(
|
||||
acl_rules: Optional[List[Any]] = None,
|
||||
use_backend_rules: Optional[List[Any]] = None,
|
||||
redirect_rules: Optional[List[Any]] = None,
|
||||
) -> List[str]:
|
||||
"""Issue #38 follow-up — non-blocking `-f <file>` pattern-file
|
||||
advisory for the manual frontend API.
|
||||
|
||||
The Bulgu #12 hard reject was removed from the Pydantic models:
|
||||
pattern files are operator-managed host files (same policy as the
|
||||
SPOE `filter ... config <path>` reference preserved since v1.8.8)
|
||||
and the agent's pre-reload `haproxy -c` makes a missing file fail
|
||||
safely. This helper returns one warning listing the unique file
|
||||
paths referenced across the rule fields, or [] when no rule uses
|
||||
`-f` — operators who don't use pattern files see no change.
|
||||
"""
|
||||
paths: List[str] = []
|
||||
for rules in (acl_rules, use_backend_rules, redirect_rules):
|
||||
for rule in rules or []:
|
||||
text = rule if isinstance(rule, str) else (
|
||||
rule.get("condition") if isinstance(rule, dict) else None)
|
||||
if isinstance(text, str):
|
||||
paths.extend(re.findall(r"(?:^|\s)-f\s+(\S+)", text))
|
||||
if not paths:
|
||||
return []
|
||||
uniq = sorted(set(paths))
|
||||
return [
|
||||
f"ACL/routing rules reference pattern file(s) {', '.join(uniq)}. "
|
||||
f"Each file must exist at that exact path on every HAProxy host "
|
||||
f"in the cluster — HAProxy OpenManager does not create or "
|
||||
f"distribute pattern files. A missing file fails safely at "
|
||||
f"'haproxy -c' (the previous config keeps running)."
|
||||
]
|
||||
|
||||
|
||||
def _collect_routing_rule_contradictions(
|
||||
rules: List[Any], origin_label: str,
|
||||
) -> List[Tuple[str, Any]]:
|
||||
@@ -833,12 +868,19 @@ async def create_frontend(frontend: FrontendConfig, request: Request, authorizat
|
||||
user_agent=request.headers.get('user-agent')
|
||||
)
|
||||
|
||||
return {
|
||||
response: dict = {
|
||||
"message": f"Frontend '{frontend.name}' created successfully",
|
||||
"id": frontend_id,
|
||||
"frontend": frontend.dict(),
|
||||
"sync_results": sync_results
|
||||
}
|
||||
# Issue #38 follow-up — non-blocking pattern-file advisory
|
||||
# (additive field; absent when no rule references `-f`).
|
||||
pattern_warnings = _pattern_file_warnings(
|
||||
frontend.acl_rules, frontend.use_backend_rules, frontend.redirect_rules)
|
||||
if pattern_warnings:
|
||||
response["warnings"] = pattern_warnings
|
||||
return response
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -1246,8 +1288,12 @@ async def update_frontend(frontend_id: int, frontend: FrontendConfig, request: R
|
||||
# yellow toast on the next refresh. The save SUCCEEDED; the
|
||||
# warnings only flag latent legacy data the operator may
|
||||
# want to clean up at their convenience.
|
||||
if contradiction_warnings:
|
||||
response["warnings"] = contradiction_warnings
|
||||
# Issue #38 follow-up — append the pattern-file advisory to
|
||||
# the same list (additive; empty when no rule uses `-f`).
|
||||
all_warnings = list(contradiction_warnings or []) + _pattern_file_warnings(
|
||||
frontend.acl_rules, frontend.use_backend_rules, frontend.redirect_rules)
|
||||
if all_warnings:
|
||||
response["warnings"] = all_warnings
|
||||
return response
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
"""Issue #38 follow-up — ACL `-f <file>` pattern-file support (v1.8.9).
|
||||
|
||||
The Bulgu #12 hard rejects were removed: pattern files are
|
||||
operator-managed host files (same policy as the SPOE
|
||||
`filter ... config <path>` reference preserved since v1.8.8), bulk
|
||||
import always accepted `-f`, and the agent runs `haproxy -c` before
|
||||
every reload so a missing file fails safely. These tests pin:
|
||||
|
||||
1. ACCEPT — the manual FrontendConfig model and the wizard models
|
||||
accept `-f` in every rule field (string + dict shapes).
|
||||
2. GUARDS KEPT — `$(`/backtick shell-substitution rejects and the
|
||||
`X !X` contradiction machinery are unchanged.
|
||||
3. WARNINGS — `_pattern_file_warnings` emits exactly one advisory
|
||||
listing the referenced files, and NOTHING for `-f`-free rules
|
||||
(zero-noise: existing users see no new output).
|
||||
4. ADVISORY — the bulk-import preview advisory block scans
|
||||
acl/use_backend rules (and only those fields).
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from models.frontend import FrontendConfig # noqa: E402
|
||||
from routers.frontend import _pattern_file_warnings # noqa: E402
|
||||
|
||||
|
||||
ACL_F = "blacklisted src -f /etc/haproxy/blacklist.lst"
|
||||
UB_F = "be-secure if { src -f /etc/haproxy/allowlist.lst }"
|
||||
REDIR_F = "location /blocked if { src -f /etc/haproxy/blacklist.lst }"
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 1. ACCEPT — manual FrontendConfig model
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_frontend_config_accepts_acl_file_flag():
|
||||
fe = FrontendConfig(name="fe1", bind_port=80, mode="http", acl_rules=[ACL_F])
|
||||
assert fe.acl_rules == [ACL_F]
|
||||
|
||||
|
||||
def test_frontend_config_accepts_use_backend_file_flag():
|
||||
fe = FrontendConfig(
|
||||
name="fe1", bind_port=80, mode="http", use_backend_rules=[UB_F])
|
||||
assert fe.use_backend_rules == [UB_F]
|
||||
|
||||
|
||||
def test_frontend_config_accepts_redirect_string_file_flag():
|
||||
fe = FrontendConfig(
|
||||
name="fe1", bind_port=80, mode="http", redirect_rules=[REDIR_F])
|
||||
assert fe.redirect_rules == [REDIR_F]
|
||||
|
||||
|
||||
def test_frontend_config_accepts_redirect_dict_file_flag():
|
||||
rule = {"type": "scheme", "scheme": "https",
|
||||
"condition": "if { src -f /etc/haproxy/blacklist.lst }"}
|
||||
fe = FrontendConfig(
|
||||
name="fe1", bind_port=80, mode="http", redirect_rules=[rule])
|
||||
assert fe.redirect_rules == [rule]
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 2. GUARDS KEPT — dangerous-content rejects unchanged
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad_rule", [
|
||||
"acl1 path $(rm -rf /)",
|
||||
"acl1 path `id`",
|
||||
])
|
||||
def test_acl_shell_substitution_still_rejected(bad_rule):
|
||||
from pydantic import ValidationError
|
||||
with pytest.raises(ValidationError):
|
||||
FrontendConfig(name="fe1", bind_port=80, mode="http", acl_rules=[bad_rule])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad_rule", [
|
||||
"be1 if $(whoami)",
|
||||
"be1 if `id`",
|
||||
])
|
||||
def test_use_backend_shell_substitution_still_rejected(bad_rule):
|
||||
from pydantic import ValidationError
|
||||
with pytest.raises(ValidationError):
|
||||
FrontendConfig(
|
||||
name="fe1", bind_port=80, mode="http", use_backend_rules=[bad_rule])
|
||||
|
||||
|
||||
def test_contradiction_detection_still_works_on_file_flag_rules():
|
||||
"""Interaction guard: a `-f` rule with an `X !X` contradiction is
|
||||
still caught by the handler-level contradiction machinery — the
|
||||
`-f` relaxation must not weaken that gate."""
|
||||
from models.frontend import _frontend_has_acl_contradiction
|
||||
assert _frontend_has_acl_contradiction(
|
||||
"be1 if blacklisted !blacklisted") is True
|
||||
# And a normal -f rule is NOT a contradiction.
|
||||
assert _frontend_has_acl_contradiction(UB_F) is False
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 3. WARNINGS — _pattern_file_warnings (zero-noise contract)
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_pattern_file_warnings_lists_unique_paths():
|
||||
warnings = _pattern_file_warnings(
|
||||
acl_rules=[ACL_F, "other src -f /etc/haproxy/blacklist.lst"],
|
||||
use_backend_rules=[UB_F],
|
||||
redirect_rules=[{"condition": "if { src -f /etc/haproxy/geo.lst }"}],
|
||||
)
|
||||
assert len(warnings) == 1
|
||||
w = warnings[0]
|
||||
assert "/etc/haproxy/blacklist.lst" in w
|
||||
assert "/etc/haproxy/allowlist.lst" in w
|
||||
assert "/etc/haproxy/geo.lst" in w
|
||||
# Duplicate path listed once.
|
||||
assert w.count("/etc/haproxy/blacklist.lst") == 1
|
||||
# Non-blocking framing: mentions fail-safe haproxy -c.
|
||||
assert "haproxy -c" in w
|
||||
|
||||
|
||||
def test_pattern_file_warnings_empty_without_file_flag():
|
||||
"""Zero-noise: operators who don't use `-f` must see NO warning."""
|
||||
assert _pattern_file_warnings(
|
||||
acl_rules=["is_api path_beg /api", "is_admin src 10.0.0.0/24"],
|
||||
use_backend_rules=["be-api if is_api"],
|
||||
redirect_rules=[{"type": "scheme", "scheme": "https",
|
||||
"condition": "if !{ ssl_fc }"}],
|
||||
) == []
|
||||
assert _pattern_file_warnings() == []
|
||||
|
||||
|
||||
def test_pattern_file_warnings_ignores_dash_f_substrings():
|
||||
"""`-file`/`-foo` substrings must not trigger the advisory."""
|
||||
assert _pattern_file_warnings(
|
||||
acl_rules=["is_self path_beg /self-config-file",
|
||||
"is_foo path_beg /foo -m beg"],
|
||||
) == []
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 4. Wizard models accept `-f` (string + dict) — parity
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_wizard_models_accept_file_flag():
|
||||
from models.site_wizard import FrontendStep
|
||||
|
||||
fe = FrontendStep(
|
||||
name="fe1", mode="http", bind_address="*", bind_port=80,
|
||||
acl_rules=[ACL_F],
|
||||
use_backend_rules=["be-x if blacklisted"],
|
||||
redirect_rules=[{"type": "scheme", "target": "https",
|
||||
"condition": "if { src -f /etc/haproxy/x.lst }"}],
|
||||
)
|
||||
assert fe.acl_rules == [ACL_F]
|
||||
assert fe.redirect_rules[0]["condition"] == "if { src -f /etc/haproxy/x.lst }"
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 5. Bulk-import preview advisory — source-level pin
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_parse_bulk_advisory_scans_only_structured_rule_fields():
|
||||
"""The preview advisory scans acl_rules/use_backend_rules but NOT
|
||||
request_headers/tcp_request_rules (always-free-form fields —
|
||||
warning there would add new noise for existing users)."""
|
||||
src = Path(__file__).resolve().parents[1] / "routers" / "config.py"
|
||||
text = src.read_text()
|
||||
block_start = text.index("pattern-file advisory")
|
||||
block = text[block_start:block_start + 1200]
|
||||
assert 'acl_rules' in block
|
||||
assert 'use_backend_rules' in block
|
||||
assert 'request_headers' not in block.split("_pattern_paths")[1], (
|
||||
"advisory must not scan request_headers")
|
||||
|
||||
|
||||
def test_no_dash_f_reject_left_in_models():
|
||||
"""No model file may still hard-reject the `-f` flag."""
|
||||
for rel in ("models/frontend.py", "models/site_wizard.py"):
|
||||
text = (Path(__file__).resolve().parents[1] / rel).read_text()
|
||||
for m in re.finditer(r"-f\(\\s\|\$\)", text):
|
||||
ctx = text[max(0, m.start() - 400):m.start() + 400]
|
||||
assert "raise ValueError" not in ctx, (
|
||||
f"{rel}: a `-f` reject regex still sits next to a raise")
|
||||
@@ -206,41 +206,38 @@ def test_module_level_validate_haproxy_config_forwards_partial_fragment():
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# Wizard Pydantic gate: ACL `-f` flag must be REJECTED at submit.
|
||||
# Issue #38 follow-up: ACL `-f <file>` pattern-file references are
|
||||
# ACCEPTED (the Bulgu #12 hard reject was removed — pattern files are
|
||||
# operator-managed host files, the agent's pre-reload `haproxy -c`
|
||||
# makes a missing file fail safely, and bulk import always accepted
|
||||
# `-f`). These tests pin the ACCEPT behaviour.
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_wizard_pydantic_rejects_acl_with_file_flag():
|
||||
"""Pre-fix the wizard's ACL string validator passed
|
||||
`acl name path -i -m reg -f /path` straight through. Apply-time
|
||||
HAProxy `-c` then failed with "failed to open pattern file".
|
||||
Pin that the validator now rejects `-f` at submit.
|
||||
def test_wizard_pydantic_accepts_acl_with_file_flag():
|
||||
"""Issue #38 follow-up — the wizard's ACL string validator must
|
||||
ACCEPT `-f <file>` pattern-file references (Bulgu #12 reject
|
||||
removed). Operators with large host-managed IP blacklists rely
|
||||
on this in production.
|
||||
"""
|
||||
from models.site_wizard import FrontendStep
|
||||
|
||||
# Minimal valid wizard frontend kwargs — only the offending
|
||||
# acl_rules entry should trigger the failure.
|
||||
fe_kwargs = dict(
|
||||
fe = FrontendStep(
|
||||
name="fe1",
|
||||
mode="http",
|
||||
bind_address="*",
|
||||
bind_port=80,
|
||||
acl_rules=["acl1 path -i -m reg -f /path"],
|
||||
)
|
||||
from pydantic import ValidationError
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
FrontendStep(**fe_kwargs)
|
||||
msg = str(exc_info.value)
|
||||
assert "-f" in msg or "pattern-file" in msg.lower(), (
|
||||
f"Bulgu #12 regression: ACL -f flag must be rejected with a "
|
||||
f"clear pattern-file error. Got: {msg}"
|
||||
assert fe.acl_rules == ["acl1 path -i -m reg -f /path"], (
|
||||
"ACL `-f` rule must round-trip verbatim through the wizard model"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"rule",
|
||||
[
|
||||
# Various spacing / position variants the regex must catch.
|
||||
# Various spacing / position variants must all be accepted.
|
||||
"acl1 path -f /etc/haproxy/list",
|
||||
"acl1 path -i -f /tmp/x.lst",
|
||||
"acl1 src -f /etc/haproxy/admins.lst",
|
||||
@@ -249,23 +246,19 @@ def test_wizard_pydantic_rejects_acl_with_file_flag():
|
||||
"acl1 path -f",
|
||||
],
|
||||
)
|
||||
def test_wizard_pydantic_rejects_acl_with_file_flag_variants(rule):
|
||||
"""Every spacing / position variant the operator might type must
|
||||
be rejected. Pinned defensively so the regex never accidentally
|
||||
relaxes to "only matches trailing -f".
|
||||
"""
|
||||
def test_wizard_pydantic_accepts_acl_with_file_flag_variants(rule):
|
||||
"""Every spacing / position variant must be accepted verbatim
|
||||
(Issue #38 follow-up — no `-f` shape may be rejected)."""
|
||||
from models.site_wizard import FrontendStep
|
||||
from pydantic import ValidationError
|
||||
|
||||
fe_kwargs = dict(
|
||||
fe = FrontendStep(
|
||||
name="fe1",
|
||||
mode="http",
|
||||
bind_address="*",
|
||||
bind_port=80,
|
||||
acl_rules=[rule],
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
FrontendStep(**fe_kwargs)
|
||||
assert fe.acl_rules == [rule]
|
||||
|
||||
|
||||
def test_wizard_pydantic_does_not_falsely_match_dash_f_inside_token():
|
||||
@@ -291,42 +284,29 @@ def test_wizard_pydantic_does_not_falsely_match_dash_f_inside_token():
|
||||
assert len(fe.acl_rules) == 3
|
||||
|
||||
|
||||
def test_manual_frontend_validator_rejects_acl_with_file_flag():
|
||||
"""Parity check: the manual Frontend API
|
||||
(`models/frontend.py::validate_acl_rules`) must apply the same
|
||||
`-f` rejection. Operators see consistent behaviour from both the
|
||||
wizard and the per-entity frontend page.
|
||||
def test_manual_frontend_validator_accepts_acl_with_file_flag():
|
||||
"""Parity check (Issue #38 follow-up): the manual Frontend API
|
||||
(`models/frontend.py::validate_acl_rules`) must ACCEPT `-f`
|
||||
pattern-file references, same as the wizard and bulk import.
|
||||
"""
|
||||
from models.frontend import FrontendConfig
|
||||
from pydantic import ValidationError
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
FrontendConfig(
|
||||
name="fe1",
|
||||
bind_port=80,
|
||||
mode="http",
|
||||
acl_rules=["acl1 path -i -m reg -f /path"],
|
||||
)
|
||||
msg = str(exc_info.value)
|
||||
assert "-f" in msg or "pattern-file" in msg.lower(), (
|
||||
f"Manual frontend API parity regression: ACL -f flag must be "
|
||||
f"rejected. Got: {msg}"
|
||||
fe = FrontendConfig(
|
||||
name="fe1",
|
||||
bind_port=80,
|
||||
mode="http",
|
||||
acl_rules=["acl1 path -i -m reg -f /path"],
|
||||
)
|
||||
assert fe.acl_rules == ["acl1 path -i -m reg -f /path"]
|
||||
|
||||
|
||||
def test_wizard_pydantic_rejects_structured_redirect_dict_with_file_flag():
|
||||
"""Round-3 audit extension — structured redirect dicts (the
|
||||
alternative shape that `models/site_wizard.py::_validate_redirect_rules`
|
||||
accepts alongside legacy strings) also flow through to
|
||||
`services/haproxy_config.py::_format_redirect_rule` and emit
|
||||
their `condition` / `target` verbatim into the rendered HAProxy
|
||||
directive. Without the dict-aware reject the visual builder's
|
||||
`-f` block could be bypassed by hand-crafting a dict payload
|
||||
against the API — recreating the same `failed to open pattern
|
||||
file` failure at apply time.
|
||||
def test_wizard_pydantic_accepts_structured_redirect_dict_with_file_flag():
|
||||
"""Issue #38 follow-up — structured redirect dicts carrying `-f`
|
||||
pattern-file references in `condition`/`target` are ACCEPTED
|
||||
(the Bulgu #12 dict-aware reject was removed together with the
|
||||
string-rule reject).
|
||||
"""
|
||||
from models.site_wizard import FrontendStep, BackendStep
|
||||
from pydantic import ValidationError
|
||||
from models.site_wizard import FrontendStep
|
||||
|
||||
fe_kwargs = dict(
|
||||
name="fe1",
|
||||
@@ -334,37 +314,32 @@ def test_wizard_pydantic_rejects_structured_redirect_dict_with_file_flag():
|
||||
mode="http",
|
||||
)
|
||||
|
||||
# `condition` carrying `-f` must be rejected.
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
FrontendStep(
|
||||
**fe_kwargs,
|
||||
redirect_rules=[
|
||||
{
|
||||
"type": "scheme",
|
||||
"target": "https",
|
||||
"condition": "if { src -f /etc/haproxy/admins.lst }",
|
||||
}
|
||||
],
|
||||
)
|
||||
msg = str(exc_info.value)
|
||||
assert "pattern-file" in msg.lower() or "-f" in msg, msg
|
||||
# `condition` carrying `-f` is accepted.
|
||||
fe = FrontendStep(
|
||||
**fe_kwargs,
|
||||
redirect_rules=[
|
||||
{
|
||||
"type": "scheme",
|
||||
"target": "https",
|
||||
"condition": "if { src -f /etc/haproxy/admins.lst }",
|
||||
}
|
||||
],
|
||||
)
|
||||
assert fe.redirect_rules[0]["condition"] == "if { src -f /etc/haproxy/admins.lst }"
|
||||
|
||||
# `target` carrying `-f` must also be rejected (defence-in-depth
|
||||
# for hand-crafted payloads).
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
FrontendStep(
|
||||
**fe_kwargs,
|
||||
redirect_rules=[
|
||||
{
|
||||
"type": "location",
|
||||
"target": "/foo -f /tmp/x.lst",
|
||||
}
|
||||
],
|
||||
)
|
||||
msg = str(exc_info.value)
|
||||
assert "pattern-file" in msg.lower() or "-f" in msg, msg
|
||||
# `target` carrying `-f` is accepted too.
|
||||
fe = FrontendStep(
|
||||
**fe_kwargs,
|
||||
redirect_rules=[
|
||||
{
|
||||
"type": "location",
|
||||
"target": "/foo -f /tmp/x.lst",
|
||||
}
|
||||
],
|
||||
)
|
||||
assert fe.redirect_rules[0]["target"] == "/foo -f /tmp/x.lst"
|
||||
|
||||
# Clean structured dict still passes — no false positive.
|
||||
# Clean structured dict still passes.
|
||||
FrontendStep(
|
||||
**fe_kwargs,
|
||||
redirect_rules=[
|
||||
@@ -667,8 +642,8 @@ def test_user_reported_wizard_config_emits_no_false_warnings():
|
||||
zero WARNINGs from the directives we expanded.
|
||||
"""
|
||||
# Distilled from the user's bulk-site-create snapshot, minus the
|
||||
# `-f` ACL (which the new Pydantic gate rejects before this
|
||||
# validator ever runs).
|
||||
# `-f` ACL (accepted since the Issue #38 follow-up, but irrelevant
|
||||
# to the directive-expansion warnings this test pins).
|
||||
config = """# ─── Wizard candidate fragment (dry-run preview) ───
|
||||
frontend fe-site1
|
||||
bind *:80
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.8",
|
||||
"releaseName": "SPOE filter + frontend log-format support (Issue #38)",
|
||||
"releaseDate": "2026-07-10"
|
||||
"version": "1.8.9",
|
||||
"releaseName": "ACL -f pattern-file support (Issue #38 follow-up)",
|
||||
"releaseDate": "2026-07-13"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "haproxy-openmanager-frontend",
|
||||
"version": "1.8.8",
|
||||
"version": "1.8.9",
|
||||
"description": "HAProxy Load Balancer Management UI",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
|
||||
@@ -53,19 +53,19 @@ const MATCH_TYPE_GROUPS = [
|
||||
{ label: 'Advanced', options: MATCH_TYPES.filter(m => m.category === 'Advanced') },
|
||||
];
|
||||
|
||||
// Phase K Phase D follow-up (Bulgu #12 round 3) — the `-f <file>`
|
||||
// flag was removed from the visual builder because HAProxy OpenManager
|
||||
// does not provision pattern files onto the HAProxy node filesystem.
|
||||
// Allowing `-f` in the visual builder produced ACL rules that passed
|
||||
// every UI / Pydantic / heuristic check but ALWAYS failed HAProxy's
|
||||
// real `-c` parse at apply time with "failed to open pattern file".
|
||||
// Operators reported a multi-page wizard run ending at the Apply
|
||||
// Management red-badge for a footgun the UI made trivial to step on.
|
||||
// The Pydantic validators on the manual API + wizard reject `-f`
|
||||
// universally; the visual builder simply removes the option from the
|
||||
// dropdown so operators cannot author the unsupported state.
|
||||
// Issue #38 follow-up — `-f <file>` is back in the visual builder:
|
||||
// the Bulgu #12 removal (and the matching Pydantic rejects) assumed a
|
||||
// missing pattern file would surprise the operator at apply time, but
|
||||
// the agent runs `haproxy -c` before every reload so a missing file
|
||||
// fails safely (previous config keeps running), and bulk import plus
|
||||
// the free-form fields always accepted `-f`. Pattern files are
|
||||
// operator-managed host files, same policy as SPOE filter configs
|
||||
// (v1.8.8). The value field carries the file path (e.g. flag `-f`
|
||||
// + value `/etc/haproxy/blacklist.lst`); an informational note is
|
||||
// rendered on rules that use it.
|
||||
const FLAGS = [
|
||||
{ value: '-i', label: '-i (case insensitive)' },
|
||||
{ value: '-f', label: '-f (pattern file on host)' },
|
||||
{ value: '-m beg', label: '-m beg (begins with)' },
|
||||
{ value: '-m end', label: '-m end (ends with)' },
|
||||
{ value: '-m sub', label: '-m sub (contains)' },
|
||||
@@ -396,25 +396,23 @@ function ACLDefinitionCard({ rule, index, onChange, onDelete }) {
|
||||
};
|
||||
const isRaw = rule.raw !== undefined;
|
||||
|
||||
// Phase K Phase D follow-up (Bulgu #12 round 3) — surface `-f` flag
|
||||
// usage inline. The Pydantic validator rejects the rule server-side,
|
||||
// but operators benefit from seeing the error AS they type / when
|
||||
// they re-open a draft that carries a `-f`-flagged rule (e.g. from
|
||||
// a pre-fix draft). The error message matches the Pydantic error
|
||||
// verbatim so support flows are consistent.
|
||||
// Issue #38 follow-up — `-f <file>` pattern-file references are
|
||||
// ACCEPTED now (the Bulgu #12 reject was removed server-side too).
|
||||
// We still detect them, but only to render an informational note:
|
||||
// the referenced file is operator-managed and must exist on every
|
||||
// HAProxy host; a missing file fails safely at the agent's
|
||||
// pre-reload `haproxy -c`.
|
||||
const rawHasFileFlag = isRaw && typeof rule.raw === 'string' && ACL_FILE_FLAG_PATTERN.test(rule.raw);
|
||||
const structuredHasFileFlag =
|
||||
!isRaw && Array.isArray(rule.flags) && rule.flags.includes('-f');
|
||||
const hasFileFlag = rawHasFileFlag || structuredHasFileFlag;
|
||||
const cardStyleWithError = hasFileFlag
|
||||
? { ...ruleCardStyle, border: `1px solid ${token.colorError}` }
|
||||
: ruleCardStyle;
|
||||
const cardStyleWithError = ruleCardStyle;
|
||||
const FILE_FLAG_TOOLTIP =
|
||||
"ACL pattern-file references (-f <file>) are not supported by "
|
||||
+ "HAProxy OpenManager: the product does not provision pattern "
|
||||
+ "files onto the HAProxy node filesystem, so the reference "
|
||||
+ "would fail at HAProxy reload time. Remove '-f' and use inline "
|
||||
+ "values instead.";
|
||||
"This rule references a pattern file (-f <file>). The file must "
|
||||
+ "exist at that exact path on every HAProxy host in the cluster — "
|
||||
+ "HAProxy OpenManager does not create or distribute pattern files. "
|
||||
+ "A missing file fails safely at 'haproxy -c' (the previous config "
|
||||
+ "keeps running).";
|
||||
|
||||
if (isRaw) {
|
||||
return (
|
||||
@@ -427,11 +425,10 @@ function ACLDefinitionCard({ rule, index, onChange, onDelete }) {
|
||||
onChange={(e) => onChange(index, { raw: e.target.value })}
|
||||
placeholder="Raw ACL rule (e.g. my_acl path_beg /api)"
|
||||
prefix={<Tag color="default" style={{ marginRight: 4 }}>RAW</Tag>}
|
||||
status={hasFileFlag ? 'error' : undefined}
|
||||
/>
|
||||
</Tooltip>
|
||||
{hasFileFlag && (
|
||||
<Text type="danger" style={{ fontSize: 11, display: 'block', marginTop: 2 }}>
|
||||
<Text type="secondary" style={{ fontSize: 11, display: 'block', marginTop: 2 }}>
|
||||
{FILE_FLAG_TOOLTIP}
|
||||
</Text>
|
||||
)}
|
||||
@@ -549,12 +546,11 @@ function ACLDefinitionCard({ rule, index, onChange, onDelete }) {
|
||||
onChange={(e) => onChange(index, { ...rule, value: e.target.value })}
|
||||
placeholder={matchDef?.placeholder || 'Value'}
|
||||
size="small"
|
||||
status={structuredHasFileFlag ? 'error' : undefined}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
{structuredHasFileFlag && (
|
||||
<Text type="danger" style={{ fontSize: 11, display: 'block', marginTop: 2 }}>
|
||||
<Text type="secondary" style={{ fontSize: 11, display: 'block', marginTop: 2 }}>
|
||||
{FILE_FLAG_TOOLTIP}
|
||||
</Text>
|
||||
)}
|
||||
@@ -891,11 +887,11 @@ export default function ACLRuleBuilder({ aclRules = [], useBackendRules = [], re
|
||||
.map(d => d.name);
|
||||
}, [aclDefs]);
|
||||
|
||||
// Phase K Phase D follow-up (Bulgu #12 round 3) — count rules that
|
||||
// still carry the unsupported `-f <file>` flag. Surfaced as a
|
||||
// section-level Alert so operators know the section as a whole
|
||||
// has invalid rules even if individual cards / raw text would
|
||||
// otherwise need scrolling to find them.
|
||||
// Issue #38 follow-up — count rules that reference a `-f <file>`
|
||||
// pattern file. Surfaced as a section-level informational Alert
|
||||
// (non-blocking): the file is operator-managed and must exist on
|
||||
// every HAProxy host; a missing file fails safely at the agent's
|
||||
// pre-reload `haproxy -c`.
|
||||
const fileFlagRuleCount = useMemo(() => {
|
||||
let count = 0;
|
||||
for (const d of aclDefs) {
|
||||
@@ -1153,19 +1149,19 @@ export default function ACLRuleBuilder({ aclRules = [], useBackendRules = [], re
|
||||
Define named conditions to match incoming requests by path, header, source IP, and more.
|
||||
</Text>
|
||||
|
||||
{/* Phase K Phase D follow-up (Bulgu #12 round 3) — section-
|
||||
level warning when one or more rules still carry the
|
||||
unsupported `-f <file>` pattern-file flag. Render as a
|
||||
blocking-style Alert so the operator notices BEFORE
|
||||
Submit. The Pydantic validator rejects the same shape
|
||||
server-side; this is the up-front authoring guardrail. */}
|
||||
{/* Issue #38 follow-up — section-level informational note
|
||||
when one or more rules reference `-f <file>` pattern
|
||||
files. Non-blocking: pattern files are operator-managed
|
||||
host files (the Bulgu #12 reject was removed) and a
|
||||
missing file fails safely at the agent's pre-reload
|
||||
`haproxy -c`. */}
|
||||
{fileFlagRuleCount > 0 && (
|
||||
<Alert
|
||||
type="error"
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 8 }}
|
||||
message={`${fileFlagRuleCount} ACL rule${fileFlagRuleCount === 1 ? '' : 's'} use the unsupported \`-f <file>\` flag`}
|
||||
description="HAProxy OpenManager does not provision pattern files onto the HAProxy node filesystem, so any `-f /path/...` reference would fail HAProxy reload at apply time with 'failed to open pattern file'. Remove the `-f` flag and switch to inline values (e.g. `src 10.0.0.0/24` instead of `src -f /etc/haproxy/admins.lst`)."
|
||||
message={`${fileFlagRuleCount} ACL rule${fileFlagRuleCount === 1 ? '' : 's'} reference a \`-f <file>\` pattern file`}
|
||||
description="The referenced file must exist at that exact path on every HAProxy host in the cluster — HAProxy OpenManager does not create or distribute pattern files. A missing file fails safely at 'haproxy -c' (the previous config keeps running)."
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -866,31 +866,13 @@ const FrontendManagement = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Phase K Phase D follow-up (Bulgu #12 round 3) — hard-gate any
|
||||
// ACL / use_backend / redirect rule that carries the unsupported
|
||||
// HAProxy `-f <file>` pattern-file flag. The Pydantic validator
|
||||
// on the backend (`models/frontend.py::validate_acl_rules`)
|
||||
// rejects the same shape; blocking here surfaces the error
|
||||
// immediately at the manual frontend form and matches the wizard
|
||||
// gate so operators see consistent behaviour between the two
|
||||
// entry points.
|
||||
const FILE_FLAG_RE = /(?:^|\s)-f(?:\s|$)/;
|
||||
const aclRulesAll = [
|
||||
...(aclBuilderData.aclRules || []),
|
||||
...(aclBuilderData.useBackendRules || []),
|
||||
...(aclBuilderData.redirectRules || []).map(
|
||||
(r) => (typeof r === 'string' ? r : ''),
|
||||
),
|
||||
];
|
||||
if (aclRulesAll.some((r) => typeof r === 'string' && FILE_FLAG_RE.test(r))) {
|
||||
message.error(
|
||||
'One or more ACL / routing / redirect rules use the unsupported HAProxy ' +
|
||||
'`-f <file>` pattern-file flag. HAProxy OpenManager does not provision ' +
|
||||
'pattern files onto the HAProxy node filesystem, so the reference would ' +
|
||||
'fail at reload time. Remove the `-f` flag and use inline values instead.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Issue #38 follow-up — the Bulgu #12 client-side hard gate for
|
||||
// the ACL `-f <file>` pattern-file flag was removed together with
|
||||
// the server-side Pydantic rejects: pattern files are operator-
|
||||
// managed host files (same policy as SPOE filter configs since
|
||||
// v1.8.8) and the agent's pre-reload `haproxy -c` makes a missing
|
||||
// file fail safely. The server response now carries a non-blocking
|
||||
// warning listing the referenced files (rendered below).
|
||||
|
||||
// Phase K Phase D follow-up (Bulgu #13) — gate for
|
||||
// self-contradictory routing / redirect conditions (`X !X`).
|
||||
@@ -1182,8 +1164,31 @@ const FrontendManagement = () => {
|
||||
} else {
|
||||
message.success('Frontend created successfully');
|
||||
}
|
||||
|
||||
// Issue #38 follow-up — surface server-emitted warnings on
|
||||
// CREATE too (e.g. the `-f <file>` pattern-file advisory).
|
||||
// Mirrors the update-branch rendering above.
|
||||
const createWarnings = Array.isArray(response.data?.warnings)
|
||||
? response.data.warnings
|
||||
: [];
|
||||
if (createWarnings.length > 0) {
|
||||
message.warning(
|
||||
<div>
|
||||
<div><strong>Frontend saved, but the server flagged {createWarnings.length} rule warning(s):</strong></div>
|
||||
<div style={{ marginTop: 6, fontSize: '12px', fontFamily: 'monospace' }}>
|
||||
{createWarnings.slice(0, 5).map((w, i) => (
|
||||
<div key={i}>• {w.length > 240 ? `${w.slice(0, 237)}...` : w}</div>
|
||||
))}
|
||||
{createWarnings.length > 5 && (
|
||||
<div>(+{createWarnings.length - 5} more)</div>
|
||||
)}
|
||||
</div>
|
||||
</div>,
|
||||
10,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
setModalVisible(false);
|
||||
fetchFrontends();
|
||||
fetchSSLCertificates(); // Refresh SSL certificates after frontend update
|
||||
|
||||
@@ -3177,36 +3177,15 @@ const SiteWizard = () => {
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Phase K Phase D follow-up (Bulgu #12 round 3) —
|
||||
// hard-gate the Step 2 → Step 3 advance on any ACL
|
||||
// rule that carries the unsupported `-f <file>`
|
||||
// pattern-file flag. The Pydantic validator rejects
|
||||
// the same shape at submit, but blocking the Next
|
||||
// button here surfaces the error immediately at
|
||||
// its source step (the ACL builder is right above)
|
||||
// instead of bouncing the operator from Step 4's
|
||||
// dry-run card back to Step 2 with a less-specific
|
||||
// jumpback button. The ACLRuleBuilder ALSO renders
|
||||
// a section-level red Alert when this state is
|
||||
// active so the operator already sees what to fix.
|
||||
const FILE_FLAG_RE = /(?:^|\s)-f(?:\s|$)/;
|
||||
const aclRulesAll = [
|
||||
...(aclBuilderData.aclRules || []),
|
||||
...(aclBuilderData.useBackendRules || []),
|
||||
...(aclBuilderData.redirectRules || []).map(
|
||||
(r) => (typeof r === 'string' ? r : ''),
|
||||
),
|
||||
];
|
||||
if (aclRulesAll.some((r) => typeof r === 'string' && FILE_FLAG_RE.test(r))) {
|
||||
message.error(
|
||||
'One or more rules use the unsupported HAProxy `-f <file>` ' +
|
||||
'pattern-file flag. HAProxy OpenManager does not provision ' +
|
||||
'pattern files onto the HAProxy node filesystem, so the ' +
|
||||
'reference would fail at reload time. Remove the `-f` flag ' +
|
||||
'and use inline values instead before continuing.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Issue #38 follow-up — the Bulgu #12 Step 2 → 3
|
||||
// hard gate for the ACL `-f <file>` pattern-file
|
||||
// flag was removed together with the server-side
|
||||
// Pydantic rejects: pattern files are operator-
|
||||
// managed host files (same policy as SPOE filter
|
||||
// configs since v1.8.8) and the agent's pre-reload
|
||||
// `haproxy -c` makes a missing file fail safely.
|
||||
// The ACLRuleBuilder renders an informational note
|
||||
// on `-f` rules instead of a blocking error.
|
||||
// Phase K Phase D follow-up (Bulgu #13) — block
|
||||
// advance when any routing / redirect rule has a
|
||||
// self-contradictory condition (`acl1 !acl1`).
|
||||
|
||||
Reference in New Issue
Block a user