This commit is contained in:
Jonas Bengtsson
2026-09-19 12:00:34 +02:00
parent 553d821fc8
commit adbaab50b3
2 changed files with 78 additions and 1 deletions
+39 -1
View File
@@ -13,6 +13,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request, status
from sqlalchemy.orm import Session
from app import audit
from app.config import get_settings
from app.db import get_db
from app.deps import CurrentUser, require_roles
from app.dynsec import DynsecError, dynsec_client
@@ -36,6 +37,25 @@ BrokerAdmin = Annotated[CurrentUser, Depends(require_roles(ManagementRole.BROKER
DbDep = Annotated[Session, Depends(get_db)]
def _guard_mgmt_identity(username: str) -> None:
"""Refuse operations on the backend's own management identity.
The dynsec client named MOSQUITTO_DYNSEC_USERNAME is the platform's
control-channel service account — the backend connects to the broker
AS this identity (spec section 2.1: management identities are separate
from MQTT identities). Resetting its password or disabling it severs
the management channel: Mosquitto kicks every connection using the
username, and the backend then cannot reconnect with its env-var
password. It is therefore not a manageable MQTT client.
"""
if username == get_settings().mosquitto_dynsec_username:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
f"'{username}' is the broker management identity and cannot "
"be modified through client management",
)
def _metadata(db: Session, username: str) -> MqttClientMetadata | None:
return db.get(MqttClientMetadata, username)
@@ -63,7 +83,12 @@ def _client_out(db: Session, client: dict) -> MqttClientOut:
@router.get("/clients", response_model=list[MqttClientOut])
def list_clients(db: DbDep, _user: BrokerAdmin):
return [_client_out(db, c) for c in dynsec_client.list_clients()]
mgmt = get_settings().mosquitto_dynsec_username
return [
_client_out(db, c)
for c in dynsec_client.list_clients()
if c.get("username") != mgmt
]
@router.post("/clients", response_model=MqttPasswordReset, status_code=201)
@@ -74,6 +99,7 @@ def create_client(
_user: BrokerAdmin,
):
"""Create a dynsec client; the plaintext password is returned ONCE."""
_guard_mgmt_identity(body.username)
password = body.password or generate_password()
try:
dynsec_client.create_client(
@@ -109,6 +135,10 @@ def create_client(
@router.get("/clients/{username}", response_model=MqttClientOut)
def get_client(username: str, db: DbDep, _user: BrokerAdmin):
# Hidden like in the listing: the management identity is not a
# manageable client, so it is reported as not present.
if username == get_settings().mosquitto_dynsec_username:
raise HTTPException(status.HTTP_404_NOT_FOUND, "client not found")
try:
client = dynsec_client.get_client(username)
except DynsecError as exc:
@@ -123,6 +153,7 @@ def delete_client(
db: DbDep,
_user: BrokerAdmin,
):
_guard_mgmt_identity(username)
try:
dynsec_client.delete_client(username)
except DynsecError as exc:
@@ -144,6 +175,7 @@ def delete_client(
def disable_client(
username: str, request: Request, db: DbDep, _user: BrokerAdmin
):
_guard_mgmt_identity(username)
dynsec_client.disable_client(username)
audit.record(
db, "CLIENT_DISABLED", request=request, object_type="mqtt_client", object_id=username
@@ -155,6 +187,7 @@ def disable_client(
def enable_client(
username: str, request: Request, db: DbDep, _user: BrokerAdmin
):
_guard_mgmt_identity(username)
dynsec_client.enable_client(username)
audit.record(
db, "CLIENT_ENABLED", request=request, object_type="mqtt_client", object_id=username
@@ -170,6 +203,7 @@ def reset_password(
_user: BrokerAdmin,
):
"""Generate a new random password; returned once (spec section 11)."""
_guard_mgmt_identity(username)
password = generate_password()
try:
dynsec_client.set_client_password(username, password)
@@ -194,6 +228,7 @@ def assign_client_role(
_user: BrokerAdmin,
):
"""Add one role to an existing client (incremental, spec section 11)."""
_guard_mgmt_identity(username)
try:
dynsec_client.add_client_role(username, body.rolename, body.priority)
except DynsecError as exc:
@@ -219,6 +254,7 @@ def unassign_client_role(
db: DbDep,
_user: BrokerAdmin,
):
_guard_mgmt_identity(username)
try:
dynsec_client.remove_client_role(username, rolename)
except DynsecError as exc:
@@ -290,6 +326,7 @@ def add_group_member(
_user: BrokerAdmin,
):
"""Add one client to a group (spec section 12)."""
_guard_mgmt_identity(body.username)
try:
dynsec_client.add_group_client(groupname, body.username, body.priority)
except DynsecError as exc:
@@ -315,6 +352,7 @@ def remove_group_member(
db: DbDep,
_user: BrokerAdmin,
):
_guard_mgmt_identity(username)
try:
dynsec_client.remove_group_client(groupname, username)
except DynsecError as exc: