Skip to content

Rute Terbuka, Rute Terjaga: Public List dan Admin CRUD

Adityo Guni Waluyo

Guard izin terpasang di rute admin, tetapi baris permission belum ada di database: semua admin kecuali superadmin kena 403. Migrasi seed menutup celah itu.

Ringkasan

Admin dapat akses dashboard tapi tidak bisa akses menu organisasi karena 403. Akun admin tidak punya izin organisasi:read dan organisasi:write. Perbaikan dilakukan dengan migrasi yang menambahkan izin tersebut.

Seorang admin organisasi membuka dashboard, mengklik menu Organisasi, dan mendapat 403. Akun itu memegang peran admin, bukan superadmin, dan tokennya masih valid. Rute GET /admin/organizations terdaftar, middleware guard juga terpasang. Yang hilang bukan kode.

Pemeriksaan ke tabel izin menjelaskan semuanya: baris organisasi:read dan organisasi:write tidak pernah ada. Middleware memeriksa, tidak menemukan, menolak. Superadmin lolos karena jalur superadmin melewati pemeriksaan baris, bukan karena barisnya ada.

Guard yang Terpasang Belum Tentu Guard yang Bekerja

Middleware permission di chi adalah fungsi biasa yang dipasang di grup rute. Ia membaca peran dari konteks permintaan, memuat daftar izin, lalu membandingkan. Ketika daftar izin kosong, hasil perbandingan apa pun adalah tolak.

Pemisahan dua lapis rute membuat kesalahan ini tidak terlihat saat pengembangan. Rute publik GET /organizations sengaja dibuka tanpa guard supaya katalog bisa dibaca anonim. Rute admin memakai grup terpisah dengan guard. Keduanya berbagi handler yang mirip, sehingga perbedaan hanya terletak pada definisi grup rute.

Gejala 403 yang konsisten pada semua rute admin, sementara rute publik berjalan normal, mengarah ke guard, bukan ke handler atau koneksi basis data. Perbedaan status antara akun superadmin dan admin mempersempit kecurigaan ke daftar izin.

r.Route("/organizations", func(r chi.Router) {
    r.Get("/", listOrganizations)
    r.Get("/{id}", getOrganization)
})

r.Route("/admin/organizations", func(r chi.Router) {
    r.Use(requirePermission("organisasi:read", "organisasi:write"))
    r.Get("/", listOrganizationsAdmin)
    r.Post("/", createOrganization)
    r.Put("/{id}", updateOrganization)
    r.Delete("/{id}", deleteOrganization)
})

Izin sebagai Data, Bukan Konfigurasi Kode

Kesalahan yang lazim adalah menganggap guard middleware sebagai sumber kebenaran izin. Guard hanya penegak; daftar izin hidup di basis data dan diubah lewat migrasi. Ketika migrasi yang menyeed baris izin tertinggal, tidak ada yang gagal saat build. Kompilasi lolos, unit test handler lolos, tetapi jalur runtime menolak setiap permintaan non-superadmin [1].

OWASP menempatkan otorisasi sebagai keputusan yang harus lengkap di setiap permintaan, bukan diasumsikan dari konfigurasi yang ada [1]. Praktek yang menutup celah ini: perlakukan izin sebagai bagian dari skema, migrasikan bersama tabel peran, dan uji jalur negatif, yaitu peran yang seharusnya boleh tetapi belum diuji.

Perbedaan antara rute yang bekerja dan yang gagal dapat diperiksa dengan satu query ke tabel izin. Jika SELECT pada tabel tersebut untuk peran admin mengembalikan nol baris, masalahnya bukan di handler dan bukan di middleware.

Cara lain menemukannya lebih cepat: uji dengan peran admin, bukan hanya superadmin, di CI. Superadmin melewati pemeriksaan izin, sehingga suite yang hanya menguji superadmin akan selalu hijau meskipun tabel izin kosong. Sisi sebaliknya juga berlaku. Guard yang terpasang pada grup rute yang benar tetapi membaca nama izin yang salah dapat membuka akses lebih lebar, bukan lebih sempit.

Migrasi Seed dan Kebersihan Down

Perbaikan yang diambil adalah migrasi 000084 yang menyeed organisasi:read dan organisasi:write untuk peran admin dan editor. Migrasi up menyisipkan dua baris per peran; migrasi down menghapusnya kembali. Alat seperti golang-migrate menjalankan keduanya secara berurutan dan melacak versi skema di tabel khusus [2].

Idempotensi penting di sini. Jika seed dijalankan ulang, misalnya karena rollback lalu up lagi, penyisipan tanpa penjagaan akan menabrak unique constraint atau menggandakan baris. Pola aman adalah INSERT ... ON CONFLICT DO NOTHING, atau memeriksa keberadaan baris sebelum insert.

Migrasi down yang bersih penting karena environment pengujian sering direset. Jika down tidak menghapus baris yang di-seed, reset skema akan menyisakan data lama dan pengujian dapat lulus atau gagal secara palsu.

chi menyediakan pola grup yang membuat pemisahan publik dan admin eksplisit di satu tempat [3]. Rute publik dan rute terjaga berada dalam blok yang berbeda, sehingga penambahan izin baru hanya menyentuh satu grup. Namun chi tidak menegakkan isi tabel izin; itu tetap tanggung jawab migrasi dan pengujian.

Verifikasi setelah seed: panggil ulang endpoint admin memakai akun admin biasa, bukan superadmin. Kalau respons berubah dari 403 menjadi 200, seed berjalan seperti seharusnya.

Keputusan untuk modul berikutnya: setiap kali rute admin baru ditambahkan, migrasi seed untuk izinnya harus ada di commit yang sama, bukan commit terpisah. Kalau guard dan data izin hidup di dua tempat, cepat atau lambat keduanya akan berbeda, dan yang menemukan perbedaannya adalah pengguna, bukan test.

Sumber:

Artikel terkait