Checklist Keamanan Tenancy
Semua item dalam daftar ini adalah jenis kegagalan yang tampak seperti halaman yang bekerja normal. Tenant scope yang diam-diam tidak diterapkan tetap dapat mengembalikan row, merender tabel, dan menghasilkan status 200; tidak ada apa pun di layar yang memberi tahu bahwa where seharusnya ada tetapi hilang. Periksa daftar ini sebelum tenant panel masuk production, dan ulangi setiap kali resource, route, atau job baru ditambahkan.
Pemeriksaan lima menit
# 1. The tenant middleware is actually on the routes.
php artisan route:list --path=app
# 2. Nothing in the panel reads records outside Resource::query().
grep -rn "::query()" app/Panels/App --include=*.php
# 3. Every model a tenant owns has an owner column that is not fillable.
grep -rn "fillable" app/Models --include=*.php2
3
4
5
6
7
8
use PandaPanel\Tenancy\Tenancy;
// 4. Two tenants see two different sets of rows.
expect(Tenancy::for($acme, fn () => DocumentResource::query()->pluck('id')->all()))
->not->toEqual(Tenancy::for($beta, fn () => DocumentResource::query()->pluck('id')->all()));2
3
4
5
Routing dan resolution
Membership
Resource
Write operation
use PandaPanel\Tenancy\Tenancy;
public function creating(Document $document): void
{
$document->workspace_id ??= Tenancy::require()->getKey();
}2
3
4
5
6
Query di luar resource
Resource::query() mencakup list, record lookup, action, bulk action, global search, dan export. Ia tidak melindungi query yang tidak pernah melewati resource.
use PandaPanel\Tenancy\Tenancy;
Document::query()->where('workspace_id', Tenancy::require()->getKey());2
3
Console, queue, dan Octane
Infrastruktur bersama pada database-per-tenant
User id hanya unik di dalam satu tenant, sementara beberapa bagian framework menggunakan id tersebut sebagai key.
Session dan browser
Semantik error
Pastikan hasil berikut benar-benar terlihat di aplikasi. Jika berbeda, berarti ada bagian dari chain yang tidak berjalan:
| Kondisi | Hasil yang diharapkan |
|---|---|
| Tenant tidak ditemukan resolver | 404 No such tenant. |
| Tenant ditemukan tetapi user bukan anggota | 403 |
Model user tanpa HasPanelTenants | 403 |
| Scoped resource tetapi tidak ada tenant yang di-bind | PanelRegistrationException (500) |
| Record key milik tenant lain | 404 |
$tenantRelationship menunjuk method yang tidak valid | PanelRegistrationException (500) |
Status 403 untuk non-member, bukan 404, adalah keputusan yang disengaja: menyembunyikan tenant mana yang ada dari seseorang yang sudah harus menyebutkannya hanya menghasilkan security theatre dan membuat pesan error kurang jelas.
Test yang sebaiknya tersedia
it('refuses a tenant this user does not belong to', function (): void {
$this->actingAs($ada)
->get('/app/documents?workspace='.$beta->getKey())
->assertForbidden();
});
it('answers 404 for a tenant that is not there', function (): void {
$this->get('/app/documents?workspace=999999')->assertNotFound();
});
it('refuses a user model that does not know about tenants at all', function (): void {
$this->actingAs(User::factory()->create()); // no HasPanelTenants
$this->get('/app/documents?workspace='.$acme->getKey())->assertForbidden();
});
it('raises rather than running unscoped when no tenant is bound', function (): void {
expect(fn () => DocumentResource::query()->get())
->toThrow(PanelRegistrationException::class);
});
it('cannot reach another tenant\'s record by key', function (): void {
$this->get('/app/documents/'.$betaDocument->getKey().'?workspace='.$acme->getKey())
->assertNotFound();
});
it('offers no tenant this user does not belong to', function (): void {
$this->get('/app/documents?workspace='.$acme->getKey())
->assertInertia(fn (AssertableInertia $page) => $page->has('tenancy.available', 1));
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Test suite framework sudah memiliki empat pola pertama di tests/Feature/Panel/TenancyTest.php. Salin bentuk test-nya, bukan fixture-nya.
Catatan
- Kegagalan nyata yang paling umum adalah resource yang tidak mendeklarasikan relationship pada single-database panel karena developer mengira ada global scope otomatis. Tidak ada. Relationship yang Anda sebut adalah seluruh mekanismenya.
- Kegagalan paling umum kedua adalah query yang tidak pernah melewati resource — misalnya widget, select, atau endpoint buatan sendiri.
- Kegagalan ketiga adalah queued work, karena error muncul di log worker dan bukan di layar pengguna.
- Tenancy bukan authorization, dan keduanya tidak saling menggantikan. Jalankan panel dengan policy tetap aktif dan pastikan keduanya dapat menolak secara independen; fixture policy yang juga selalu menolak dapat membuat Anda salah menyimpulkan tenant scope bekerja padahal sebenarnya hanya Gate yang menolak.