Aturan Akses Panel
Menentukan siapa yang boleh masuk ke sebuah panel, berbeda dengan apa yang boleh mereka lakukan setelah berada di dalamnya. Pertanyaan ini diperiksa oleh middleware pada setiap request yang masuk ke panel dan dijawab oleh dua aturan independen yang keduanya harus menyetujui akses: predicate pada panel dan method pada user model. Hal yang lebih spesifik — resource mana, record mana, action mana — merupakan tanggung jawab Gate dan didokumentasikan pada Authorization.
Aturan pada panel
use App\Models\User;
use Illuminate\Contracts\Auth\Authenticatable;
use PandaPanel\Core\Panel;
use PandaPanel\Core\PanelProvider;
final class AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->path('admin')
->auth()
->canAccess(static fn (?Authenticatable $user): bool => $user instanceof User && $user->is_admin);
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
User yang sudah login tetapi gagal melewati pemeriksaan ini menerima 403, bukan redirect. Guest tidak pernah mencapai pemeriksaan tersebut pada panel yang memanggil auth() karena middleware auth melakukan redirect terlebih dahulu.
public function canAccess(Closure $callback): self // Closure(?Authenticatable): bool
public function isAccessibleTo(?Authenticatable $user): bool2
canAccess() menyimpan satu closure; memanggilnya dua kali akan mengganti closure sebelumnya. Parameternya nullable karena isAccessibleTo() juga dipanggil di luar request — misalnya oleh panel switcher dan oleh firstAccessibleTo() ketika menentukan ke mana user diarahkan setelah login.
Aturan pada user
Bagian lainnya berada pada user model:
use Illuminate\Foundation\Auth\User as Authenticatable;
use PandaPanel\Contracts\PanelUser;
use PandaPanel\Core\Panel;
final class User extends Authenticatable implements PanelUser
{
public function canAccessPanel(Panel $panel): bool
{
return ! $this->suspended;
}
}2
3
4
5
6
7
8
9
10
11
PandaPanel\Contracts\PanelUser mendeklarasikan tepat satu method:
public function canAccessPanel(Panel $panel): bool;Gunakan closure untuk aturan yang berkaitan dengan panel ini — misalnya "panel ini hanya untuk administrator". Gunakan contract untuk aturan yang berkaitan dengan account — misalnya suspended, tidak memiliki tenant, atau belum onboarding. Karena ditulis pada model, aturan tersebut otomatis berlaku untuk setiap panel dan tidak akan terlupakan ketika panel keempat ditambahkan.
Keduanya harus menyetujui
Panel::isAccessibleTo() memeriksa contract terlebih dahulu, lalu closure:
$panel->isAccessibleTo($user);
// false when the user model implements PanelUser and says no
// false when the panel's own predicate says no
// true when neither refuses2
3
4
Panel yang mengizinkan akses tidak dapat mengesampingkan user model yang menolak, dan user model yang permisif tidak dapat melonggarkan predicate panel. User model yang tidak menerapkan keduanya tidak ditolak oleh aturan tambahan apa pun, sesuai perilaku panel yang sudah ada sebelum contract ini diperkenalkan.
// A panel that refuses everybody stays refused, however permissive the model is.
$panel = Panel::make('both')->canAccess(static fn (?Authenticatable $user): bool => false);
$panel->isAccessibleTo($permissiveUser); // false2
3
4
Tempat pemeriksaan dijalankan
PandaPanel\Http\Middleware\ResolvePanel ditambahkan terakhir pada middleware stack panel, setelah auth dan verified, sehingga $request->user() sudah tersedia ketika predicate dijalankan. Urutannya:
- me-resolve panel berdasarkan id yang ditanam pada parameter middleware;
- mengikatnya sebagai current panel;
- menjalankan
abort_unless($panel->isAccessibleTo($request->user()), 403); - menjalankan boot callback panel dan
boot()milik plugin.
Langkah 4 sengaja dijalankan setelah langkah 3: user yang ditolak tidak boleh memicu pekerjaan boot panel.
Karena pemeriksaan dilakukan oleh middleware, semua route yang didaftarkan panel ikut terlindungi — page, resource page, action endpoint, search endpoint, upload, dan export. Tidak ada page yang dapat terlupa untuk diproteksi.
Hal yang tidak dicakup oleh akses panel
Akses panel adalah pintu masuk, bukan permission system. Setelah berada di dalam panel:
| Pertanyaan | Dijawab oleh |
|---|---|
| Apakah user boleh melihat resource ini di sidebar dan menampilkan list-nya? | Resource::canViewAny() → ability policy viewAny |
| Apakah user boleh membuka record ini? | Resource::canView($record) → view |
| Apakah user boleh membuat, mengedit, atau menghapus? | canCreate(), canEdit(), canDelete() dan varian bulk *Any |
| Apakah user boleh membuka standalone page ini? | Page::canAccess(), diterapkan pada route |
| Apakah user boleh melihat widget ini? | Widget::canView(), diperiksa sebelum data() berjalan |
| Apakah user boleh menjalankan action ini? | authorization milik action itu sendiri |
Semua pemeriksaan tersebut dilakukan secara independen di server pada saat fitur digunakan. Menyembunyikan navigation item atau button hanya merupakan kenyamanan UI, bukan kontrol keamanan.
use PandaPanel\Pages\Page;
final class Reports extends Page
{
public static function canAccess(): bool
{
return auth()->user()?->can('view-reports') === true;
}
}2
3
4
5
6
7
8
9
Strict authorization
Policy yang tidak ada secara default terbaca sama seperti policy yang menolak. Saat development, ubah kondisi tersebut menjadi failure yang jelas:
$panel->strictAuthorization(); // strictAuthorization(bool $strictAuthorization = true): self
panel('admin')->hasStrictAuthorization(); // bool, false by default2
Ketika aktif, resource yang model-nya tidak memiliki registered policy — atau policy-nya tidak memiliki ability yang sedang diperiksa — akan melempar PandaPanel\Exceptions\PanelAuthorizationException daripada sekadar menolak. Policy yang mendefinisikan before() dikecualikan karena method tersebut dapat menjawab semua ability. Seluruh can*() pada Resource melewati satu pemanggilan authorize(), sehingga ini benar-benar satu pemeriksaan, bukan delapan implementasi terpisah.
Fitur ini nonaktif secara default karena mengubah 403 menjadi 500. Di development, exception menyebutkan model dan ability yang bermasalah; di production, denial merupakan jawaban yang lebih aman.
Guest
Route panel menyertakan middleware auth Laravel secara default, sehingga guest diarahkan ke login kecuali auth stack sengaja dikosongkan. Login yang digunakan bergantung pada apakah panel menyediakan login miliknya sendiri:
$panel->login(); // a login page at /{path}/login, carrying this panel's brandDengan register_guest_redirect aktif (default), PandaPanel\Support\PanelLoginRedirect mengarahkan guest yang membuka URL panel ke login panel tersebut jika tersedia, atau ke route('login') jika tidak. Intended URL dipertahankan sehingga setelah login mereka kembali ke tujuan awal.
auth() tetap berguna ketika panel juga harus mewajibkan email terverifikasi:
$panel->auth(); // auth + verified
$panel->auth(verified: false); // auth only2
Panel publik sekarang merupakan pilihan middleware yang eksplisit. canAccess() tetap dijalankan dan menerima null untuk guest:
$panel->authMiddleware([]);$panel->canAccess(static fn (?Authenticatable $user): bool => $user !== null || app()->isLocal());Akses dan panel switcher
SharePanelData memfilter panel switcher menggunakan isAccessibleTo(), sehingga daftar yang dilihat user sama persis dengan kumpulan panel yang memang boleh mereka masuki. PanelManager::firstAccessibleTo() menggunakan predicate yang sama untuk menentukan panel tujuan user yang sudah login ketika request tidak menyebutkan panel tertentu.
use PandaPanel\Core\PanelManager;
app(PanelManager::class)->firstAccessibleTo($user); // ?Panel, panels walked in id order2
3
Keduanya dapat memanggil predicate di luar request panel. Jaga agar pemeriksaannya murah dan tidak bergantung pada request state — canAccess() yang membaca request()->route() akan menerima pertanyaan yang konteksnya memang tidak dapat dijawab.
Mengujinya
it('keeps a normal user out of the admin panel', function (): void {
$this->actingAs(User::factory()->create())
->get('/admin')
->assertForbidden();
});
it('never boots a panel the user may not enter', function (): void {
$ran = false;
app(PanelManager::class)->get('admin')->bootUsing(function () use (&$ran): void {
$ran = true;
});
$this->actingAs(User::factory()->create())->get('/admin')->assertForbidden();
expect($ran)->toBeFalse();
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Catatan
- Penggunaan 403 daripada redirect disengaja. Redirect dapat memberi tahu user yang tidak berhak bahwa panel lain ada, dan dapat menciptakan loop untuk user yang sama sekali tidak memiliki panel.
canAccess()berjalan pada setiap request ke panel. Query database di dalamnya berarti satu query tambahan pada setiap page load; cache hasilnya pada user model jika mahal.- Closure tidak pernah diserialisasi.
toSharedArray()tidak membawa jejak closure danpanel:cachehanya menyimpan class name. - User model dapat mengimplementasikan
PanelUsertetapi tetap ditolak predicate panel, dan sebaliknya. Saat debugging 403, periksa keduanya.