Enforcement RBAC Bidang: Gerbang Middleware dan Filter IN
Enforcement scope bidang di Go: middleware di gerbang, filter IN di repository, dan subtest yang membaca seperti policy keamanan.
Ringkasan
Gerbangnya dijaga middleware ResolveBidang yang nempelin scope bidang ke context abis JWT dicek, jadi handler tinggal baca aja. Di repo filternya pakai sqlx.In buat kategori dan IS NULL OR = buat announcements, route global sama CMS dipisah biar gampang diaudit. Repo sengaja fail-open kalau modul kosong, tapi tetap aman karena semua route admin wajib lewat gerbang dan dikunci subtest t.Run.
Pagi itu tugas saya satu: melanjutkan wiring route di main.go. Resolver ResolveBidang sudah jalan, JWT sudah terverifikasi, tapi setiap endpoint list masih mengembalikan semua baris data. Gerbang sudah berdiri; pintu-pintu ruangan di dalamnya belum berkunci.
Tebakan pertama saya: selipkan pemeriksaan modul di setiap method handler. Untuk dua endpoint mungkin oke. CMS kami punya entities, categories, announcements, reviews, analytics, ditambah route user dan media. Begitu daftar handler memanjang, pola ini berubah jadi utang: satu handler yang kelewat pasang pemeriksaan berarti satu lubang kebocoran yang tidak kelihatan dari code review.
Kontrak dua lapis
Lapisan pertama di gerbang. Middleware ResolveBidang menaruh scope bidang ke context setelah JWT diverifikasi, dan handler membacanya lewat BidangFrom tanpa tahu detail pemetaannya. Lapisan kedua di dalam. Repository menerima slice berisi modul yang boleh dilewati, lalu query entities List dan ListCategories menyaring kolom kategori dengan klausa IN yang dibangun sqlx.In: fungsi ini menerima slice dan mengembalikan query beserta argumen baru yang siap dieksekusi [6].
if len(q.Modules) > 0 {
inQuery, inArgs, err := sqlx.In(" AND c.module IN (?)", q.Modules)
if err != nil {
return nil, 0, err
}
where += inQuery
args = append(args, inArgs...)
}Announcements bentuknya lain. Kolom bidang di sana memakai kontrak NULL berarti pengumuman global, dan karena NULL di SQL tidak pernah true dalam perbandingan apa pun, filter wajib ditulis eksplisit: bidang IS NULL OR bidang = ? [4]. Reviews menyaring lewat JOIN ke categories dengan scope module yang sama. Arah keputusan ini sejalan dengan catatan OWASP soal otorisasi: saat tidak ada aturan akses yang cocok, aplikasi tidak boleh netral; ia wajib memilih menolak atau mengizinkan [2]. Gerbang di middleware memilih menolak lebih dulu.
Di main.go jalurnya saya pisah. globalRoutes berisi menu, setting, dan layanan: konten sitewide yang hanya boleh dilewati bidang ALL lewat RequireGlobalBidang. Sisanya route CMS: entities, categories, announcements, reviews, analytics, semuanya melewati middleware scope sebelum menyentuh handler. Struktur ini bikin audit cukup dua pertanyaan: apakah route ini global, dan kalau bukan, scope-nya dibaca di mana.
Repo fail-open yang disengaja
Bagian yang paling sering ditanya: kenapa repository-nya fail-open? Kalau slice Modules nil atau kosong, filter tidak dipasang sama sekali. Kelihatan seperti lubang, padahal keputusan sadar. Caller publik memang tidak punya konteks scope, dan memaksa mereka mengirim daftar modul kosong demi keamanan cuma menambah ritual tanpa makna. Kontrak keamanannya ditegakkan di satu pintu masuk: semua route admin wajib melewati ResolveBidang, dan integration test mengunci perilakunya.
Subtest sebagai bukti
Buktinya saya tulis sebagai subtest lewat t.Run, yang memberi nama unik dari gabungan nama parent dan entri [7]. Skenarionya membaca seperti policy keamanannya sendiri: admin OLAHRAGA hanya melihat entitas OLAHRAGA, baris BUDAYA tidak boleh muncul, dan ALL melihat semuanya. Saat nanti ada refactor yang melonggarkan filter, test inilah yang berteriak lebih dulu, bukan laporan pengguna.
Mengunci satu gerbang plus beberapa filter yang tertata ternyata lebih murah dirawat daripada menyebar pemeriksaan ke belasan handler. Lapisan bawah tetap bodoh dan enak dipakai, lapisan atas yang menjaga, dan buktinya jalan otomatis tiap build.
Sumber: