Files
timemaster/backend/migrations/versions/0034_reseller.py
T
patrickandClaude Opus 4.8 898a81f499
Security Audit / Python Dependency Audit (push) Has been cancelled
Security Audit / Node.js Dependency Audit (push) Has been cancelled
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>
2026-06-23 01:15:51 +02:00

71 lines
3.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.
"""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)