Mandantenfähigkeit ausgebaut: - Neue Rolle RESELLER (company_id NULL); companies.reseller_id + is_active - RLS-Erweiterung (Migration 0034): companies/users zusätzlich auf app.reseller_id gefenced → Reseller sieht/verwaltet DB-seitig nur eigene Firmen, keine personenbezogenen Zeit-/Abwesenheitsdaten (DSGVO: nur Verwaltung) - get_current_user setzt app.reseller_id + Bypass aus für RESELLER - tenant_service: Firma + Erst-Admin (Einladung), Übersicht mit Kennzahlen - Router /reseller/* (Self-Service) und /admin/* (SUPER_ADMIN: Mandanten + Reseller) - Login-Sperre bei deaktiviertem Mandanten - Frontend: TenantsPage (/admin/tenants), eigene ResellerCompaniesPage (/reseller), rollenbasierte Login-Weiterleitung, Nav "Mandanten" für SUPER_ADMIN - 4 neue Tests inkl. Cross-Reseller-RLS-Isolation; 172/172 grün Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
71 lines
3.2 KiB
Python
71 lines
3.2 KiB
Python
"""Reseller role + tenant ownership + company is_active + RLS extension
|
||
|
||
Revision ID: 0034
|
||
Revises: 0033
|
||
Create Date: 2026-06-23
|
||
|
||
- Neuer Enum-Wert UserRole.RESELLER
|
||
- companies.is_active (Mandanten-Login-Sperre)
|
||
- companies.reseller_id (Besitz durch Reseller; NULL = Plattform/SUPER_ADMIN)
|
||
- RLS-Policies für companies + users so erweitert, dass ein Reseller (app.reseller_id)
|
||
genau seine eigenen Firmen und deren User sieht/verwaltet (keine personenbezogenen
|
||
Zeit-/Abwesenheitsdaten – diese Tabellen bleiben unverändert auf company_id gefenced).
|
||
"""
|
||
from alembic import op
|
||
from sqlalchemy import text
|
||
|
||
revision = "0034"
|
||
down_revision = "0033"
|
||
branch_labels = None
|
||
depends_on = None
|
||
|
||
# ── RLS-Ausdrücke (analog 0024) ──────────────────────────────────────────────
|
||
_BYPASS = "COALESCE(current_setting('app.bypass_rls', true), 'off') = 'on'"
|
||
_CID = "company_id = NULLIF(current_setting('app.company_id', true), '')::uuid"
|
||
_IID = "id = NULLIF(current_setting('app.company_id', true), '')::uuid"
|
||
_RID = "reseller_id = NULLIF(current_setting('app.reseller_id', true), '')::uuid"
|
||
# Firmen, die dem aktuellen Reseller gehören (Subquery unterliegt selbst der
|
||
# companies-Policy, liefert dem Reseller also genau seine eigenen Firmen).
|
||
_USER_RESELLER = (
|
||
"company_id IN (SELECT id FROM companies WHERE "
|
||
"reseller_id = NULLIF(current_setting('app.reseller_id', true), '')::uuid)"
|
||
)
|
||
|
||
|
||
def _exec(sql: str) -> None:
|
||
op.execute(text(sql))
|
||
|
||
|
||
def _recreate_policies(table: str, using: str) -> None:
|
||
for op_name in ("select", "insert", "update", "delete"):
|
||
_exec(f"DROP POLICY IF EXISTS rls_{table}_{op_name} ON {table}")
|
||
_exec(f"CREATE POLICY rls_{table}_select ON {table} FOR SELECT USING {using}")
|
||
_exec(f"CREATE POLICY rls_{table}_insert ON {table} FOR INSERT WITH CHECK {using}")
|
||
_exec(f"CREATE POLICY rls_{table}_update ON {table} FOR UPDATE USING {using} WITH CHECK {using}")
|
||
_exec(f"CREATE POLICY rls_{table}_delete ON {table} FOR DELETE USING {using}")
|
||
|
||
|
||
def upgrade() -> None:
|
||
# 1) Enum-Wert RESELLER (ADD VALUE darf in TX laufen, wird hier nicht verwendet)
|
||
_exec("ALTER TYPE userrole ADD VALUE IF NOT EXISTS 'RESELLER'")
|
||
|
||
# 2) Neue Spalten
|
||
_exec("ALTER TABLE companies ADD COLUMN IF NOT EXISTS is_active BOOLEAN NOT NULL DEFAULT TRUE")
|
||
_exec("ALTER TABLE companies ADD COLUMN IF NOT EXISTS reseller_id UUID NULL "
|
||
"REFERENCES users(id) ON DELETE SET NULL")
|
||
_exec("CREATE INDEX IF NOT EXISTS ix_companies_reseller_id ON companies (reseller_id)")
|
||
|
||
# 3) RLS erweitern
|
||
_recreate_policies("companies", f"({_BYPASS} OR {_IID} OR {_RID})")
|
||
_recreate_policies("users", f"({_BYPASS} OR {_CID} OR {_USER_RESELLER})")
|
||
|
||
|
||
def downgrade() -> None:
|
||
# RLS auf 0024-Stand zurücksetzen
|
||
_recreate_policies("companies", f"({_BYPASS} OR {_IID})")
|
||
_recreate_policies("users", f"({_BYPASS} OR {_CID})")
|
||
_exec("DROP INDEX IF EXISTS ix_companies_reseller_id")
|
||
_exec("ALTER TABLE companies DROP COLUMN IF EXISTS reseller_id")
|
||
_exec("ALTER TABLE companies DROP COLUMN IF EXISTS is_active")
|
||
# Enum-Wert RESELLER bleibt (PostgreSQL kann Enum-Werte nicht entfernen)
|