Mengamankan Dokumen Tim di Laravel dengan Policy dan Private Download
Cegah akses lintas tim dengan scoped binding, policy keanggotaan, dan pengunduhan file dari disk privat.
This article is not yet available in your selected language. Showing the original.
Aplikasi kolaborasi menyimpan dokumen beberapa tim. URL /teams/12/documents/900 tidak berarti dokumen 900 milik tim 12, dan pengguna yang login belum tentu anggota tim tersebut. Menyembunyikan tombol download juga tidak mencegah seseorang mengetik URL langsung.
Studi kasus Laravel 13 ini memasang dua lapis pemeriksaan: scoped route binding memastikan hubungan dokumen dengan tim, sementara policy memeriksa keanggotaan pengguna. Berkas disimpan privat dan dikirim melalui controller setelah kedua pemeriksaan lulus.
Relasi sebagai kontrak binding
Contoh mengasumsikan model Team memiliki relasi documents() dan members(). Document mempunyai kolom team_id, path, original_name, serta relasi team(). Pada pivot keanggotaan, keberadaan baris berarti pengguna masih menjadi anggota aktif. Jika aplikasi menyimpan status undangan atau membership nonaktif, tambahkan syarat status pada relasi atau policy.
use App\Http\Controllers\DownloadTeamDocumentController;
use Illuminate\Support\Facades\Route;
Route::get('/teams/{team}/documents/{document}/download', DownloadTeamDocumentController::class)
->middleware('auth')
->scopeBindings()
->name('teams.documents.download');
Dengan nama parameter document, Laravel mencari relasi documents pada parent Team. Pastikan relasi tersebut benar, bukan query global ke semua dokumen. Pasangan team dan document yang tidak cocok menghasilkan 404 sebelum file dibaca.
Policy memeriksa anggota pada setiap request
Buat policy lewat php artisan make:policy DocumentPolicy --model=Document --no-interaction. Laravel dapat menemukan policy berdasarkan konvensi nama; jika aplikasi memakai pemetaan khusus, daftarkan sesuai pola yang sudah digunakan.
<?php
namespace App\Policies;
use App\Models\Document;
use App\Models\User;
class DocumentPolicy
{
public function view(User $user, Document $document): bool
{
return $document->team->members()
->whereKey($user->id)
->exists();
}
}
Jangan hanya membandingkan $user->current_team_id dengan ID pada URL. Tim yang dipilih pengguna bukan bukti keanggotaan. Jika dokumen memiliki aturan lebih ketat daripada tingkat tim, misalnya hanya dapat diakses pemilik atau role tertentu, terapkan kondisi tambahan pada policy yang sama.
File privat, path berasal dari database
Gunakan disk local yang dikonfigurasi untuk penyimpanan privat. Jangan menaruh berkas ini pada disk public atau mengembalikan URL publik. Simpan hasil path yang dibuat store() saat upload; pengunggah tidak boleh mengisi kolom path secara bebas.
<?php
namespace App\Http\Controllers;
use App\Models\Document;
use App\Models\Team;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\StreamedResponse;
class DownloadTeamDocumentController extends Controller
{
public function __invoke(Team $team, Document $document): StreamedResponse
{
Gate::authorize('view', $document);
$disk = Storage::disk('local');
abort_unless($disk->exists($document->path), 404);
return $disk->download($document->path, basename($document->original_name), [
'Cache-Control' => 'private, no-store',
'X-Content-Type-Options' => 'nosniff',
]);
}
}
Generate tautan melalui route('teams.documents.download', [$team, $document]). basename() membatasi nama unduhan ke nama berkas; validasi nama dan karakter kontrol tetap dilakukan saat upload. Header cache membantu mencegah browser atau proxy menyimpan dokumen sensitif, tetapi tidak dapat menarik kembali file yang sudah diunduh pengguna.
Daftar dokumen juga membutuhkan scope
Policy detail tidak otomatis membatasi query daftar atau pencarian. Ambil dokumen dari $team->documents() setelah mengotorisasi akses tim. Jika aplikasi memakai pencarian global, pastikan filter keanggotaan diterapkan sebelum pagination dan hitungan total; jangan mengambil semua data lalu menyembunyikannya di Blade.
Signed URL hanya membuktikan URL tidak dimodifikasi dan belum kedaluwarsa. Ia tidak membuktikan pemegang URL masih menjadi anggota tim. Jika memakai signed URL pada endpoint ini, tetap jalankan autentikasi dan policy. Untuk temporary URL object storage, tentukan TTL pendek dan pahami bahwa pencabutan keanggotaan tidak membatalkan URL yang sudah diberikan sebelum TTL habis.
Skenario pengujian
Anggota tim A berhasil mengunduh dokumen A. Pengguna tim B mendapat 403 untuk pasangan URL yang valid tetapi tidak berizin. Dokumen A pada URL tim B mendapat 404. Uji pengguna yang membership-nya dicabut, berkas yang hilang, pengguna belum login, dan upaya mengirim path lain melalui query string.
Assert respons hanya mengandung isi file yang diizinkan. Gunakan fake storage serta factory pengguna, tim, dan dokumen untuk pengujian aplikasi; pastikan struktur pivot sesuai model proyek.
Rujukan API: Authorization policies, Scoped bindings, dan File downloads.
Comments 0
Share your questions, experiences, or feedback about this article.
Sign in with Google to leave a comment.
Sign in with Google
No comments yet. Be the first to share your feedback.