🚧 WIP
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -180,6 +180,37 @@ def test_listener_management_and_preview(admin_client):
|
||||
assert delete.status_code == 204
|
||||
|
||||
|
||||
def test_listener_update_and_preview(admin_client):
|
||||
listener_id = admin_client.post(
|
||||
"/api/v1/broker/listeners",
|
||||
json={"name": "Production MQTT", "port": 1883, "protocol": "mqtt"},
|
||||
).json()["id"]
|
||||
|
||||
updated = admin_client.put(
|
||||
f"/api/v1/broker/listeners/{listener_id}",
|
||||
json={
|
||||
"name": "Production MQTT",
|
||||
"port": 1884,
|
||||
"bind_address": "127.0.0.1",
|
||||
"protocol": "websockets",
|
||||
"allow_anonymous": True,
|
||||
},
|
||||
)
|
||||
assert updated.status_code == 200, updated.text
|
||||
body = updated.json()
|
||||
assert body["port"] == 1884
|
||||
assert body["bind_address"] == "127.0.0.1"
|
||||
assert body["protocol"] == "websockets"
|
||||
assert body["allow_anonymous"] is True
|
||||
|
||||
config_text = admin_client.get("/api/v1/broker/config/preview").json()[
|
||||
"config_text"
|
||||
]
|
||||
assert "listener 1884 127.0.0.1" in config_text
|
||||
assert "protocol websockets" in config_text
|
||||
assert "listener_allow_anonymous true" in config_text
|
||||
|
||||
|
||||
def test_audit_events_recorded(admin_client, db_session):
|
||||
from sqlalchemy import select
|
||||
|
||||
|
||||
@@ -46,9 +46,12 @@ def test_generate_includes_global_settings(session):
|
||||
assert "max_packet_size 2000000" in result.config_text
|
||||
|
||||
|
||||
def test_generate_refuses_without_listeners(session):
|
||||
with pytest.raises(ValueError, match="no enabled listeners"):
|
||||
generate_config(session)
|
||||
def test_generate_with_empty_model_warns_instead_of_raising(session):
|
||||
"""Fresh-platform state: preview must work, not 500 (config page fix)."""
|
||||
result = generate_config(session)
|
||||
assert "global_plugin /usr/lib/mosquitto_dynamic_security.so" in result.config_text
|
||||
assert "persistence true" in result.config_text
|
||||
assert any("No enabled listeners" in w for w in result.warnings)
|
||||
|
||||
|
||||
def test_disabled_listeners_are_omitted(session):
|
||||
|
||||
Reference in New Issue
Block a user