Laravel / Lumen Authorisierung mittels Policies: zusätzliche Parameter an die Policy-Methode übergeben

Changelog:

  • 05.10.2019: Hinweis zu den aktualisierten Docs hinzugefügt.

Hinweis: Seit Version 6.0 ist der in diesem Artikel beschriebene Fall in der offiziellen Laravel-Dokumentation dokumentiert.

Die hier betrachtete Frage lautet: Wie übergebe ich weitere Informationen an die referenzierte Methode einer Policy, wenn ich aus einem Controller heraus …

$this->authorize('nameOfThePolicyMethod', ...);

… aufrufe?

Auf einen Blick:

AuthServiceProvider (Lumen):

<?php
namespace App\Providers;
use App\Models\SomeModel;
use App\Policies\SomeModelPolicy;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
    public function boot()
    {
        Gate::policy(SomeModel::class, SomeModelPolicy::class);
    }
}

Policy:

<?php
namespace App\Policies;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class SomeModelPolicy
{
    use HandlesAuthorization;
    public function create(User $user, $someObject, string $test)
    {
        // Use $someVariable to check $user authorization.
        // $someObject is not of type SomeModel.
        // $test is some random string or whatever.
        return $someObject->allowsUser($user);
    }
}

Controller:

<?php
namespace App\Http\Controllers;
use App\SomeClass;
use App\Models\SomeModel;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class SomeController extends Controller
{
    public function store(Request $request, SomeClass $someObject)
    {
        $this->authorize('create', [
            SomeModel::class,
            $someObject,
            'test'
        ]);
        return SomeModel::create($request->all());
    }
}

Hintergrund

Bei der Arbeit an einem Lumen-Projekt bin ich heute über ein kleines Steinchen gestolpert. Laravels Gates und Policies bilden einen wirklich nützlichen Weg, den Zugriff auf bestimmte Ressourcen, oder deren Modifikation, zu beschränken. Die offizielle Doku bietet für diesen Einsatz einen guten Einstieg.

Heute ist mir allerdings ein Fall begegnet, der in den Docs nicht festgehalten ist und den ich jetzt für mich selbst und die Nachwelt hier kurz schildern will. Die Ausgangslage ist mit folgendem ausgedachten Szenario vergleichbar:

  • Es gibt Benutzer (User)
  • Benutzer können Teil eines Projektes sein (Project)
  • Ein Projekt kann Themen beinhalten (Topic)
  • Jeder Benutzer eines Projektes kann für dieses Projekt ein neues Thema erstellen, bearbeiten und löschen

Mir ist klar, dass man in diesem Szenario völlig anders vorgehen würde, als der folgende Code nahelegt. Zum Beispiel über eine Middleware auf der gesamte Router-Gruppe der Projekte oder dem Controller. Aber ob sinnvoll oder nicht, es geht mir hier um folgende Frage:

Wie kann ich im Controller den aktuellen User mithilfe einer Policy authorisieren, wenn ich dabei nicht (nur) das referenzierte Model, sondern zusätzliche Informationen dafür brauche? Konkretes Beispiel: Ob ein Benutzer ein Thema erstellen darf, hängt davon ab, ob er Teil des Projektes ist. Das soll die Policy entscheiden.

Noch einmal, die Sinnhaftigkeit dieses Ansatzes ist streitbar. Er dient nur der Veranschaulichung. Nun zum Code.

Als erstes registrieren wir die Beziehung von Policy und Model im AuthServiceProvider (hier in Lumen):

<?php
namespace App\Providers;
use App\Models\Topic;
use App\Policies\TopicPolicy;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
    public function boot()
    {
        Gate::policy(Topic::class, TopicPolicy::class);
    }
}

Jetzt schauen wir uns die sehr einfache Policy an:

<?php
namespace App\Policies;
use App\Models\User;
use App\Models\Project;
use Illuminate\Auth\Access\HandlesAuthorization;
class TopicPolicy
{
    use HandlesAuthorization;
    public function create(User $user, Project $project)
    {
        return $project->hasUser($user);
    }
}

Bei einem Erstellen-Vorgang haben wir noch kein erstelltes Thema, dass wir irgendwie in die Authorisierung mit einfließen lassen können. Wir wollen den Zugang also stattdessen anhand anderen Daten gewähren oder ablehnen, in diesem Fall: gehört der Benutzer zum Projekt?

Die Methode hasUser() habe ich natürlich in meiner Project-Klasse selbst definiert. Die Logik an dieser Stelle ist uns hier herzlich egal. Wir müssen nur wissen, dass die Methode einen Boolean-Wert zurückgeben wird.

Jetzt könnte man naiverweise denken (so auch ich zu Beginn), dass man in den Controller einfach folgende Anweisung geben sollte:

<?php
namespace App\Http\Controllers;
use App\Models\Topic;
use App\Models\Project;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class TopicController extends Controller
{
    public function store(Request $request, Project $project)
    {
        $this->authorize('create', $project);
        return Topic::create($request->all());
    }
}

Doch leider laufen wir hier ins Leere, denn die list()-Methode wird in der Policy nicht einmal aufgerufen. Ein Fehler zeigt uns, dass wir bei dem Aufruf nicht authorisiert sind (und es auch nie sein werden). Warum ist das so?

Der vom Basis-Controller genutzte Trait ProvidesConvenienceMethods nimmt anhand des zweiten Parameters vom Typ Project an, dass wir uns für das Model „Project“ und eine damit verknüpften ProjectPolicy interessieren. Aber diese Verknüpfung gibt es ja gar nicht, wir haben nicht einmal eine ProjectPolicy. Abgesehen davon wollen wir auch keine list()-Methode von einer ProjectPolicy, sondern die list()-Methode der TopicPolicy.

Die Docs machen in Bezug auf Policies nur diese beiden Vorschläge:

$this->authorize('update', $topic);

oder, wenn es kein Objekt vom Model gibt:

$this->authorize('create', Topic::class);

Unsere Idee einen zusätzlichen Parameter mitzuliefern wird also gar nicht abgebildet. Doch es gibt eine Lösung! Der zweite Parameter für die authorize()-Methode muss ein Array sein, welches an Stelle 0 die Klasse des entsprechenden Models enthält. So kann das Framework die richtige Policy feststellen und die korrekte Methode in der Policy aufrufen. Jeder weitere Wert des Array wird dann als eigener Parameter an die Methode übergeben.

Der richtige Aufruf, um an die list()-Methode aus der TopicPolicy ein Objekt vom Typ Project zu übergeben, sieht also so aus:

<?php
namespace App\Http\Controllers;
use App\Models\Topic;
use App\Models\Project;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class TopicController extends Controller
{
    public function store(Request $request, Project $project)
    {
        $this->authorize('create', [Topic::class, $project]);
        return Topic::create($request->all()):
    }
}

Über das Route Model Binding habe ich direkt das entsprechende Projekt in die Controller-Methode bekommen und muss das Projekt nicht mehr aus der Datenbank abrufen.

So übergibt man also der wirklich sehr bequemen Methode authorize() in einer Controller-Methode weitere Werte, die man dann in der Policy verarbeiten kann… für was auch immer.

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden /  Ändern )

Google Foto

Du kommentierst mit Deinem Google-Konto. Abmelden /  Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden /  Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden /  Ändern )

Verbinde mit %s