9
Kontrol Akses dan Peran
Tiga lapis menentukan siapa boleh melakukan apa di panel admin: gerbang masuk panel, permission granular dari Filament Shield, dan Policy per model yang menambahkan pemeriksaan kepemilikan baris. Ketiga lapis itu dievaluasi berurutan pada setiap request, dan satu permission saja tidak pernah cukup untuk membuka record di proyek tempat user bukan anggota.
Rantai keputusan otorisasi
graph TD
A["Request ke panel admin"] --> B{"canAccessPanel lolos?"}
B -- "tidak" --> R1["ditolak: email belum terverifikasi atau tanpa role"]
B -- "ya" --> C["Policy method dipanggil"]
C --> D{"authUser->can('...')"}
D -- "gagal" --> R2["denied"]
D -- "lolos" --> E{"hasAccessToProject / hasAccessToTicket"}
E -- "super_admin" --> OK["allowed"]
E -- "created_by = user" --> OK
E -- "assignee ticket" --> OK
E -- "member project" --> OK
E -- "bukan anggota" --> R2
style A fill:#336791,color:#fff
style D fill:#f59e0b,color:#000
style E fill:#8b5cf6,color:#fff
style OK fill:#2d7d46,color:#fff
style R1 fill:#dc2626,color:#fff
style R2 fill:#dc2626,color:#fff
%% fig: Rantai keputusan otorisasi
%% desc: Request melewati gerbang panel, permission Shield, lalu pemeriksaan kepemilikan baris sebelum record dibuka.Gerbang masuk panel
canAccessPanel mengembalikan hasVerifiedEmail() && roles()->exists() app/Models/User.php:104-1071. Syarat itu melekat pada model karena User mengimplementasikan FilamentUser app/Models/User.php:17-202. Kedua syarat diuji terpisah: user terverifikasi tanpa role ditolak, user dengan role tetapi email belum terverifikasi ditolak, dan akses diberikan hanya ketika keduanya terpenuhi tests/Feature/UserCanAccessPanelTest.php:9-293.
Panel admin memakai halaman login kustom lewat ->login(Login::class) app/Providers/Filament/AdminPanelProvider.php:334. Kelas itu hanya mewarisi BaseLogin dan mendaftarkan render hook tombol Google setelah form login, tanpa pemeriksaan peran app/Filament/Pages/Auth/Login.php:11-195. Plugin Shield didaftarkan pada panel yang sama app/Providers/Filament/AdminPanelProvider.php:56-586. Untuk alur login lengkap, lihat Instalasi, Konfigurasi, dan Docker.
Permission granular dari Shield
FilamentShield::buildPermissionKeyUsing mengganti cara Shield menyusun nama permission app/Providers/AppServiceProvider.php:30-537. Untuk setiap Resource, nama kelas dibuang sufiks Resource, diubah ke snake_case, lalu setiap _ diganti ::; hasilnya ditempel setelah affix aksi app/Providers/AppServiceProvider.php:34-458.
graph LR
A["TicketCommentResource"] --> B["beforeLast('Resource') = TicketComment"]
B --> C["snake = ticket_comment"]
C --> D["replace('_', '::') = ticket::comment"]
E["affix update"] --> F["update_ticket::comment"]
D --> F
style A fill:#f59e0b,color:#000
style D fill:#8b5cf6,color:#fff
style F fill:#2d7d46,color:#fff
%% fig: Pembentukan permission key untuk resource komentar
%% desc: Nama kelas Filament dipotong dan dinormalisasi, lalu affix aksi digabungkan menjadi key yang diperiksa Policy.Closure itu mengembalikan key utuh, sehingga bentuk akhirnya ditentukan oleh closure, bukan oleh permissions.separator dan permissions.case di config/filament-shield.php:101-1059. Daftar method yang digenerate Shield untuk setiap policy memuat 11 entri, dari viewAny sampai reorder config/filament-shield.php:118-13410.
| Kelas Filament | Permission view_any | Permission update |
|---|---|---|
ProjectResource | view_any_project | update_project |
TicketResource | view_any_ticket | update_ticket |
TicketCommentResource | view_any_ticket::comment | update_ticket::comment |
TicketPriorityResource | view_any_ticket::priority | update_ticket::priority |
Pemakaian key bertanda :: terlihat langsung di policy komentar dan prioritas: view_any_ticket::comment app/Policies/TicketCommentPolicy.php:44-4711 dan view_any_ticket::priority app/Policies/TicketPriorityPolicy.php:15-1812.
Role hasil seeder
RoleSeeder menyusun permission dari enam resource (project, ticket, ticket_priority, ticket_comment, notification, user) dikali lima aksi (view, view_any, create, update, delete), yaitu 30 permission, lalu memanggil Permission::firstOrCreate untuk masing-masing database/seeders/RoleSeeder.php:15-3213. Tiga role dibuat dengan Role::firstOrCreate dan di-sync dengan himpunan permission yang berbeda database/seeders/RoleSeeder.php:39-6114. DatabaseSeeder memanggil seeder ini database/seeders/DatabaseSeeder.php:23-2415.
| Role | Permission yang di-sync | Baris |
|---|---|---|
super_admin | seluruh Permission::all() | database/seeders/RoleSeeder.php:44-4516 |
admin | semua permission kecuali delete_user | database/seeders/RoleSeeder.php:47-4917 |
member | 11 permission baca ditambah update_ticket untuk drag & drop | database/seeders/RoleSeeder.php:51-6118 |
Shield juga mendefinisikan super_admin dengan define_via_gate bernilai false dan intercept_gate bernilai before config/filament-shield.php:65-7019, serta role panel_user untuk user yang hanya butuh akses panel config/filament-shield.php:83-8620. Pemberian role ke user dilakukan lewat bulk action assignRole pada UserResource, dengan mode replace yang memanggil roles()->sync atau mode add yang memanggil roles()->syncWithoutDetaching app/Filament/Resources/Users/UserResource.php:153-17921.
Policy per model
| Policy | Model | Pemeriksaan tambahan |
|---|---|---|
ProjectPolicy | Project | hasAccessToProject: super_admin atau baris project_members |
TicketPolicy | Ticket | hasAccessToTicket: super_admin, pembuat ticket, assignee, atau member proyek |
TicketCommentPolicy | TicketComment | ticket harus ada, lalu aturan yang sama dengan ticket |
NotificationPolicy | Notification | user_id sama dengan user |
UserPolicy | User | tidak ada, murni permission |
RolePolicy | Spatie\Permission\Models\Role | tidak ada, murni permission |
TicketPriorityPolicy | TicketPriority | tidak ada, murni permission |
ProjectPolicy::view mengembalikan $authUser->can('view_project') && $this->hasAccessToProject(...) app/Policies/ProjectPolicy.php:33-3622. Helper hasAccessToProject meloloskan super_admin lebih dulu, lalu memeriksa $project->members()->where('users.id', $authUser->id)->exists() app/Policies/ProjectPolicy.php:19-2623. Komentar di dalam berkas menyatakan helper itu mencerminkan ProjectResource::getEloquentQuery() supaya satu permission tidak pernah cukup app/Policies/ProjectPolicy.php:15-1824.
TicketPolicy::hasAccessToTicket menambah tiga jalur selain keanggotaan proyek: pembuat ticket lewat created_by, assignee lewat assignees(), dan member proyek lewat project()->whereHas('members', ...) app/Policies/TicketPolicy.php:22-3925. TicketPolicy::view menggabungkan permission dengan helper tersebut app/Policies/TicketPolicy.php:46-4926. TicketCommentPolicy mengambil $ticketComment->ticket lebih dahulu dan mengembalikan false bila ticket tidak ada, lalu mengulang aturan ticket app/Policies/TicketCommentPolicy.php:19-4227.
NotificationPolicy::ownsNotification hanya membandingkan $notification->user_id dengan user, dengan super_admin sebagai pengecualian app/Policies/NotificationPolicy.php:18-2528. UserPolicy tidak menerima argumen model pada view, update, dan delete, sehingga hanya permission yang menentukan app/Policies/UserPolicy.php:12-1529. RolePolicy mengikuti pola yang sama dan hanya membaca permission seperti view_role app/Policies/RolePolicy.php:20-23, dengan tipe model Spatie\Permission\Models\Role app/Policies/RolePolicy.php:8. TicketPriorityPolicy membaca key ticket::priority tanpa pemeriksaan baris app/Policies/TicketPriorityPolicy.php:15-18.
Ada dua model peran di repository ini. App\Models\Roles hanya mendeklarasikan $fillable = ['name', 'guard_name'] app/Models/Roles.php:7-1032, sedangkan RolePolicy bekerja pada model peran milik Spatie app/Policies/RolePolicy.php:831. Halaman Model Data dan Migration memuat tabel roles beserta kolomnya.
Query resource mencerminkan policy
ProjectResource::getEloquentQuery menyaring daftar ke proyek tempat user menjadi member, kecuali super_admin app/Filament/Resources/Projects/ProjectResource.php:217-23333. TicketResource::getEloquentQuery memakai tiga cabang yang sama dengan policy, yaitu assignee, created_by, dan member proyek app/Filament/Resources/Tickets/TicketResource.php:45-6234. TicketCommentResource::getEloquentQuery menerapkan pola itu untuk komentar app/Filament/Resources/TicketComments/TicketCommentResource.php:27-3135.
Perilaku yang dikunci test
tests/Feature/PolicyOwnershipTest.php menguji lima skenario kepemilikan: user dengan permission update_ticket tetap tidak bisa mengubah ticket di luar proyeknya tests/Feature/PolicyOwnershipTest.php:45-5336, member proyek bisa mengubah ticket di proyeknya sendiri tests/Feature/PolicyOwnershipTest.php:55-6537, user tidak bisa melihat komentar di luar proyeknya walau punya view_ticket::comment tests/Feature/PolicyOwnershipTest.php:67-85, user tidak bisa melihat proyek yang bukan miliknya tests/Feature/PolicyOwnershipTest.php:87-94, dan notification hanya terbaca oleh pemiliknya tests/Feature/PolicyOwnershipTest.php:96-112.
Jebakan
⚠️ Nama permission di seeder dan di policy tidak selalu sama.
RoleSeedermenghasilkanview_ticket_commentdari resourceticket_commentdatabase/seeders/RoleSeeder.php:15-3213, sedangkanTicketCommentPolicymemeriksaview_ticket::commentapp/Policies/TicketCommentPolicy.php:44-4711. Rolememberyang hanya di-sync dengan bentuk pertama tidak akan lolos policy komentar database/seeders/RoleSeeder.php:51-6118.
⚠️
ProjectPolicy::viewAnyhanya memeriksaview_any_projecttanpa cek keanggotaan app/Policies/ProjectPolicy.php:28-3141. Penyaringan daftar sepenuhnya bergantung padagetEloquentQueryapp/Filament/Resources/Projects/ProjectResource.php:217-23333, jadi query yang melewati method itu akan menampilkan proyek asing.
⚠️
super_adminlolos lebih dulu di setiap helper kepemilikan app/Policies/TicketPolicy.php:22-3925. Mencabut role itu dari seorang user langsung mempersempit aksesnya ke baris yang memang miliknya.
Pembatasan akses untuk pengguna di luar organisasi dibahas terpisah di Portal Eksternal Klien.