feat: add policy enforcement mode normalization and audit mode protection for firewall apply operations
Add normalized_policy_definition helper to validate and default enforcement_mode to "enforced" or "audit" when creating/updating policies, extend firewall_apply to block live apply when policy is in audit mode with explanatory message, add policy_mode field to all firewall apply response paths, update FirewallPreview UI to show enforcement mode in policy dropdown with version number, display audit mode warning
This commit is contained in:
@@ -261,6 +261,13 @@ def flow_int(value: object, default: int = 0) -> int:
|
||||
return default
|
||||
|
||||
|
||||
def normalized_policy_definition(definition: dict) -> dict:
|
||||
normalized = dict(definition or {})
|
||||
mode = str(normalized.get("enforcement_mode") or "enforced").lower()
|
||||
normalized["enforcement_mode"] = mode if mode in {"enforced", "audit"} else "enforced"
|
||||
return normalized
|
||||
|
||||
|
||||
def subnet_label_for_ip(subnets: list[Subnet], value: str) -> str | None:
|
||||
try:
|
||||
address = ip_address(value)
|
||||
@@ -1339,7 +1346,7 @@ def policies(_: CurrentUser, db: Session = Depends(get_db)) -> list[Policy]:
|
||||
|
||||
@api_router.post("/policies", response_model=PolicyRead)
|
||||
def create_policy(payload: PolicyCreate, user: CurrentUser, db: Session = Depends(get_db)) -> Policy:
|
||||
policy = Policy(project_id=payload.project_id, name=payload.name, enabled=payload.enabled, definition=payload.definition)
|
||||
policy = Policy(project_id=payload.project_id, name=payload.name, enabled=payload.enabled, definition=normalized_policy_definition(payload.definition))
|
||||
db.add(policy)
|
||||
commit_or_400(db)
|
||||
db.refresh(policy)
|
||||
@@ -1356,7 +1363,7 @@ def update_policy(policy_id: str, payload: PolicyCreate, user: CurrentUser, db:
|
||||
policy.project_id = payload.project_id
|
||||
policy.name = payload.name
|
||||
policy.enabled = payload.enabled
|
||||
policy.definition = payload.definition
|
||||
policy.definition = normalized_policy_definition(payload.definition)
|
||||
policy.version += 1
|
||||
commit_or_400(db)
|
||||
db.refresh(policy)
|
||||
@@ -1410,16 +1417,27 @@ async def firewall_apply(payload: FirewallApplyRequest, user: CurrentUser, db: S
|
||||
if not policy or not cluster:
|
||||
raise HTTPException(status_code=404, detail="Policy or cluster not found")
|
||||
preview = resolve_firewall_preview(db, cluster, await FirewallOrchestrator().preview(cluster, policy))
|
||||
writable_rules = [rule for rule in preview.generated_rules if not rule.get("audit_only")]
|
||||
policy_mode = policy.enforcement_mode
|
||||
if payload.dry_run:
|
||||
result = {
|
||||
"applied": False,
|
||||
"dry_run": True,
|
||||
"policy_mode": policy_mode,
|
||||
"reason": "Dry run completed. No firewall rules were applied.",
|
||||
"rules": preview.generated_rules,
|
||||
}
|
||||
elif preview.generated_rules and not writable_rules:
|
||||
result = {
|
||||
"applied": False,
|
||||
"policy_mode": policy_mode,
|
||||
"reason": "Policy is in audit mode. Audit policies do not write Proxmox firewall rules. Change the policy mode to enforced before live apply.",
|
||||
"rules": preview.generated_rules,
|
||||
}
|
||||
elif preview.conflicts:
|
||||
result = {
|
||||
"applied": False,
|
||||
"policy_mode": policy_mode,
|
||||
"reason": "Live apply stopped because the preview has unresolved conflicts.",
|
||||
"conflicts": preview.conflicts,
|
||||
"rules": preview.generated_rules,
|
||||
@@ -1439,9 +1457,12 @@ async def firewall_apply(payload: FirewallApplyRequest, user: CurrentUser, db: S
|
||||
except Exception as exc:
|
||||
result = {
|
||||
"applied": False,
|
||||
"policy_mode": policy_mode,
|
||||
"reason": f"Provider apply failed: {exc}",
|
||||
"rules": preview.generated_rules,
|
||||
}
|
||||
else:
|
||||
result["policy_mode"] = policy_mode
|
||||
applied = bool(result.get("applied"))
|
||||
operation_success = applied or payload.dry_run
|
||||
job = Job(
|
||||
|
||||
Reference in New Issue
Block a user