This commit is contained in:
Jonas Bengtsson
2026-09-19 09:40:24 +02:00
parent ee4800993d
commit 19bccce083
13 changed files with 435 additions and 43 deletions
+16 -7
View File
@@ -38,10 +38,13 @@ def _listener_block(listener: ListenerConfig, warnings: list[str]) -> list[str]:
lines: list[str] = []
if not listener.enabled:
return lines
lines.append(f"listener {listener.port} {listener.bind_address or ''}".rstrip())
if listener.bind_address is None:
# `listener <port>` without bind address binds all interfaces.
lines[-1] = f"listener {listener.port}"
# Always emit an EXPLICIT bind address. Mosquitto's reload compares
# listeners textually; "listener 1883" vs "listener 1883 0.0.0.0" are
# treated as different listeners, and reload CANNOT add/remove
# listeners (spec section 28). Matching the bootstrap's explicit
# form keeps the management listener reloadable.
bind = listener.bind_address or "0.0.0.0"
lines.append(f"listener {listener.port} {bind}")
lines.append(f"protocol {listener.protocol}")
if listener.allow_anonymous:
lines.append("listener_allow_anonymous true")
@@ -104,11 +107,17 @@ def generate_config(db: Session) -> GenerationResult:
lines.extend(block)
lines.append("")
else:
# Empty model is a legitimate UI state (fresh platform): generate a
# SAFE config instead of raising, so /config/preview works. With no
# listener directives Mosquitto binds loopback-only (127.0.0.1/::1)
# on port 1883 with anonymous access — reachable only inside the
# container network. The warning explains exactly what will happen.
warnings.append(
"no enabled listeners; broker would fall back to loopback-only "
"with anonymous access — refusing to generate"
"No enabled listeners in the configuration model. The generated "
"config contains no listener directives, so Mosquitto will bind "
"loopback-only (127.0.0.1/::1) on port 1883 with anonymous access "
"— internal to the container network only. Add a listener below."
)
raise ValueError("no enabled listeners in configuration model")
lines.append("# --- Persistence ---")
lines.append("persistence true")
+11 -2
View File
@@ -30,6 +30,7 @@ from app.models import (
)
from app.schemas import (
BrokerSettingsIn,
BrokerSettingsOut,
ConfigPreviewOut,
DeployRequest,
ListenerIn,
@@ -126,13 +127,21 @@ def update_listener(
for key, value in body.model_dump().items():
setattr(listener, key, value)
db.flush()
# Audit the security-relevant listener fields (spec section 38): the
# full PUT payload is a replacement, so record what it now says.
audit.record(
db,
"LISTENER_UPDATED",
request=request,
object_type="listener",
object_id=str(listener.id),
new_value={"name": body.name, "port": body.port},
new_value={
"name": body.name,
"port": body.port,
"bind_address": body.bind_address,
"protocol": body.protocol,
"allow_anonymous": body.allow_anonymous,
},
)
return listener
@@ -161,7 +170,7 @@ def delete_listener(
# --- Global settings -----------------------------------------------------------
@router.get("/settings", response_model=list[BrokerSettingsIn])
@router.get("/settings", response_model=list[BrokerSettingsOut])
def list_settings(db: Annotated[Session, Depends(get_db)], _user: AnyReader):
return db.scalars(select(BrokerSettings)).all()
+6
View File
@@ -185,6 +185,12 @@ class BrokerSettingsIn(BaseModel):
value_type: Literal["string", "int", "bool"] = "string"
class BrokerSettingsOut(BrokerSettingsIn):
"""GET /broker/settings rows — same fields, from ORM attributes."""
model_config = {"from_attributes": True}
class ListenerIn(BaseModel):
name: str = Field(min_length=1, max_length=100)
enabled: bool = True