Lompat ke konten
dokumentasi.id
Commit a2522f5
v1
Star645

Kamu membaca versi lama. Halaman ini hanya bisa dibaca.

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.
Fig. 1: Rantai keputusan otorisasiRequest 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-107. Syarat itu melekat pada model karena User mengimplementasikan FilamentUser app/Models/User.php:17-20. 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-29.

Panel admin memakai halaman login kustom lewat ->login(Login::class) app/Providers/Filament/AdminPanelProvider.php:33. Kelas itu hanya mewarisi BaseLogin dan mendaftarkan render hook tombol Google setelah form login, tanpa pemeriksaan peran app/Filament/Pages/Auth/Login.php:11-19. Plugin Shield didaftarkan pada panel yang sama app/Providers/Filament/AdminPanelProvider.php:56-58. 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-53. 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-45.

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.
Fig. 2: Pembentukan permission key untuk resource komentarNama 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-105. Daftar method yang digenerate Shield untuk setiap policy memuat 11 entri, dari viewAny sampai reorder config/filament-shield.php:118-134.

Kelas FilamentPermission view_anyPermission update
ProjectResourceview_any_projectupdate_project
TicketResourceview_any_ticketupdate_ticket
TicketCommentResourceview_any_ticket::commentupdate_ticket::comment
TicketPriorityResourceview_any_ticket::priorityupdate_ticket::priority

Pemakaian key bertanda :: terlihat langsung di policy komentar dan prioritas: view_any_ticket::comment app/Policies/TicketCommentPolicy.php:44-47 dan view_any_ticket::priority app/Policies/TicketPriorityPolicy.php:15-18.

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-32. Tiga role dibuat dengan Role::firstOrCreate dan di-sync dengan himpunan permission yang berbeda database/seeders/RoleSeeder.php:39-61. DatabaseSeeder memanggil seeder ini database/seeders/DatabaseSeeder.php:23-24.

RolePermission yang di-syncBaris
super_adminseluruh Permission::all()database/seeders/RoleSeeder.php:44-45
adminsemua permission kecuali delete_userdatabase/seeders/RoleSeeder.php:47-49
member11 permission baca ditambah update_ticket untuk drag & dropdatabase/seeders/RoleSeeder.php:51-61

Shield juga mendefinisikan super_admin dengan define_via_gate bernilai false dan intercept_gate bernilai before config/filament-shield.php:65-70, serta role panel_user untuk user yang hanya butuh akses panel config/filament-shield.php:83-86. 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-179.

Policy per model

PolicyModelPemeriksaan tambahan
ProjectPolicyProjecthasAccessToProject: super_admin atau baris project_members
TicketPolicyTickethasAccessToTicket: super_admin, pembuat ticket, assignee, atau member proyek
TicketCommentPolicyTicketCommentticket harus ada, lalu aturan yang sama dengan ticket
NotificationPolicyNotificationuser_id sama dengan user
UserPolicyUsertidak ada, murni permission
RolePolicySpatie\Permission\Models\Roletidak ada, murni permission
TicketPriorityPolicyTicketPrioritytidak ada, murni permission

ProjectPolicy::view mengembalikan $authUser->can('view_project') && $this->hasAccessToProject(...) app/Policies/ProjectPolicy.php:33-36. Helper hasAccessToProject meloloskan super_admin lebih dulu, lalu memeriksa $project->members()->where('users.id', $authUser->id)->exists() app/Policies/ProjectPolicy.php:19-26. Komentar di dalam berkas menyatakan helper itu mencerminkan ProjectResource::getEloquentQuery() supaya satu permission tidak pernah cukup app/Policies/ProjectPolicy.php:15-18.

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-39. TicketPolicy::view menggabungkan permission dengan helper tersebut app/Policies/TicketPolicy.php:46-49. TicketCommentPolicy mengambil $ticketComment->ticket lebih dahulu dan mengembalikan false bila ticket tidak ada, lalu mengulang aturan ticket app/Policies/TicketCommentPolicy.php:19-42.

NotificationPolicy::ownsNotification hanya membandingkan $notification->user_id dengan user, dengan super_admin sebagai pengecualian app/Policies/NotificationPolicy.php:18-25. UserPolicy tidak menerima argumen model pada view, update, dan delete, sehingga hanya permission yang menentukan app/Policies/UserPolicy.php:12-15. 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-10, sedangkan RolePolicy bekerja pada model peran milik Spatie app/Policies/RolePolicy.php:8. 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-233. TicketResource::getEloquentQuery memakai tiga cabang yang sama dengan policy, yaitu assignee, created_by, dan member proyek app/Filament/Resources/Tickets/TicketResource.php:45-62. TicketCommentResource::getEloquentQuery menerapkan pola itu untuk komentar app/Filament/Resources/TicketComments/TicketCommentResource.php:27-31.

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-53, member proyek bisa mengubah ticket di proyeknya sendiri tests/Feature/PolicyOwnershipTest.php:55-65, 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. RoleSeeder menghasilkan view_ticket_comment dari resource ticket_comment database/seeders/RoleSeeder.php:15-32, sedangkan TicketCommentPolicy memeriksa view_ticket::comment app/Policies/TicketCommentPolicy.php:44-47. Role member yang hanya di-sync dengan bentuk pertama tidak akan lolos policy komentar database/seeders/RoleSeeder.php:51-61.

⚠️ ProjectPolicy::viewAny hanya memeriksa view_any_project tanpa cek keanggotaan app/Policies/ProjectPolicy.php:28-31. Penyaringan daftar sepenuhnya bergantung pada getEloquentQuery app/Filament/Resources/Projects/ProjectResource.php:217-233, jadi query yang melewati method itu akan menampilkan proyek asing.

⚠️ super_admin lolos lebih dulu di setiap helper kepemilikan app/Policies/TicketPolicy.php:22-39. 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.