feat: Reseller-Rolle + SUPER_ADMIN-Mandantenübersicht
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>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user