This commit is contained in:
Jonas Bengtsson
2026-09-16 06:08:37 +02:00
parent cce6a4960e
commit 8d2c54beae
54 changed files with 8480 additions and 41 deletions
+210
View File
@@ -0,0 +1,210 @@
"""ORM models — Release 1 subset (spec sections 39, 9, 11, 13, 14, 26).
Dynamic Security objects (clients/groups/roles) are NOT stored here; the
broker owns them (spec section 40). PostgreSQL holds management users,
RBAC, broker config model + revisions, and MQTT client metadata.
"""
from __future__ import annotations
import enum
import uuid
from datetime import UTC, datetime
from sqlalchemy import (
JSON,
Boolean,
DateTime,
Enum,
ForeignKey,
Integer,
String,
Text,
UniqueConstraint,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db import Base
def utcnow() -> datetime:
"""Timezone-aware UTC now — Postgres timestamptz requires aware values."""
return datetime.now(UTC)
# --------------------------------------------------------------------------
# Management identities + RBAC (spec sections 8, 9)
# --------------------------------------------------------------------------
class ManagementRole(str, enum.Enum):
"""Built-in platform roles (spec section 9). Custom roles: later release."""
SUPER_ADMIN = "super_admin"
BROKER_ADMIN = "broker_admin"
PKI_ADMIN = "pki_admin"
OPERATOR = "operator"
AUDITOR = "auditor"
class ManagementUser(Base):
__tablename__ = "users"
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
username: Mapped[str] = mapped_column(String(100), unique=True, index=True)
# Argon2id hash (spec section 43); never the plaintext password.
password_hash: Mapped[str] = mapped_column(String(255))
display_name: Mapped[str | None] = mapped_column(String(200))
role: Mapped[ManagementRole] = mapped_column(
Enum(ManagementRole, name="management_role"),
default=ManagementRole.BROKER_ADMIN,
)
enabled: Mapped[bool] = mapped_column(Boolean, default=True)
must_change_password: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
modified_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=utcnow, onupdate=utcnow
)
class ManagementSession(Base):
"""Server-side session store (spec section 8: revocable, expiring)."""
__tablename__ = "sessions"
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
user_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("users.id", ondelete="CASCADE"), index=True
)
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True)
revoked: Mapped[bool] = mapped_column(Boolean, default=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
# --------------------------------------------------------------------------
# Broker configuration model + revisions (spec sections 25, 26, 30, 31, 61)
# --------------------------------------------------------------------------
class ListenerConfig(Base):
"""One Mosquitto listener as managed through the UI (spec section 30)."""
__tablename__ = "broker_listeners"
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
name: Mapped[str] = mapped_column(String(100))
enabled: Mapped[bool] = mapped_column(Boolean, default=True)
port: Mapped[int] = mapped_column(Integer)
bind_address: Mapped[str | None] = mapped_column(String(100))
protocol: Mapped[str] = mapped_column(String(20), default="mqtt") # mqtt|websockets
allow_anonymous: Mapped[bool] = mapped_column(Boolean, default=False)
max_connections: Mapped[int] = mapped_column(Integer, default=-1)
# TLS settings (used once PKI ships in Release 2; kept now so the
# config model is stable from the start).
tls_enabled: Mapped[bool] = mapped_column(Boolean, default=False)
tls_min_version: Mapped[str | None] = mapped_column(String(20))
require_certificate: Mapped[bool] = mapped_column(Boolean, default=False)
use_identity_as_username: Mapped[bool] = mapped_column(Boolean, default=False)
cert_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("certificates.id"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
modified_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=utcnow, onupdate=utcnow
)
class BrokerSettings(Base):
"""Key/value store for global broker options (spec section 31)."""
__tablename__ = "broker_settings"
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
key: Mapped[str] = mapped_column(String(100), unique=True, index=True)
value: Mapped[str] = mapped_column(Text)
value_type: Mapped[str] = mapped_column(String(20), default="string")
class ConfigRevision(Base):
"""Generated mosquitto.conf revisions (spec sections 26, 61)."""
__tablename__ = "broker_config_revisions"
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
revision_number: Mapped[int] = mapped_column(Integer, unique=True, index=True)
created_by: Mapped[str | None] = mapped_column(String(100))
comment: Mapped[str | None] = mapped_column(Text)
# Full generated mosquitto.conf text
config_text: Mapped[str] = mapped_column(Text)
validation_result: Mapped[dict] = mapped_column(JSON, default=dict)
deployed: Mapped[bool] = mapped_column(Boolean, default=False)
deployed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
__table_args__ = (
UniqueConstraint("revision_number", name="uq_config_revision_number"),
)
# --------------------------------------------------------------------------
# PKI placeholders (populated in Release 2; referenced by listeners)
# --------------------------------------------------------------------------
class Certificate(Base):
__tablename__ = "certificates"
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
common_name: Mapped[str] = mapped_column(String(200))
certificate_pem: Mapped[str] = mapped_column(Text)
# Encrypted at rest (AES-256-GCM) once keys are managed (Release 2).
private_key_pem: Mapped[str | None] = mapped_column(Text)
expiry_date: Mapped[datetime] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
# --------------------------------------------------------------------------
# MQTT client metadata (spec sections 11, 40) — dynsec owns the auth data,
# PostgreSQL stores the descriptive/ownership metadata.
# --------------------------------------------------------------------------
class MqttClientMetadata(Base):
__tablename__ = "mqtt_client_metadata"
username: Mapped[str] = mapped_column(String(100), primary_key=True)
display_name: Mapped[str | None] = mapped_column(String(200))
description: Mapped[str | None] = mapped_column(Text)
owner: Mapped[str | None] = mapped_column(String(200))
purpose: Mapped[str | None] = mapped_column(String(200))
tags: Mapped[list] = mapped_column(JSON, default=list)
last_known_connection: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
modified_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=utcnow, onupdate=utcnow
)
# Certificate link becomes active in Release 2 (mTLS identity binding).
certificate_id: Mapped[uuid.UUID | None] = mapped_column(
ForeignKey("certificates.id", ondelete="SET NULL")
)
# --------------------------------------------------------------------------
# Audit log (spec section 38) — append-only from the application's view.
# --------------------------------------------------------------------------
class AuditEvent(Base):
__tablename__ = "audit_events"
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
timestamp: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=utcnow, index=True
)
user: Mapped[str | None] = mapped_column(String(100))
source_ip: Mapped[str | None] = mapped_column(String(64))
action: Mapped[str] = mapped_column(String(100), index=True)
object_type: Mapped[str | None] = mapped_column(String(100))
object_id: Mapped[str | None] = mapped_column(String(200))
old_value: Mapped[str | None] = mapped_column(JSON)
new_value: Mapped[str | None] = mapped_column(JSON)
result: Mapped[str] = mapped_column(String(20), default="success")
correlation_id: Mapped[str | None] = mapped_column(String(64))