Launch-pflichtiger Compliance-Slice: öffentliche Anfrage zu einer PM speist eine
manuelle Admin-Queue (keine KI).
- Migration legal_requests + Model + Enums (Type: dsgvo/personal_rights/report,
Status: open/in_progress/resolved/rejected) + Factory.
- Öffentliches Formular /release/{slug}/rechtliches (LegalRequestController +
web/legal-request.blade.php): typ-abhängiger Hinweistext (Alpine), E-Mail bei
DSGVO/Persönlichkeitsrecht erforderlich, zwei versteckte Honeypot-Felder,
Rate-Limit + Bremse "1 offene Anfrage pro PM/Typ". Regeltexte als Entwurf mit
TODO für rechtliche Finalisierung markiert.
- Routen bewusst in eigener routes/legal.php (entkoppelt vom laufenden Web-Umbau),
host-agnostisch via domains.php eingebunden.
- Admin-Bereich "Recht & Compliance": Sidebar-Nav mit Offen-Zähler, Volt-Queue
index/show (in Bearbeitung/erledigt/abgelehnt/wieder öffnen + interne Notiz).
- Tests: je Typ, Honeypots (Dataset), Bremse, Admin-Queue + Status-Übergänge.
- Doku: Detailplan WS-3-Status + Deployment-Migrationsreihenfolge ergänzt.
Hinweis: Der "Melden"-/E&F-Button auf der PM-Detailseite (release-detail.blade.php)
wird mit dem separaten Web-Frontend-Commit verdrahtet; Ziel ist legal-request.create.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
13 KiB
Version: Juni 2026 · Datum: 16.06.2026 · Status: Umgesetzt Scope: Sicherheits- und deployment-kritische Punkte aus der Auth-/Rollen-/Verifizierungs-Phase (WS-6 & WS-2). Dieses Dokument bündelt, was beim Go-Live zwingend beachtet werden muss. Feed für die spätere Live-Deployment-Checkliste (WS-7).
1. ⚠️ SICHERHEIT — Legacy-Rollen-Überberechtigung (behoben)
Befund: Der Legacy-Import (app/Services/Import/UserImporter.php) mappte die Alt-Gruppe 2 auf die neue Rolle editor. Die Alt-Gruppe 2 war jedoch die normale Self-Publisher-Masse (65.950 von 66.151 Usern) – keine Redaktions-Mitarbeiter. Im neuen Rollenmodell gewährt editor Admin-Panel-Zugriff (User::canAccessAdmin() prüft hasRole(['admin','editor'])).
Auswirkung: Praktisch jeder Legacy-Kunde konnte sich – insbesondere über den Magic-Link-Login – ins komplette Admin-Panel einloggen.
Fix:
- Mapping korrigiert: Alt-Gruppe 2 →
customer(statteditor).editorwird ab jetzt ausschließlich manuell an echtes Personal vergeben. - Daten-Migration
2026_06_16_080913_downgrade_legacy_editor_users_to_customerstuft die bestehenden Legacy-editor(alle mitlegacy_portal) aufcustomerherab. Chunked (Placeholder-Limit), nicht umkehrbar. - Ergebnis nach Migration:
editor = 0,customer ≈ 65.952.
Deployment / Nachkontrolle:
- Migration MUSS auf Prod laufen.
- Es bleiben 4 Legacy-Admins (Alt-Gruppe 1) – manuell prüfen, ob das wirklich Personal ist.
- Kontrolle nach Deploy:
Spatie\Permission\Models\Role::findByName('editor')->users()->whereNotNull('legacy_portal')->count()muss 0 sein.
2. ⚠️ DEPLOYMENT-KRITISCH — E-Mail-Verifizierung scharf
User implementiert jetzt MustVerifyEmail; die verified-Middleware auf den Panel-Routen greift damit real (war vorher wirkungslos).
Lockout-Gefahr: Ohne Vorsorge würden ALLE Bestands-User mit email_verified_at = NULL (alle 66.151, davon ~59.403 aktiv) ausgesperrt.
Absicherung: Migration 2026_06_15_101337_backfill_email_verified_at_for_existing_users setzt email_verified_at für alle bestehenden User.
Deployment: Diese Migration MUSS mit dem Deploy laufen, bevor echter Traffic auf die geschützten Routen kommt. Neue Registrierungen (nach Deploy) bleiben absichtlich NULL, bis verifiziert.
Registrierungs-Verhalten (Entscheidung 15.06.):
- Selbst-Registrierung legt ein inaktives, rollenloses Konto an → Danke-/Notice-Seite → Verifizierungsmail.
- Bestätigter Link →
is_active = true+customer-Rolle (App\Listeners\ActivateUserAfterVerification). - Ausnahme-Kanäle (gelten als verifiziert, kein E-Mail-Schritt): Magic-Link, Kontaktzugang (WS-2), Google-Login (geplant).
3. VERHALTENSÄNDERUNG — Firmen-Scope für Pressemitteilungen
PM-Zugriff war hart an user_id (Autor) gebunden. Jetzt additiv: Zugriff = Autor ODER Mitglied der zugeordneten Firma (Owner via owner_user_id oder company_user-Pivot).
- Betroffen:
PressReleasePolicy::canManage()(view/update/submitForReview/delete) sowie die Queries der Kunden-Komponenten (Liste/Show/Edit) undUser::accessibleCompanyIds(). - Solo-Owner: keine sichtbare Änderung (sie sind Autor ihrer PMs).
- Firmen mit mehreren Mitgliedern: Mitglieder sehen/bearbeiten jetzt alle PMs der Firma (gewollt – „gesamte Firma").
4. Anmeldung per E-Mail-Link – zusammengeführt (WS-2)
Eine einzige öffentliche Seite /anmeldelink (auth.magic-link) deckt beide Magic-Link-Fälle ab; das frühere Login-Modal und die separate Pressekontakt-Seite sind aufgelöst (war nicht selbsterklärend, zwei fast identische Wege). Die alte URL /pressekontakt-zugang leitet per Redirect auf /anmeldelink.
- Enumeration-sicher (neutrale Antwort unabhängig vom Treffer), Honeypot + Rate-Limit (pro E-Mail+IP 3/h, pro IP 15/h).
- Einheitlicher Service-Eintritt
ContactAccessService::requestMagicAccess(). Berücksichtigt jede Stelle, an der eine E-Mail hinterlegt ist – an einer Firma (companies.email) und an einem Pressekontakt (contacts.email), portalübergreifend (Abfragen ohnePortalScope):- Bestehendes aktives Konto → Login-Magic-Link (auch ohne Hinterlegung – ersetzt das alte Login-Modal).
- Hinterlegte Firma/Kontakt(e) ohne Konto → lazy angelegter, per E-Mail de-duplizierter
customer-Account (aktiv, verifiziert). - Mehrfach hinterlegt → genau ein Konto je E-Mail, dem alle zugehörigen Firmen und Pressekontakte zugeordnet werden – auch über Portalgrenzen hinweg.
- Pivot-Rolle
responsible(„Verantwortlich"): Wer seine E-Mail an einer Firma/einem Pressekontakt hinterlegt hat, erhält Schreibzugriff auf die gesamte Firma (Stammdaten, Pressekontakte, Pressemitteilungen inkl. Terminierung/„Kalender") – sieheCompanyPolicy::update(). Owner (owner_user_id) bleiben unangetastet; ein bestehender reiner Lese-Pivot (member) wird beim nächsten Login/Magic-Access aufresponsiblehochgestuft. Greift über den Login-Re-Sync auf allen Anmeldewegen. - Beides zugleich → Konto wiederverwenden, Scope ergänzen, Link.
- Deaktiviertes Bestandskonto / unbekannte E-Mail → keine Aktion, gleiche neutrale Meldung.
- Versand über den bestehenden Login-Magic-Link (
MagicLinkGenerator+MagicLinkConsumeController) – keine Schema-Änderung.
Design-Nuance (bewusst): Der Pressekontakt-Account wird beim Absenden der Anfrage angelegt (nicht erst beim Klick), um eine fragile magic_links-Schema-Änderung (ENUM/FK über SQLite-Tests + MySQL-Prod) zu vermeiden. Er ist „lazy" (nur für real existierende Kontakt-Mails) und ohne den per Mail versendeten Token wertlos. Die ältere Methode requestAccess() (nur Pressekontakte) bleibt für gezielte Aufrufe/Tests erhalten.
Re-Sync bei jedem Login: App\Listeners\SyncCompanyMembershipsOnLogin hört auf das Login-Event und ruft ContactAccessService::syncMembershipsForEmail() – greift damit bei allen Anmeldewegen (Magic-Link, Passwort, Google). Nachträglich (z. B. per API) hinzugekommene Firmen/Kontakte mit derselben E-Mail werden so beim nächsten Login automatisch zugeordnet, unabhängig vom Login-Weg. Rein additiv (kein Konto, keine Mail), bestehende Mitgliedschaften/Owner bleiben unangetastet.
- Passwort-Login für Magic-Link-Accounts: Lazy angelegte Accounts haben
password = nullund können sich (korrekt) nicht per Passwort anmelden, bis sie über „Passwort vergessen" eins setzen. Danach ist auch der reguläre Passwort-Login möglich. - Performance-Hinweis: Der Re-Sync fragt
companies/contactsperLOWER(email)ab (case-insensitive, SQLite-Tests + MySQL-Prod). Bei sehr großen Tabellen pro Login besser über einen lowercased/funktionalen Index aufemailabsichern (Follow-up, noch nicht angelegt).
5. Guest-Redirect rollenbewusst (403-Sackgasse behoben)
bootstrap/app.php → redirectUsersTo: eingeloggte User auf Gast-Routen (/login, /register) werden rollen-/verifizierungsbewusst geleitet (Admin → /dashboard, Customer → /admin/me, unverifiziert → Notice). Verhindert, dass ein Customer auf das feste /dashboard (403) läuft und festsitzt.
Ergänzend: Magic-Link-Consume nutzt einen rollensicheren Redirect ohne intended() (eine als Gast besuchte Admin-URL darf einen Customer nicht in den 403-Bereich schicken).
6. Login-/Magic-Link-Härtung (Security-Review 16.06.)
Aus einer gezielten Auth-Prüfung umgesetzt:
- Magic-Link-Versand rate-limited (vorher ungedrosselt): pro E-Mail+IP (3/h) und zusätzlich pro IP (15/h) – verhindert Mail-Fluten aktiver Accounts und das laufende Entwerten alter Links. Der Pressekontakt-Zugang war bereits limitiert.
- Inaktive (aber verifizierte) User werden beim Passwort-Login zentral blockiert (
Auth::logout()+ Fehler), analog zum Magic-Link-Consume. Schließt nur-auth+verified-Routen ohne zusätzlichenis_active-Check ab. - Rollensicherer Login-Redirect: eine gemerkte
intended-Admin-URL (z. B./admin/users) schickt einen Customer nicht mehr in den 403 – inaktzessible Admin-Pfade fallen auf das rollengerechte Ziel zurück. - ContactAccess mutiert keine deaktivierten Bestands-Accounts mehr: der
is_active-Check läuft vor jeder Firmenzuordnung/jedem Linkversand. - Magic-Link-Verbrauch ist atomar (konditionales
UPDATE … whereNull(consumed_at)): Single-Use auch bei parallelen Requests garantiert.
Captcha (dokumentiert, vorerst NICHT eingebaut): Beim Pressekontakt-Zugang greifen aktuell Honeypot + Rate-Limit. Ein echtes Captcha (Cloudflare Turnstile / hCaptcha) ist sinnvoll, wird aber erst eingebaut, falls im Livebetrieb zu viel Missbrauch auftritt (Entscheidung 16.06.) – braucht Provider-Entscheidung, Keys und ein Paket.
6b. 2FA-Bypass behoben & Login-Pfade konsolidiert (Review 16.06., Teil 2)
Befund: Der sichtbare Volt-Login machte direkt Auth::attempt() und umging damit Fortifys 2FA-Pipeline (RedirectIfTwoFactorAuthenticatable) – für Accounts mit aktivem 2FA faktisch ein 2FA-Bypass. Zusätzlich existierte der Fortify-POST /login parallel mit schwächeren Post-Login-Regeln (kein is_active-Block, resolveTarget neutralisierte nur / und /dashboard).
Fix (Volt-nativ):
- Der Volt-Login prüft die Zugangsdaten jetzt OHNE sofortiges Einloggen; bei aktivem 2FA (
hasEnabledTwoFactorAuthentication()) wird der Session-Vertraglogin.id/login.remembergesetzt und auf eine neue Volt-2FA-Challenge-Seite (/two-factor-challenge) geleitet. Diese postet an Fortifys bestehendentwo-factor-challenge-Controller (TOTP + Recovery-Code). - Gemeinsame Post-Login-Logik in
App\Support\LoginRedirect(rollengerechtes Home + 403-sicherer intended-Redirect), genutzt von Volt-Login UND der Fortify-Response. RoleAwareLoginResponseimplementiert jetzt beide Contracts (LoginResponseundTwoFactorLoginResponse) und erzwingt einheitlich: unverifiziert → Notice, verifiziert-inaktiv → Logout+Fehler, sonst 403-sicherer Redirect. Damit ist auch der direkte Fortify-POST-Pfad gehärtet.
Hinweis: fortify.views => false bleibt; die Challenge wird vom Volt-Frontend bereitgestellt, die Verifizierung übernimmt Fortify.
Nachschärfung (Review 16.06., Teil 3): Der wantsJson()-Kurzschluss in RoleAwareLoginResponse lief zuvor VOR den Sicherheitschecks – ein XHR/JSON-Login eines verifiziert-inaktiven Accounts erhielt so eine Session ohne Logout. Jetzt laufen die Prüfungen zuerst: verifiziert-inaktiv → Auth::logout() + Session-Invalidate + 403 (JSON) bzw. Login mit Fehler (HTML); unverifiziert → 204 (JSON) bzw. Notice (HTML). Erst danach der 204-/Redirect-Erfolgsfall.
6c. Google-Login (Socialite, WS-6)
/auth/google/redirect → Google, /auth/google/callback → GoogleController. Auflösung in App\Services\Auth\SocialAuthService:
- De-Dup über E-Mail: bestehende E-Mail wird wiederverwendet (keine Dubletten), nur
oauth_provider/oauth_provider_idwerden verknüpft. - Neuer User: aktiv + verifiziert +
customer(Verifizierung gilt über den Google-Kanal als erfüllt). - Offener Selbst-Registrierer (unverifiziert): wird über Google onboardet (verifiziert + aktiv + customer).
- Deaktivierter, verifizierter Account: wird nicht reaktiviert → Login blockiert.
- Abschluss über dieselbe
LoginRedirect-Logik (rollengerecht, 403-sicher).
Deployment-Voraussetzungen:
composer install(neue Abhängigkeitlaravel/socialite).- Migration
add_oauth_provider_columns_to_users(siehe unten). - ENV:
GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,GOOGLE_REDIRECT_URI. Die Redirect-URI muss in der Google Cloud Console exakt der Callback-URL der Portal-Domain entsprechen – Prod:https://pressekonto.com/auth/google/callback(der Login läuft auf dem Portal-Host, nicht zwingendAPP_URL). - Lokales Testen: Google akzeptiert keine
.test-Domains als Redirect-URI (nurlocalhost/127.0.0.1oder echte https-Domains). Der echte Google-Round-Trip wird daher auf Staging/Live mit der echten https-Domain getestet; lokal ist die Login-Logik übertests/Feature/Auth/GoogleLoginTest.php(gemockter Socialite-Provider) abgesichert. Wer lokal end-to-end testen will: gesamten Flow aufhttp://localhostlegen (Portal-Host temporär auf localhost) oder einen https-Tunnel als Portal-Host verwenden – sonst bricht die host-gebundene Session beim Wechsel.test→localhost.
7. Deployment-Reihenfolge (Migrationen dieser Phase)
2026_06_15_101337_backfill_email_verified_at_for_existing_users— verhindert Lockout.2026_06_16_080913_downgrade_legacy_editor_users_to_customer— schließt die Admin-Überberechtigung.2026_06_16_103238_add_oauth_provider_columns_to_users— Provider-Verknüpfung für Google-Login.2026_06_16_134458_create_legal_requests_table— Queue für Recht & Compliance (WS-3: DSGVO/Persönlichkeitsrecht/Meldungen).
Nach Deploy prüfen:
editor-Rolle hat keine Legacy-User mehr.- Niemand ungewollt ausgesperrt (Stichprobe aktiver Legacy-User → Login/Panel erreichbar).
- Die 4 verbliebenen Legacy-Admins sind legitimes Personal.