Mitarbeiterverwaltung in Laravel: Rollen und Rechte ohne Chaos

Mitarbeiterverwaltung in Laravel: Rollen und Rechte ohne Chaos

Individuelle Softwareentwicklung
· 9 Min. Lesezeit · Shadi Aburok

Eine Mitarbeiterverwaltung sieht aus der Geschäftsführer-Perspektive simpel aus: „Der eine sieht alles, der andere sein Team, der dritte nur seine eigenen Daten." Wer das in einer Laravel-Anwendung sauber umsetzen will, merkt schnell: zwischen „simpel" und „Spaghetti-Code aus 47 if-Abfragen" liegt nur eine schlechte Architektur-Entscheidung am Anfang.

Dieser Post beschreibt, wie wir bei Aburok Rollen und Rechte in Laravel-basierten HR- und Mitarbeiterverwaltungs-Apps strukturieren – pragmatisch, langfristig wartbar und mit Blick auf den Datenschutz, der bei Mitarbeiterdaten besonders sensibel ist.

Das eigentliche Problem ist nicht technisch, sondern fachlich

Bevor eine Zeile Code entsteht, klären wir mit dem Auftraggeber drei Fragen:

  1. Welche Rollen gibt es organisatorisch? Geschäftsführer, Personalabteilung, Teamleiter, Mitarbeiter, externe Buchhaltung? Jede Rolle ist ein Mensch in einer Funktion.
  2. Welche Aktionen müssen geschützt werden? Mitarbeiterstammdaten ansehen, ändern, Lohndaten einsehen, Urlaubsanträge genehmigen, Verträge hochladen, jemanden kündigen, Mitarbeiter-Listen exportieren.
  3. Wer darf was bei wem? Ein Teamleiter sieht seine Teammitglieder – nicht die ganze Firma. Eine externe Buchhaltung sieht Lohndaten, aber keine Krankmeldungen. Das ist nicht nur Rolle, das ist Rolle im Kontext.

Wer diese drei Fragen vor dem Code sauber beantwortet, hat den größten Teil der Implementierungsarbeit schon hinter sich. Wer sie nicht beantwortet, baut sich ein Berechtigungssystem, das nach drei Monaten niemand mehr versteht.

Rollen sind nicht Rechte – und das ist wichtig

Die häufigste Fehleinschätzung in Laravel-Projekten: Rollen und Rechte werden vermischt. Im Code steht dann sowas:

if ($user->role === 'admin' || $user->role === 'manager') {
    // darf Lohndaten sehen
}

Das funktioniert – bis ein Wirtschaftsprüfer kurz Zugriff bekommen soll, ohne Admin zu werden. Oder bis die Personalabteilung Lohndaten sehen, aber keine Stammdaten ändern darf. Plötzlich passt keine Rolle, und das if wird länger.

Die saubere Trennung:

  • Rolle = organisatorische Zugehörigkeit (Geschäftsführer, Teamleiter, Personalabteilung)
  • Recht (Permission) = konkrete erlaubte Aktion (mitarbeiter.lohndaten.ansehen, urlaub.genehmigen, vertrag.hochladen)

Eine Rolle bündelt eine Menge an Rechten. Ein Wirtschaftsprüfer ist keine eigene Rolle, sondern bekommt ein paar einzelne Rechte für drei Tage. Ein Teamleiter ist eine Rolle mit klar definiertem Permission-Set.

Warum wir Spatie Laravel Permission nehmen

Es gibt verschiedene Lösungen für RBAC (Role-Based Access Control) in Laravel. Wir nutzen in fast allen Mittelstands-Projekten spatie/laravel-permission. Drei Gründe:

  1. Aktiv gepflegt, seit Jahren stabil. Spatie ist eine belgische Agentur, die seit 2014 Laravel-Pakete veröffentlicht. „In 5 Jahren noch wartbar" ist hier gegeben – wichtig, wenn die App lange laufen soll.
  2. Pragmatische API. $user->assignRole('teamleiter'), $user->givePermissionTo('urlaub.genehmigen'), $user->can('urlaub.genehmigen'). Lesbar im Code, intuitiv für jeden Laravel-Entwickler, der nach uns kommt.
  3. Trennung von Rollen und Permissions. Genau das, was im vorigen Abschnitt beschrieben ist – das Paket erzwingt diese Trennung architektonisch, statt sie zu vermischen.

Selbst eine eigene Lösung zu bauen ist verlockend, weil RBAC „eigentlich nicht so kompliziert" ist. Aber Cache-Strategien, Multi-Guard-Support, Wildcard-Permissions, Datenbank-Indizes – das alles steckt schon drin und ist getestet. Wir bauen lieber das Fachliche oben drauf.

Konkretes Beispiel: HR-App für einen Mittelständler mit 80 Mitarbeitern

Stell Dir vor, ein Kunde aus dem Maschinenbau will eine interne App, mit der Mitarbeiterstammdaten, Urlaubsanträge und Krankmeldungen verwaltet werden. Vier Rollen, klar definiert:

Rolle Was darf sie?
Geschäftsführung Alles – alle Mitarbeiter, alle Daten, alle Aktionen
Personalabteilung Alle Stammdaten ändern, Verträge hochladen, Lohndaten sehen, aber keine Urlaubsanträge genehmigen
Teamleiter Nur eigene Teammitglieder sehen; Urlaubsanträge des Teams genehmigen; keine Lohndaten
Mitarbeiter Nur eigene Daten ansehen; eigene Urlaubsanträge stellen; eigene Krankmeldungen einreichen

Das übersetzt sich in folgende Permissions (Convention: bereich.aktion):

mitarbeiter.stammdaten.ansehen
mitarbeiter.stammdatenndern
mitarbeiter.lohndaten.ansehen
mitarbeiter.vertrag.hochladen
urlaub.eigenen.einreichen
urlaub.team.genehmigen
krankmeldung.eigene.einreichen
krankmeldung.team.ansehen
mitarbeiter.exportieren

Diese Permissions werden den Rollen zugewiesen. Die Rollen werden den Usern zugewiesen. Punkt. Im Code steht später nie wieder if ($user->role === 'admin'), sondern immer if ($user->can('urlaub.team.genehmigen')).

Implementierung – die wichtigsten Bausteine

1. Paket installieren

composer require spatie/laravel-permission
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
php artisan migrate

Die Migration legt fünf Tabellen an: roles, permissions, model_has_roles, model_has_permissions, role_has_permissions. Sauber normalisiert, gut indiziert.

2. Im User-Model das Trait einbinden

use Spatie\Permission\Traits\HasRoles;

class User extends Authenticatable
{
    use HasRoles;
}

Mehr ist hier nicht zu tun. Damit hat jedes User-Objekt Methoden wie assignRole(), givePermissionTo(), hasRole(), can().

3. Rollen und Permissions in einem Seeder anlegen

Wir machen das immer in einem Seeder, nie händisch in der Datenbank. Grund: Der Stand ist im Code dokumentiert, in jedem Environment reproduzierbar und Versionskontrolle-fähig.

use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;

class RolesAndPermissionsSeeder extends Seeder
{
    public function run(): void
    {
        // Permissions
        $permissions = [
            'mitarbeiter.stammdaten.ansehen',
            'mitarbeiter.stammdaten.ändern',
            'mitarbeiter.lohndaten.ansehen',
            'mitarbeiter.vertrag.hochladen',
            'mitarbeiter.exportieren',
            'urlaub.eigenen.einreichen',
            'urlaub.team.genehmigen',
            'krankmeldung.eigene.einreichen',
            'krankmeldung.team.ansehen',
        ];

        foreach ($permissions as $p) {
            Permission::firstOrCreate(['name' => $p]);
        }

        // Rollen
        $gf = Role::firstOrCreate(['name' => 'geschäftsführung']);
        $gf->syncPermissions(Permission::all());

        $hr = Role::firstOrCreate(['name' => 'personalabteilung']);
        $hr->syncPermissions([
            'mitarbeiter.stammdaten.ansehen',
            'mitarbeiter.stammdaten.ändern',
            'mitarbeiter.lohndaten.ansehen',
            'mitarbeiter.vertrag.hochladen',
            'mitarbeiter.exportieren',
        ]);

        $teamleiter = Role::firstOrCreate(['name' => 'teamleiter']);
        $teamleiter->syncPermissions([
            'mitarbeiter.stammdaten.ansehen',
            'urlaub.team.genehmigen',
            'krankmeldung.team.ansehen',
        ]);

        $mitarbeiter = Role::firstOrCreate(['name' => 'mitarbeiter']);
        $mitarbeiter->syncPermissions([
            'urlaub.eigenen.einreichen',
            'krankmeldung.eigene.einreichen',
        ]);
    }
}

firstOrCreate ist wichtig, damit der Seeder beim Re-Deployment auf Produktion nichts doppelt anlegt.

4. Routen mit Middleware schützen

Route::middleware(['auth', 'permission:urlaub.team.genehmigen'])->group(function () {
    Route::get('/urlaub/genehmigen', [UrlaubController::class, 'list']);
    Route::post('/urlaub/{antrag}/genehmigen', [UrlaubController::class, 'approve']);
});

Wer das Permission nicht hat, sieht einen 403. Klar und einfach.

5. Im Blade die richtigen Buttons anzeigen

@can('mitarbeiter.lohndaten.ansehen')
    <a href="{{ route('lohn.show', $mitarbeiter) }}" class="btn">Lohndaten</a>
@endcan

Das ist der Punkt, an dem Spatie Permission richtig glänzt: Die UI passt sich automatisch an die Rolle des angemeldeten Users an, ohne dass man Schalter pflegen muss.

Die wirklich harte Stelle: „Eigenes Team, eigene Daten"

Was bisher gezeigt wurde, ist klassisches RBAC: Permission ja oder nein. Aber „ein Teamleiter sieht nur sein Team" ist eine kontextabhängige Logik, die nicht in eine reine Permission passt. Sie braucht eine zusätzliche Schicht.

Hier kommen Laravel Policies ins Spiel:

class MitarbeiterPolicy
{
    public function view(User $user, Mitarbeiter $mitarbeiter): bool
    {
        // Geschäftsführung + HR: immer
        if ($user->can('mitarbeiter.stammdaten.ansehen') && $user->hasAnyRole(['geschäftsführung', 'personalabteilung'])) {
            return true;
        }

        // Teamleiter: nur eigenes Team
        if ($user->hasRole('teamleiter')) {
            return $mitarbeiter->team_id === $user->team_id;
        }

        // Mitarbeiter: nur sich selbst
        return $user->id === $mitarbeiter->user_id;
    }
}

Im Controller:

public function show(Mitarbeiter $mitarbeiter)
{
    $this->authorize('view', $mitarbeiter);
    return view('mitarbeiter.show', compact('mitarbeiter'));
}

So bleibt die Permission-Schicht sauber „darf der Nutzer das überhaupt tun?", und die Policy klärt „darf er es bei diesem Datensatz?". Diese Trennung ist die wichtigste Architektur-Entscheidung – wer sie nicht macht, hat in einem Jahr 80 verschachtelte if-Abfragen in jedem Controller.

DSGVO ist keine Fußnote

Mitarbeiterdaten gehören zu den besonders schutzbedürftigen personenbezogenen Daten im Sinne der DSGVO. Drei Dinge, die wir in jeder Mitarbeiterverwaltung von Anfang an einbauen:

  1. Audit-Log: Wer hat wann welche Mitarbeiterdaten angesehen, geändert, exportiert? Wir nutzen meist spatie/laravel-activitylog, und legen für die kritischen Modelle (Mitarbeiter, Lohn, Vertrag) einen Logger an. Bei einer DSGVO-Prüfung musst Du nachweisen können, wer auf welche Daten zugegriffen hat.
  2. Datensparsamkeit: Eine API für Mitarbeiter-Listen liefert nicht alle Felder, sondern nur die, die der Aufrufer mit seinen Rechten sehen darf. API Resources mit conditional fields lösen das sauber.
  3. Löschkonzept: Mitarbeiter scheiden aus, ihre Daten dürfen nicht ewig in der Datenbank liegen. Nach gesetzlicher Aufbewahrungsfrist (10 Jahre für Lohnbuchhaltung) wird anonymisiert oder gelöscht. Das ist eine Geschäftsregel, kein technisches Detail – muss aber im Code abgebildet sein.

Diese drei Punkte werden in der Anforderungsphase oft vergessen. Sie nachträglich einzubauen ist deutlich aufwendiger als sie von Anfang an mitzudenken.

Stolperfallen, die wir mehrfach erlebt haben

Performance bei vielen Permissions. Spatie Permission cacht die Berechtigungen pro Request – Eager-Loading ist also okay. Aber: Wenn Du in einer Liste mit 200 Mitarbeitern für jeden einzelnen ein @can aufrufst, das eine Policy mit DB-Query triggert, hast Du 200 zusätzliche Queries. Hier lohnt sich, vorher die Rolle des Users zu prüfen und die Liste serverseitig zu filtern, statt jeden einzelnen Eintrag im Template zu „verstecken".

Cache nach Rollen-Änderung leeren. Wenn ein User eine neue Rolle bekommt, kann der gecachte Berechtigungsstand alt sein. app()->make(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions() nach jeder Änderung an Rollen oder Berechtigungen aufrufen.

Permissions in Deutsch oder Englisch? Wir nutzen deutsche Permission-Namen (urlaub.team.genehmigen), weil sie für den fachlichen Auftraggeber lesbar sind. Aber: Konsequent durchziehen, nicht mischen. Wer mit englischen Teams arbeitet, nimmt Englisch. Beides geht, Mischmasch frustriert.

Wildcard-Permissions sparsam einsetzen. Spatie unterstützt urlaub.* als Permission – das ist verlockend, weil es kompakter ist. Aber: Bei einem Audit kann niemand mehr klar sagen, was das Recht „alles unter urlaub" konkret umfasst. Wir bleiben bei expliziten Permissions, auch wenn es mehr Tipparbeit ist.

Wann Spatie Permission an Grenzen stößt

Spatie Permission ist klassisches RBAC – Rolle hat Permission, fertig. Wenn die Anforderungen komplexer werden, reicht das nicht mehr:

  • Zeitbasierte Rechte („Wirtschaftsprüfer darf 3 Tage lang Lohndaten sehen"): geht mit einer eigenen Tabelle und einer Policy, die das Datum prüft.
  • Vier-Augen-Prinzip („Kündigung muss von Geschäftsführer und HR genehmigt werden"): braucht einen Workflow-State im Datensatz, nicht nur eine Permission.
  • Attributbasierte Zugriffe („Nur Lohndaten von Mitarbeitern desselben Standorts"): geht mit Policies, wird aber schnell komplex. Für sehr große Strukturen lohnt sich dann ein dediziertes ABAC-System (z. B. casbin/php-casbin) oder ein eigener Policy-Engine-Layer.

In 95 % der Mittelstands-Mitarbeiterverwaltungen, die wir gesehen haben, reicht aber Spatie Permission plus Laravel Policies vollkommen aus. Wer es trotzdem überdreht baut, baut Komplexität, die niemand mehr versteht.

Wer das einmal sauber aufsetzt, hat lange Ruhe

Eine durchdachte Rollen-Rechte-Architektur ist eine der Stellen, an denen Senior-Engineering-Erfahrung den Unterschied macht. Sie kostet am Anfang ein, zwei Tage Konzeptarbeit – und spart später Wochen, in denen sonst jemand verzweifelt versucht, ein gewachsenes if-Geflecht zu entwirren.

Falls Du eine Mitarbeiterverwaltung in Laravel planst oder eine bestehende App in Richtung saubereres Berechtigungsmodell bringen willst, helfen wir gerne weiter. Ein 30-minütiges Erstgespräch ist kostenlos – wir hören zu, schauen uns die bestehende Struktur an und sagen ehrlich, ob ein Refactor sinnvoll ist oder ein Neubau.

Erstgespräch unverbindlich vereinbaren – wir melden uns innerhalb eines Arbeitstages.

Weitere Artikel