Belajar Laravel #12: Authorization — Gate & Policy, Pagar Kedua Setelah Login

Hari ini saya belajar tentang Authorization di Laravel — bagian kedua dari sistem keamanan setelah Authentication. Kalau Authentication itu tentang “siapa kamu?”, maka Authorization tentang “apa yang boleh kamu lakukan?”. Saya baru menyadari betapa pentingnya memisahkan kedua konsep ini setelah sempat membuat aplikasi dengan if-else panjang untuk cek role user.

Di episode sebelumnya saya sudah menyiapkan Authentication — user bisa login dan logout. Tapi masalah muncul ketika ada beberapa user dengan tingkatan berbeda: admin boleh menghapus postingan, editor boleh mengedit, dan penulis biasa hanya bisa menulis. Tanpa mekanisme authorization yang rapi, logika pencarian permission bercampur di dalam controller dan Blade view — persis seperti yang saya alami.

Gate: Pagar Sederhana untuk Satu Aksi

Gate adalah cara paling dasar di Laravel untuk mendefinisikan apakah user boleh melakukan sesuatu. Bayangkan Gate sebagai petugas di pintu masuk gedung: dia tidak peduli kamu siapa secara pribadi, yang dia tanyakan adalah “apakah kamu punya izin masuk ke ruangan ini?”

Gate didefinisikan di file app/Providers/AuthServiceProvider.php di dalam method boot():

// app/Providers/AuthServiceProvider.php
use App\Models\Post;
use App\Models\User;
use Illuminate\Support\Facades\Gate;

public function boot(): void
{
    Gate::define('update-post', function (User $user, Post $post) {
        // Hanya pemilik post atau admin yang boleh edit
        return $user->id === $post->user_id || $user->is_admin;
    });

    Gate::define('delete-post', function (User $user, Post $post) {
        return $user->is_admin;
    });
}

Di sini saya mendefinisikan dua gate: update-post dan delete-post. Yang pertama membolehkan user yang membuat post atau admin. Yang kedua hanya admin. Notice bahwa setiap gate menerima $user (user yang sedang login) dan parameter opsional lainnya — dalam kasus ini $post yang ingin diubah.

Menggunakan Gate di Controller dan View

Setelah gate didefinisikan, saya bisa memanggilnya di mana saja. Di controller:

// app/Http/Controllers/PostController.php
public function update(Request $request, Post $post)
{
    $this->authorize('update-post', $post);

    // Baru sampai sini jika user punya izin
    $post->update($request->validated());
    return redirect()->route('posts.show', $post);
}

$this->authorize() otomatis melempar 403 Forbidden jika gate mengembalikan false. Saya tidak perlu menulis if-else manual — Laravel menangani semuanya. Di Blade view, saya bisa pakai @can directive:

@can('update-post', $post)
    <a href="{{ route('posts.edit', $post) }}">Edit</a>
@endcan

@can('delete-post', $post)
    <form action="{{ route('posts.destroy', $post) }}" method="POST">
        @csrf
        @method('DELETE')
        <button type="submit">Hapus</button>
    </form>
@endcan

Blade directive @can dan @cannot sangat membantu menjaga tampilan tetap rapi — tombol edit dan hapus hanya muncul jika user memang punya izin. Saya juga bisa menggunakan @if(Gate::allows('update-post', $post)) di view, tapi @can lebih ringkas dan mudah dibaca.

Gate Denial: Menangani Akses yang Ditolak

Ketika Gate menolak, Laravel menggunakan Illuminate\Auth\Access\AuthorizationException. Secara default, response 403 dikirim. Tapi saya bisa mengkustomisasi respons penolakan dengan callback Gate::after():

// Di AuthServiceProvider.php
Gate::after(function (User $user, string $ability) {
    // Superadmin lolos semua gate
    if ($user->is_superadmin) {
        return true;
    }
});

Callback Gate::after() berjalan setelah semua gate diperiksa. Jika ada yang mengembalikan true, user diizinkan. Ini berguna untuk role superadmin yang tidak perlu diperiksa per-aksi. Saya belajar bahwa gate dijalankan secara berurutan sampai ada yang jelas mengizinkan atau menolak.

Policy: Gate yang Terorganisir per Model

Seiring aplikasi bertambah besar, semua gate di AuthServiceProvider bisa jadi sangat panjang. Policy adalah solusinya — satu file per model yang memuat semua logika izin terkait model tersebut. Bayangkan policy sebagai dokumen peraturan khusus untuk satu jenis ruangan di gedung.

Membuat policy dengan artisan:

php artisan make:policy PostPolicy --model=Post

Ini menghasilkan file app/Policies/PostPolicy.php dengan method-method yang sudah tersedia:

// app/Policies/PostPolicy.php
namespace App\Policies;

use App\Models\Post;
use App\Models\User;

class PostPolicy
{
    public function viewAny(User $user): bool
    {
        return true; // Semua user bisa melihat daftar post
    }

    public function view(User $user, Post $post): bool
    {
        return true; // Semua user bisa melihat post
    }

    public function create(User $user): bool
    {
        return $user->hasRole('author');
    }

    public function update(User $user, Post $post): bool
    {
        return $user->id === $post->user_id || $user->is_admin;
    }

    public function delete(User $user, Post $post): bool
    {
        return $user->is_admin;
    }
}

Setiap method mewakili satu ability. Parameter kedua ($post) otomatis di-resolve dari route model binding. Laravel secara otomatis mendeteksi policy yang terkait dengan model — saya tidak perlu mendaftarkan satu per satu di AuthServiceProvider. Cukup buat PostPolicy dan Laravel tahu bahwa method-methodnya berlaku untuk model Post.

Policy di Controller dan View

Menggunakan policy di controller hampir identik dengan gate, tapi lebih terstruktur:

// Di PostController.php
public function update(Request $request, Post $post)
{
    $this->authorize('update', $post);

    $validated = $request->validated();
    $post->update($validated);

    return redirect()->route('posts.show', $post)
        ->with('success', 'Post berhasil diperbarui.');
}

public function destroy(Post $post)
{
    $this->authorize('delete', $post);
    $post->delete();

    return redirect()->route('posts.index')
        ->with('success', 'Post berhasil dihapus.');
}

Laravel otomatis mencocokkan $this->authorize('update', $post) dengan method update() di PostPolicy. Jika tidak ada policy atau method-nya tidak ada, Laravel fallback ke gate dengan nama App\Models\Post@update. Saya menemukan mekanisme fallback ini sangat elegan — policy hanyalah cara terorganisir untuk mendefinisikan gate.

Di Blade, penggunaannya juga mirip:

@can('update', $post)
    <button class="btn btn-primary">Edit</button>
@endcan

@can('delete', $post)
    <button class="btn btn-danger">Hapus</button>
@endcan

Authorization di Blade: @can, @cannot, dan @guest

Laravel menyediakan beberapa directive Blade untuk authorization yang sangat membantu:

@can('create', App\Models\Post::class)
    <a href="{{ route('posts.create') }}" class="btn btn-success">
        Tulis Baru
    </a>
@endcan

@cannot('update', $post)
    <p class="text-muted">Anda tidak memiliki akses untuk mengedit post ini.</p>
@endcannot

@guest
    <a href="{{ route('login') }}">Login untuk mengakses</a>
@endguest

Saya juga bisa memeriksa multiple abilities sekaligus dengan @canAny:

@canAny(['update', 'delete'], $post)
    <div class="admin-panel">
        Panel admin: edit atau hapus tersedia
    </div>
@endcanAny

Dan untuk mengecek apakah user punya ability tertentu tanpa conditional Blade, saya bisa pakai @if(Gate::allows('update', $post)) atau helper auth()->user()->can('update', $post) di mana saja — bahkan di model atau service class.

Gate vs Policy: Kapan Pakai yang Mana?

Sempat bingung kapan harus pakai Gate langsung dan kapan harus buat Policy. Setelah praktek, ini pemahaman saya:

SituasiGunakanAlasan
Izin sederhana, sekali pakaiGateCukup definisikan di AuthServiceProvider, tidak perlu file terpisah
Izin terkait model (CRUD)PolicySatu file terorganisir untuk semua ability model tersebut
Izin global (misal: akses admin panel)GateTidak terikat ke model tertentu
Role-based permission kompleksPolicy + GatesPolicy untuk model, Gates untuk role-level

Latihan Kecil: Mengamankan Post Controller

Saya mencoba mengamankan PostController penuh menggunakan policy. Berikut hasilnya:

// app/Http/Controllers/PostController.php
namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function index()
    {
        $posts = Post::latest()->paginate(10);
        return view('posts.index', compact('posts'));
    }

    public function create()
    {
        $this->authorize('create', Post::class);
        return view('posts.create');
    }

    public function store(Request $request)
    {
        $this->authorize('create', Post::class);

        $validated = $request->validate([
            'title'   => 'required|max:255',
            'content' => 'required',
        ]);

        $post = Post::create([
            ...$validated,
            'user_id' => auth()->id(),
        ]);

        return redirect()->route('posts.show', $post);
    }

    public function update(Request $request, Post $post)
    {
        $this->authorize('update', $post);

        $validated = $request->validate([
            'title'   => 'required|max:255',
            'content' => 'required',
        ]);

        $post->update($validated);
        return redirect()->route('posts.show', $post);
    }

    public function destroy(Post $post)
    {
        $this->authorize('delete', $post);
        $post->delete();
        return redirect()->route('posts.index');
    }
}

Perhatikan bagaimana setiap method yang melakukan perubahan data memanggil $this->authorize() di awal. Method index() tidak perlu karena semua user boleh melihat daftar post. Pattern ini membuat controller tetap bersih — logika izin ada di policy, bukan bercampur di dalam handler.

Kesalahan yang Saya Temui

Beberapa jebakan yang saya alami selama belajar authorization:

  • Policy tidak ter-deteksi otomatis: Laravel hanya auto-discover policy jika namespace-nya benar (App\Policies\PostPolicy). Kalau saya membuat file di folder yang salah atau namespace tidak sesuai, authorization gagal diam-diam — semua gate mengembalikan false.
  • Lupa parameter kedua di Gate: Gate::define('update-post', function (User $user) { ... }) tanpa parameter $post akan error saat saya pakai $this->authorize('update-post', $post). Parameter kedua harus ada meskipun tidak dipakai.
  • View tetap menampilkan tombol: Jika saya lupa menambahkan @can di Blade, tombol edit/hapus tetap muncul untuk semua user. Tapi saat diklik, controller akan melempar 403 — aman dari sisi data, tapi user experience-nya buruk.

Yang Dipelajari Hari Ini

Authorization di Laravel memiliki dua level: Gate untuk pendekatan sederhana dan global, Policy untuk pendekatan terorganisir per model. Keduanya menggunakan mekanisme yang sama di balik layar. Yang paling penting bagi saya: jangan campur logika izin di dalam controller — delegasikan ke Gate atau Policy supaya mudah di-maintain dan di-test.

Di episode berikutnya saya akan belajar tentang Session & Cookie — bagaimana Laravel menyimpan state antar request, flash message, dan praktik aman menggunakan cookie. Ini penting untuk fitur seperti shopping cart atau form multi-step.


Sumber

Tinggalkan Balasan

Alamat email Anda tidak akan dipublikasikan. Ruas yang wajib ditandai *

Situs ini menggunakan Akismet untuk mengurangi spam. Pelajari bagaimana data komentar Anda diproses