feat: Zonen als first-class Entity + Domain↔Backend-Verknüpfung sichtbar

* Migration 0012: firewall_zones (id, name UNIQUE, description, builtin),
  Seed wan/lan/dmz/mgmt/cluster als builtin. CHECK-Constraints auf
  network_interfaces.role + firewall_rules.{src,dst}_zone +
  firewall_nat_rules.{in,out}_zone gedroppt — Validation lebt jetzt
  app-side (Handler prüft Existenz in firewall_zones).
* Backend: firewall.ZonesRepo (CRUD + Exists + References-Lookup),
  /api/v1/firewall/zones, builtin geschützt (Name nicht änderbar,
  Delete blockiert), Rename eines Custom-Zone aktuell ohne Cascade
  (Handler-Sorge bei Rules/NAT/Networks).
* Handler-Validation in CreateRule/UpdateRule/CreateNAT/UpdateNAT +
  NetworksHandler: Zone-Existence-Check pro Mutation, 400 bei Tippfehler.
* Frontend: Firewall-Tab "Zonen" (CRUD mit builtin-Schutz). Networks-
  Form lädt Rollen aus /firewall/zones (statt hardcoded Liste); Rules-
  und NAT-Forms ziehen die Zone-Auswahl ebenfalls aus der API.
* Domain-Form bekommt Primary-Backend-Picker (Field war im Modell,
  fehlte im UI). Backends-Tabelle zeigt umgekehrt welche Domains
  darauf zeigen — bidirektionale Sicht ohne Schemaänderung.
* HAProxy-Renderer: safeID-FuncMap escaped Server-Namen mit Whitespace
  ("Control Master 1" → "Control_Master_1"). Vorher ist haproxy beim
  Reload an Spaces im Backend-Namen kaputt gegangen.
* Version 1.0.3 → 1.0.6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-10 18:05:27 +02:00
parent aa14b6b2be
commit 51ea1fc802
23 changed files with 782 additions and 37 deletions

View File

@@ -0,0 +1,48 @@
-- +goose Up
-- +goose StatementBegin
-- firewall_zones promotes zones from a hard-coded enum
-- (wan/lan/dmz/mgmt/cluster) into a first-class entity. Operators
-- can add their own (e.g. iot, guest, voip) without a schema
-- change. Existing role/zone TEXT columns on network_interfaces,
-- firewall_rules and firewall_nat_rules continue to store the
-- zone NAME — referential integrity is enforced at the application
-- layer (handler validates name exists in firewall_zones), not by
-- a hard FK, so 'any' on rules and NULL on NAT keep working
-- without special-casing.
--
-- builtin = TRUE marks the seed zones; the API rejects DELETE on
-- those rows to prevent the operator from removing a zone the
-- renderer still expects.
CREATE TABLE IF NOT EXISTS firewall_zones (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
description TEXT,
builtin BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT firewall_zones_name_check CHECK (name ~ '^[a-z][a-z0-9_-]{0,31}$')
);
INSERT INTO firewall_zones (name, description, builtin) VALUES
('wan', 'Public-facing internet uplink', TRUE),
('lan', 'Internal trusted network', TRUE),
('dmz', 'Quarantined service network', TRUE),
('mgmt', 'Admin-only management network', TRUE),
('cluster', 'Inter-node cluster traffic (KeyDB / mTLS API)', TRUE)
ON CONFLICT (name) DO NOTHING;
-- Drop the hard-coded CHECK constraints so the operator can declare
-- new zones without the SQL layer rejecting them. App-side
-- validation in the handlers takes over.
ALTER TABLE network_interfaces DROP CONSTRAINT IF EXISTS network_interfaces_role_check;
ALTER TABLE firewall_rules DROP CONSTRAINT IF EXISTS firewall_rules_src_zone_check;
ALTER TABLE firewall_rules DROP CONSTRAINT IF EXISTS firewall_rules_dst_zone_check;
ALTER TABLE firewall_nat_rules DROP CONSTRAINT IF EXISTS firewall_nat_rules_zone_check;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE IF EXISTS firewall_zones;
-- +goose StatementEnd