Authorization Page
Sebuah standalone page menentukan sendiri siapa yang boleh membukanya melalui satu static method. Pemeriksaan dijalankan pada route — di dalam Page::render(), sebelum proses lain berlangsung — sehingga page yang disembunyikan dari sidebar tetap akan ditolak ketika URL-nya dibuka secara langsung. Menyembunyikan navigation bukan mekanisme authorization.
Tiga lapisan berlaku pada setiap request page, dengan urutan: Panel harus mengizinkan user masuk, page harus menyatakan akses diperbolehkan, lalu setiap widget di dalam page melakukan pemeriksaannya sendiri.
Contoh minimal yang berfungsi
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Pages;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use PandaPanel\Pages\Page;
final class Settings extends Page
{
protected static ?string $navigationIcon = 'settings';
public static function canAccess(): bool
{
$user = Auth::user();
return $user instanceof User && $user->is_admin;
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
User biasa tidak lagi melihat Settings di sidebar, dan GET /admin/settings menghasilkan 403, bukan redirect.
canAccess()
public static function canAccess(): bool; // PandaPanel\Pages\Page, default trueMethod ini static, tanpa argument, dan tanpa parameter user. Ia menjawab untuk user yang sedang terautentikasi karena hanya user itulah yang dapat digunakan untuk merender page. Menerima parameter user justru memberi kesan bahwa page dapat dirender seolah-olah sebagai user lain, padahal tidak.
Ada dua caller yang memanggilnya, dan keduanya penting:
| Caller | Kapan | Efek ketika false |
|---|---|---|
Page::render() | setiap request ke route page | abort_unless(static::canAccess(), 403) |
NavigationBuilder | saat membangun sidebar | item dihapus sebelum badge dievaluasi |
use PandaPanel\Pages\Page;
use Illuminate\Support\Facades\Gate;
final class AuditLog extends Page
{
public static function canAccess(): bool
{
return Gate::allows('viewAuditLog');
}
}2
3
4
5
6
7
8
9
10
Tujuan utamanya adalah enforcement pada level route. Framework sendiri mempertahankan test berikut:
it('returns 403 for a direct url to a page the user cannot access', function (): void {
$page = new ForbiddenPage;
expect(fn () => $page->render())->toThrow(HttpException::class);
});2
3
4
5
Menyembunyikan dibanding menolak akses
Keduanya adalah switch yang berbeda, dan hanya salah satunya merupakan kontrol keamanan.
use PandaPanel\Pages\Page;
final class Webhooks extends Page
{
// Not in the sidebar. Still routed, still openable.
protected static bool $shouldRegisterNavigation = false;
// Not openable. This is the control.
public static function canAccess(): bool
{
return false;
}
}2
3
4
5
6
7
8
9
10
11
12
13
$shouldRegisterNavigation = false membuat navigationItem() mengembalikan null. Ini tepat untuk page yang hanya dicapai melalui link dari tempat lain, tetapi tidak memengaruhi route sama sekali.
Akses Panel diperiksa lebih dahulu
Page di dalam Panel yang tidak boleh dimasuki user tidak akan pernah mencapai page tersebut karena PandaPanel\Http\Middleware\ResolvePanel dijalankan sebelum controller.
use App\Models\User;
use Illuminate\Contracts\Auth\Authenticatable;
use PandaPanel\Core\Panel;
$panel->canAccess(static fn (?Authenticatable $user): bool => $user instanceof User && $user->is_admin);2
3
4
5
public function canAccess(Closure $callback): self; // fn (?Authenticatable): bool
public function isAccessibleTo(?Authenticatable $user): bool;2
isAccessibleTo() mengajukan dua pertanyaan dan keduanya harus setuju: predicate milik Panel sendiri, serta PanelUser::canAccessPanel() pada user model jika model tersebut mengimplementasikan contract. Panel yang menjawab yes tidak dapat mengabaikan user model yang menjawab no.
Middleware berjalan setelah auth dan verified, sehingga $request->user() sudah tersedia. Guest diarahkan ke login lebih dahulu; user yang sudah login tetapi gagal pemeriksaan menerima 403, bukan redirect. boot() milik Panel baru berjalan setelah pemeriksaan berhasil, sehingga user yang ditolak tidak dapat memicu pekerjaan boot milik Panel.
it('redirects a guest away from a page rather than rendering it', function (): void {
$this->get('/admin/settings')->assertRedirect('/login');
});
it('refuses a page inside a panel the user cannot access', function (): void {
$this->actingAs(User::factory()->create())
->get('/admin/settings')
->assertForbidden();
});2
3
4
5
6
7
8
9
Lihat Authorization dan Panel access.
Middleware, untuk kondisi yang tidak cukup dijelaskan boolean
canAccess() hanya dapat menjawab yes atau no. Beberapa concern membutuhkan redirect — misalnya password confirmation, signed URL, atau subscription wall. Kondisi seperti itu ditempatkan pada route melalui middleware:
use Illuminate\Auth\Middleware\RequirePassword;
use PandaPanel\Pages\Page;
final class SecuritySettings extends Page
{
/** @var list<string> */
protected static array $middleware = [RequirePassword::class];
}2
3
4
5
6
7
8
/** @return list<string> */
public static function middleware(): array;2
Registrar menambahkan middleware tersebut ke route page di atas stack middleware milik Panel:
$route = $this->router->get($page::routePath(), PanelPageController::class)
->defaults('page', $page)
->name('pages.'.$page::slug());
if ($page::middleware() !== []) {
$route->middleware($page::middleware());
}2
3
4
5
6
7
Inilah alasan built-in page SecuritySettings menggunakan middleware, bukan pemeriksaan canAccess(): session yang sudah stale harus diarahkan ke screen konfirmasi. Menggunakan canAccess() akan mengubah kebutuhan re-confirmation menjadi 403.
Middleware adalah list<string>, sehingga semua format middleware string yang diterima Laravel dapat digunakan, termasuk parameter seperti 'throttle:6,1' dan 'can:manage-billing'.
Widget pada page
Widget di dalam page melakukan authorization secara independen:
public static function canView(): bool; // PandaPanel\Widgets\Widget, default trueuse PandaPanel\Widgets\StatsWidget;
final class RevenueStats extends StatsWidget
{
public static function canView(): bool
{
return auth()->user()?->can('viewRevenue') === true;
}
}2
3
4
5
6
7
8
9
PandaPanel\Pages\WidgetCollection::for() memfilter melalui canView() sebelum membuat instance widget. Karena itu widget yang tidak diizinkan tidak pernah diinstansiasi dan tidak pernah menjalankan query. Page yang boleh dibuka user dapat menampilkan kumpulan widget berbeda untuk user berbeda tanpa branching di widgets(). Lihat Widget authorization.
Cluster
Cluster memiliki gate sendiri dan gate tersebut independen dari member di dalamnya:
use PandaPanel\Clusters\Cluster;
final class OperationsCluster extends Cluster
{
public static function canAccess(): bool
{
return auth()->user()?->can('viewOperations') === true;
}
}2
3
4
5
6
7
8
9
Cluster yang tidak boleh dimasuki user menyembunyikan seluruh kumpulan dari sidebar dan tidak menghasilkan sub-navigation bar. Namun cluster tidak meng-authorize member-nya — setiap member tetap melakukan pemeriksaannya sendiri dan route member itulah yang menolak akses. Lihat Clusters.
Apa yang dilakukan sidebar
PandaPanel\Support\NavigationBuilder merupakan convenience layer di atas pemeriksaan yang sama:
Page::canAccess()danResource::canViewAny()dijalankan sebelum hal lain, sehingga item unauthorized tidak pernah sampai pada proses evaluasi badge.- Group yang menjadi kosong setelah authorization dihapus, bukan dirender sebagai heading tanpa item.
- Semuanya dievaluasi per request. Hasil authorization tidak pernah ditulis ke panel manifest karena hasilnya bergantung pada user.
it('omits a page the user cannot access from navigation', function (): void {
$labels = collect(app(NavigationBuilder::class)->for($panel, '/nav-host'))
->flatMap(fn (array $group): array => array_column($group['items'], 'label'))
->all();
expect($labels)->toContain('Settings')
->and($labels)->not->toContain('Restricted');
});2
3
4
5
6
7
8
Menguji akses sebuah page
use App\Models\User;
it('lets an admin open the page', function (): void {
$this->actingAs(User::factory()->admin()->create())
->get('/admin/settings')
->assertOk();
});
it('refuses everybody else', function (): void {
$this->actingAs(User::factory()->create())
->get('/admin/settings')
->assertForbidden();
});2
3
4
5
6
7
8
9
10
11
12
13
Lakukan assertion terhadap route, bukan hanya terhadap canAccess(). Test yang hanya memanggil static method masih dapat lolos untuk page yang route-nya sebenarnya belum terdaftar. Lihat Testing authorization.
Gotchas
canAccess()dijalankan setidaknya dua kali per page view. Sekali untuk sidebar dan sekali dirender(). Jaga supaya pemeriksaannya murah, atau cache hasilnya di dalam request — satu query per pemanggilan dapat berubah menjadi satu query per navigation item.canAccess()tidak dapat melakukan redirect.abort_unless()menghasilkan 403. Concern yang perlu mengirim user ke tempat lain harus menggunakan middleware.- Page di dalam cluster yang tidak dapat diakses user tetap memiliki route. Gate cluster mengendalikan navigation dan cluster bar. Tambahkan pemeriksaan pada page juga jika page tersebut sendiri harus benar-benar ditutup.
- Strict authorization tidak berlaku di sini.
strictAuthorization()mengatur ability Resource berbasis Gate dan melempar exception ketika policy tidak tersedia.Page::canAccess()hanyalah PHP biasa dan default-nyatrue; page yang lupa meng-override method tersebut terbuka bagi semua user yang boleh masuk ke Panel. - Akses Panel diperiksa sebelum Panel boot. Apa pun yang didaftarkan pada boot callback Panel — termasuk plugin — tidak berjalan untuk user yang ditolak.
Auth::user()di dalamcanAccess()membaca guard yang sedang aktif. Tidak ada setting guard khusus padaPanel; Panel yang memakai non-default guard melakukannya denganauthMiddleware(['auth:admin']), dan middlewareAuthenticatemilik Laravel yang membuat guard tersebut aktif pada request. Static call yang sama ketika dijalankan di luar route group Panel akan membaca application default.