Release / verify (push) Canceled after 0s
CI / checks (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / backend (push) Canceled after 0s
Release / test (push) Canceled after 0s
Release / publish (backend) (push) Canceled after 0s
Release / publish (frontend) (push) Canceled after 0s
Release / publish (proxy) (push) Canceled after 0s
Release / publish (updater) (push) Canceled after 0s
- **Regel-Bereinigung: Entfernen, Bestätigen, Löschen, Wiederherstellen:** Im Tab **Bereinigung** (Regelwerk › Regel-Analyse) lassen sich Kandidaten jetzt auswählen (Checkbox, höchstens so viele Regeln je Lauf wie in den Einstellungen; „vermutlich“ nur mit einer ausdrücklichen Einzelbestätigung je Regel, „unbekannt“ nie). **Vorschau** zeigt je Firewall (im Cluster zuerst der Backup) die Regeln mit Befund, Sicherheit und Schutzprüfung; **Ausrollen** deaktiviert die Regeln und wendet an. Danach läuft ein Countdown: **Bestätigen** heißt „ich erreiche die Firewall noch“, ohne Bestätigung setzt OPNexus **automatisch zurück**; bis dahin geht auch **Jetzt zurücksetzen**. Bestätigte Regeln lassen sich nach der Wartezeit (einstellbar, Standard 7 Tage) **löschen**; vorher sichert OPNexus den Regelinhalt, und unter **Verlauf der Bereinigungen** kommt die Regel mit **Wiederherstellen** deaktiviert und möglichst an alter Position zurück (sonst mit Hinweis am Ende der Gruppe); **Wieder aktivieren** hat wieder ein Bestätigungsfenster. Wichtig: **Ausrollen, Wiederholen und Wiederherstellen darf nur ein Administrator**, eine Vorschau gehört dem, der sie erstellt hat (ein Admin öffnet selbst eine neue); **Bestätigen geht nur in der Browser-Sitzung**, nie per API-Token. **Die Bereinigung bleibt gesperrt, bis unter Einstellungen die Verwaltungsquellen** (IP-Adressen oder Netze, von denen aus verwaltet wird) **eingetragen sind**: Regeln, die diesen Zugang treffen könnten, deaktiviert OPNexus nie. Dort stehen auch die Obergrenze je Lauf, die Wartezeit bis zum Löschen und **weitere Verwaltungsports** (zusätzlich zu API-Port, 443, 80, 22). Tragen Sie die Adresse ein, mit der OPNexus selbst bei den Firewalls ankommt (im Cluster für jeden Knoten). Quellen, Ports und Firewall-Adressen sehen nur Administratoren. Der Hinweis „Entfernen folgt“ entfällt. - **Update: neuere CLI aus dem Zielrelease:** `opnexus update` (auch per Klick im Webinterface) holt sich nach dem Pull die CLI aus dem Updater-Image des Zielreleases und startet sich damit neu, damit Korrekturen am Update-Ablauf sofort greifen statt erst beim übernächsten Update. Das geschieht nur, wenn `cosign` und `cosign.pub` vorhanden sind und die Signatur des Image-**Digests** in diesem Lauf geprüft wurde (ausgeführt wird genau dieser Digest); sonst und bei jedem Fehler läuft das Update mit der vorhandenen CLI weiter. Abschalten: `OPNEXUS_NO_CLI_REFRESH=1`. Wirksam wird es ab dem Update *von* dieser Version.
141 lines
6.6 KiB
Python
141 lines
6.6 KiB
Python
"""Separation of duties: with ``operators_may_deploy`` off, only admins roll changes out (AuthMiddleware enforces it).
|
|
|
|
The rule-cleanup settings (1.31) live here too: how many rules one cleanup may touch, how long a rule must stay
|
|
switched off before it may be deleted, the management sources (addresses the firewalls are managed from; no rule
|
|
that could match them is ever cleaned up) and extra management ports (besides the API port, 443, 80 and 22). Only an admin writes any of it (middleware: ``/api/settings/...`` is
|
|
admin-only for operators and tokens below scope admin; checked again here) and every change is audited with the
|
|
old and the new values."""
|
|
import ipaddress
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, Depends
|
|
from pydantic import BaseModel, Field
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.database import get_db
|
|
from app.errors import ErrorCode, api_error
|
|
from app.models.governance import GovernanceSettings
|
|
from app.models.user import AdminUser
|
|
from app.routers.auth import current_user
|
|
from app.services import audit_service
|
|
|
|
router = APIRouter(prefix="/settings/governance", tags=["governance"])
|
|
|
|
MAX_CLEANUP_RULES = 25 # same ceiling as services/rule_cleanup_service.MAX_RULES_CEILING
|
|
MAX_DELETE_MIN_DAYS = 365
|
|
MAX_MANAGEMENT_SOURCES = 20
|
|
MAX_MANAGEMENT_PORTS = 20
|
|
|
|
|
|
class GovernanceRead(BaseModel):
|
|
operators_may_deploy: bool
|
|
cleanup_max_rules: int = 10
|
|
cleanup_delete_min_days: int = 7
|
|
# The list itself is shown to admins only; everybody else sees just whether it is set.
|
|
cleanup_management_sources: list[str] = Field(default_factory=list)
|
|
cleanup_management_sources_configured: bool = False
|
|
# Extra management ports (besides API port, 443, 80, 22): also admins only, others see the flag.
|
|
cleanup_management_ports: list[int] = Field(default_factory=list)
|
|
cleanup_management_ports_configured: bool = False
|
|
|
|
|
|
class GovernanceUpdate(BaseModel):
|
|
operators_may_deploy: bool | None = None
|
|
cleanup_max_rules: int | None = None
|
|
cleanup_delete_min_days: int | None = None
|
|
cleanup_management_sources: list[str] | None = None
|
|
cleanup_management_ports: list[Any] | None = None # validated in clean_management_ports (no bool coercion)
|
|
|
|
|
|
def _bad(field: str, reason: str):
|
|
return api_error(422, ErrorCode.BEREINIGUNG_EINSTELLUNG_UNGUELTIG, field=field, reason=reason)
|
|
|
|
|
|
def clean_management_sources(values: list[str]) -> list[str]:
|
|
"""IP addresses and CIDR networks only (no host names: they could resolve differently later), canonical form,
|
|
without duplicates; not the whole address space, which would protect every rule."""
|
|
if len(values) > MAX_MANAGEMENT_SOURCES:
|
|
raise _bad("cleanup_management_sources", "too_many")
|
|
cleaned: list[str] = []
|
|
for raw in values:
|
|
text = str(raw).strip()
|
|
try:
|
|
net = ipaddress.ip_network(text, strict=False)
|
|
except ValueError:
|
|
raise _bad("cleanup_management_sources", "not_an_address") from None
|
|
if net.prefixlen == 0:
|
|
raise _bad("cleanup_management_sources", "everything")
|
|
entry = str(net.network_address) if net.prefixlen == net.max_prefixlen else str(net)
|
|
if entry not in cleaned:
|
|
cleaned.append(entry)
|
|
return cleaned
|
|
|
|
|
|
def clean_management_ports(values) -> list[int]:
|
|
"""TCP ports 1..65535, at most MAX_MANAGEMENT_PORTS, without duplicates, sorted."""
|
|
if len(values) > MAX_MANAGEMENT_PORTS:
|
|
raise _bad("cleanup_management_ports", "too_many")
|
|
ports: set[int] = set()
|
|
for raw in values:
|
|
text = str(raw).strip()
|
|
if isinstance(raw, bool) or not text.isdigit() or not 1 <= int(text) <= 65535:
|
|
raise _bad("cleanup_management_ports", "not_a_port")
|
|
ports.add(int(text))
|
|
return sorted(ports)
|
|
|
|
|
|
def _read(row: GovernanceSettings, user: AdminUser) -> GovernanceRead:
|
|
sources = list(row.cleanup_management_sources or [])
|
|
ports = list(row.cleanup_management_ports or [])
|
|
return GovernanceRead(
|
|
operators_may_deploy=row.operators_may_deploy, cleanup_max_rules=row.cleanup_max_rules or 10,
|
|
cleanup_delete_min_days=row.cleanup_delete_min_days or 7,
|
|
cleanup_management_sources=sources if user.role == "admin" else [],
|
|
cleanup_management_sources_configured=bool(sources),
|
|
cleanup_management_ports=ports if user.role == "admin" else [],
|
|
cleanup_management_ports_configured=bool(ports))
|
|
|
|
|
|
async def get_settings(db: AsyncSession) -> GovernanceSettings:
|
|
row = await db.get(GovernanceSettings, 1)
|
|
if row is None:
|
|
row = GovernanceSettings(id=1, operators_may_deploy=True)
|
|
db.add(row)
|
|
await db.flush()
|
|
return row
|
|
|
|
|
|
@router.get("", response_model=GovernanceRead)
|
|
async def read(db: AsyncSession = Depends(get_db), user: AdminUser = Depends(current_user)):
|
|
"""Readable by everybody signed in (the UI explains why a deploy was refused)."""
|
|
return _read(await get_settings(db), user)
|
|
|
|
|
|
@router.put("", response_model=GovernanceRead)
|
|
async def update(payload: GovernanceUpdate, db: AsyncSession = Depends(get_db), user: AdminUser = Depends(current_user)):
|
|
if user.role != "admin":
|
|
raise api_error(403, ErrorCode.ADMIN_REQUIRED)
|
|
row = await get_settings(db)
|
|
changes: dict[str, dict] = {}
|
|
if payload.cleanup_max_rules is not None:
|
|
if not 1 <= payload.cleanup_max_rules <= MAX_CLEANUP_RULES:
|
|
raise _bad("cleanup_max_rules", f"1..{MAX_CLEANUP_RULES}")
|
|
if payload.cleanup_delete_min_days is not None:
|
|
if not 1 <= payload.cleanup_delete_min_days <= MAX_DELETE_MIN_DAYS:
|
|
raise _bad("cleanup_delete_min_days", f"1..{MAX_DELETE_MIN_DAYS}")
|
|
sources = clean_management_sources(payload.cleanup_management_sources) if payload.cleanup_management_sources is not None else None
|
|
ports = clean_management_ports(payload.cleanup_management_ports) if payload.cleanup_management_ports is not None else None
|
|
for field, value in (("cleanup_max_rules", payload.cleanup_max_rules), ("cleanup_delete_min_days", payload.cleanup_delete_min_days),
|
|
("cleanup_management_sources", sources), ("cleanup_management_ports", ports)):
|
|
if value is not None and value != getattr(row, field):
|
|
changes[field] = {"old": getattr(row, field), "new": value}
|
|
setattr(row, field, value)
|
|
if payload.operators_may_deploy is not None:
|
|
row.operators_may_deploy = payload.operators_may_deploy
|
|
if changes:
|
|
db.add(audit_service.make_event(
|
|
kind="settings", action="cleanup_settings", status="accepted", entity_id="governance",
|
|
message="Einstellungen der Regel-Bereinigung geändert", details={"changes": changes}))
|
|
await db.commit()
|
|
return _read(row, user)
|