auth: automatische Firmenzuordnung beim Wegfall der E-Mail-Grundlage entfernen statt senken (Block 7.13, U01, E7)

- Login-Sync löscht auto-Pivots ohne aktuellen Treffer; member wäre die
  Teamrolle mit Schreibrecht auf alle Firmen-PMs, keine Leserolle
- hochgestufte manuelle Mitgliedschaften tragen granted_via=auto-upgrade
  und fallen auf member zurück; manual und owner bleiben unberührt
- company-memberships:audit --classify stuft nur manual-Zeilen um
- Plan: E2 revidiert, E7 entschieden, 1.5 und 7.13 nachgezogen

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-18 13:00:48 +00:00
parent c26bdff681
commit 5122624687
4 changed files with 92 additions and 26 deletions

View file

@ -83,7 +83,7 @@ class AuditCompanyMemberships extends Command
$before = (string) $this->option('before');
$candidates = $this->responsibleRows()
->filter(fn (object $row): bool => isset($explained[$row->company_id.':'.$row->user_id]))
->where('granted_via', '!=', 'auto')
->where('granted_via', 'manual')
->filter(fn (object $row): bool => $before === '' || (string) $row->created_at < $before)
->values();
@ -112,7 +112,7 @@ class AuditCompanyMemberships extends Command
->where('company_id', $row->company_id)
->where('user_id', $row->user_id)
->where('role', 'responsible')
->where('granted_via', '!=', 'auto')
->where('granted_via', 'manual')
->update(['granted_via' => 'auto']);
}
}

View file

@ -109,10 +109,14 @@ class ContactAccessService
* hat, ist unverifiziert und erhält nichts – der Login-Event feuert auch
* vor der Verifizierungs-Weiche. Deaktivierte Konten ebenfalls nicht.
*
* Gegenrichtung (Audit F10, E2): automatisch vergebene `responsible`-Rechte,
* Gegenrichtung (Audit F10/U01, E7): automatisch vergebene Zuordnungen,
* deren E-Mail-Grundlage weggefallen ist (Adresse an Firma oder Kontakt
* getauscht, Kontakt gelöscht), fallen auf `member` zurück. Manuell oder
* aus dem Altbestand vergebene Rechte (`granted_via = manual`) bleiben.
* getauscht, Kontakt gelöscht, eigene Adresse geändert), werden entfernt –
* `member` wäre keine Leserolle, sondern die Teamrolle mit Schreibrecht
* auf alle Firmen-PMs. Eigene PMs bleiben über die Autorenschaft. Eine
* automatisch HOCHGESTUFTE manuelle Mitgliedschaft (`auto-upgrade`) fällt
* auf `member` zurück. Manuell oder aus dem Altbestand vergebene Rechte
* (`granted_via = manual`) bleiben.
*/
public function syncMembershipsForEmail(User $user): void
{
@ -140,19 +144,23 @@ class ContactAccessService
}
/**
* Senkt automatisch vergebene `responsible`-Pivots ohne aktuellen
* E-Mail-Treffer auf `member`. Lesezugriff bleibt, Schreibzugriff geht.
* Entfernt automatisch vergebene `responsible`-Pivots ohne aktuellen
* E-Mail-Treffer (E7). Eine hochgestufte manuelle Mitgliedschaft geht auf
* ihren vorherigen Stand `member` zurück und gilt wieder als manuell.
*
* @param Collection<int, int> $currentCompanyIds
*/
private function revokeStaleAutomaticGrants(User $user, Collection $currentCompanyIds): void
{
DB::table('company_user')
$stale = DB::table('company_user')
->where('user_id', $user->id)
->where('role', 'responsible')
->where('granted_via', 'auto')
->whereNotIn('company_id', $currentCompanyIds->all())
->update(['role' => 'member', 'updated_at' => now()]);
->whereNotIn('company_id', $currentCompanyIds->all());
(clone $stale)->where('granted_via', 'auto')->delete();
(clone $stale)->where('granted_via', 'auto-upgrade')
->update(['role' => 'member', 'granted_via' => 'manual', 'updated_at' => now()]);
}
/**
@ -282,9 +290,11 @@ class ContactAccessService
if ($linked === null) {
$user->companies()->attach($companyId, ['role' => 'responsible', 'granted_via' => 'auto']);
} elseif ($linked->pivot->role === 'member') {
// Nur den Read-only-Default hochstufen; owner/responsible bleiben.
// Die Hochstufung ist automatisch und damit wieder rücknehmbar.
$user->companies()->updateExistingPivot($companyId, ['role' => 'responsible', 'granted_via' => 'auto']);
// Nur den Team-Default hochstufen; owner/responsible bleiben.
// `auto-upgrade` merkt sich, dass darunter eine manuelle
// Mitgliedschaft liegt – die Rücknahme führt dorthin zurück
// statt die Zeile zu löschen.
$user->companies()->updateExistingPivot($companyId, ['role' => 'responsible', 'granted_via' => 'auto-upgrade']);
}
}