Files
timemaster/backend/migrations/versions/0021_kiosk_security.py
T
patrick 981bde3dc1 feat(kiosk): Migration 0021 – Ed25519-Auth, Status-Enum, Heartbeat, IP-Whitelist
Migration 0021_kiosk_security (eingeklinkt zwischen 0020 und 0022):
- kiosk_devices: token_hash + is_active → status enum(pending/approved/revoked)
- kiosk_devices: public_key, key_algorithm, enrollment_token_hash/expires_at
- kiosk_devices: last_heartbeat_at, client_version, offline_queue_size
- kiosk_devices: current_user_id (DSGVO), ip_whitelist (CIDR)
- companies: kiosk_require_approval, kiosk_track_current_user, kiosk_heartbeat_interval_sec

Model KioskDevice: komplett überarbeitet (KioskDeviceStatus Enum)
Model Company: 3 neue Kiosk-Felder

Bestehende Geräte: status=revoked (müssen neu enrolled werden)
Existing servers: SQL manuell angewendet (Alembic skip bei inserted migrations)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 12:08:33 +02:00

144 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""kiosk_security: Ed25519-Public-Key-Auth, Status-Enum, Heartbeat, IP-Whitelist
Revision ID: 0021
Revises: 0020
Create Date: 2026-05-24
Ersetzt token_hash + is_active durch:
- status enum('pending','approved','revoked')
- public_key TEXT (Ed25519 PEM/OpenSSH)
- key_algorithm VARCHAR(20)
- enrollment_token_hash VARCHAR(64) + enrollment_expires_at
- last_heartbeat_at, client_version, offline_queue_size
- current_user_id (DSGVO: pro Firma deaktivierbar)
- ip_whitelist TEXT (CIDR-Liste, optional)
Company-Erweiterungen:
- kiosk_require_approval BOOLEAN DEFAULT TRUE
- kiosk_track_current_user BOOLEAN DEFAULT TRUE
- kiosk_heartbeat_interval_sec INTEGER DEFAULT 30
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID
revision = "0021"
down_revision = "0020"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ── kiosk_devices: neue Spalten ───────────────────────────────────────────
# Status-Enum anlegen
op.execute("""
CREATE TYPE kioskdevicestatus AS ENUM ('pending', 'approved', 'revoked')
""")
# Neue Spalten hinzufügen
op.add_column("kiosk_devices",
sa.Column("status",
sa.Enum("pending", "approved", "revoked", name="kioskdevicestatus"),
nullable=False,
server_default="revoked",
)
)
op.add_column("kiosk_devices",
sa.Column("public_key", sa.Text(), nullable=True)
)
op.add_column("kiosk_devices",
sa.Column("key_algorithm", sa.String(20), nullable=False, server_default="ed25519")
)
op.add_column("kiosk_devices",
sa.Column("enrollment_token_hash", sa.String(64), nullable=True)
)
op.add_column("kiosk_devices",
sa.Column("enrollment_expires_at", sa.DateTime(timezone=True), nullable=True)
)
op.add_column("kiosk_devices",
sa.Column("last_heartbeat_at", sa.DateTime(timezone=True), nullable=True)
)
op.add_column("kiosk_devices",
sa.Column("client_version", sa.String(50), nullable=True)
)
op.add_column("kiosk_devices",
sa.Column("offline_queue_size", sa.Integer(), nullable=False, server_default="0")
)
op.add_column("kiosk_devices",
sa.Column("current_user_id",
UUID(as_uuid=True),
sa.ForeignKey("users.id", ondelete="SET NULL"),
nullable=True,
)
)
op.add_column("kiosk_devices",
sa.Column("ip_whitelist", sa.Text(), nullable=True)
)
# Bestehende aktive Geräte → revoked (müssen neu enrolled werden)
# is_active=True Geräte werden ebenfalls revoked sie nutzen token_hash-Auth
# die nach dem Upgrade nicht mehr unterstützt wird
op.execute("UPDATE kiosk_devices SET status = 'revoked'")
# is_active und token_hash entfernen (werden durch status + public_key ersetzt)
op.drop_column("kiosk_devices", "is_active")
op.drop_column("kiosk_devices", "token_hash")
# last_seen_at → last_heartbeat_at (Umbenennung via Daten-Copy, dann drop)
op.execute("""
UPDATE kiosk_devices
SET last_heartbeat_at = last_seen_at
WHERE last_seen_at IS NOT NULL
""")
op.drop_column("kiosk_devices", "last_seen_at")
# ── companies: Kiosk-Konfiguration ────────────────────────────────────────
op.add_column("companies",
sa.Column("kiosk_require_approval", sa.Boolean(), nullable=False, server_default="true")
)
op.add_column("companies",
sa.Column("kiosk_track_current_user", sa.Boolean(), nullable=False, server_default="true")
)
op.add_column("companies",
sa.Column("kiosk_heartbeat_interval_sec", sa.Integer(), nullable=False, server_default="30")
)
def downgrade() -> None:
# companies
op.drop_column("companies", "kiosk_heartbeat_interval_sec")
op.drop_column("companies", "kiosk_track_current_user")
op.drop_column("companies", "kiosk_require_approval")
# kiosk_devices: Altspalten wiederherstellen
op.add_column("kiosk_devices",
sa.Column("last_seen_at", sa.DateTime(timezone=True), nullable=True)
)
op.execute("""
UPDATE kiosk_devices
SET last_seen_at = last_heartbeat_at
WHERE last_heartbeat_at IS NOT NULL
""")
op.add_column("kiosk_devices",
sa.Column("token_hash", sa.String(64), nullable=True)
)
op.add_column("kiosk_devices",
sa.Column("is_active", sa.Boolean(), nullable=False, server_default="false")
)
# Neue Spalten entfernen
op.drop_column("kiosk_devices", "ip_whitelist")
op.drop_column("kiosk_devices", "current_user_id")
op.drop_column("kiosk_devices", "offline_queue_size")
op.drop_column("kiosk_devices", "client_version")
op.drop_column("kiosk_devices", "last_heartbeat_at")
op.drop_column("kiosk_devices", "enrollment_expires_at")
op.drop_column("kiosk_devices", "enrollment_token_hash")
op.drop_column("kiosk_devices", "key_algorithm")
op.drop_column("kiosk_devices", "public_key")
op.drop_column("kiosk_devices", "status")
op.execute("DROP TYPE IF EXISTS kioskdevicestatus")