Set Bidang User Lain: Aksi Istimewa, Bukan Field Biasa
Hanya superadmin boleh menentukan bidang user lain; default ALL bikin 19 user lama lolos migrasi tanpa terkunci.
Ringkasan
Nambah kolom bidang ke users sih gampang, yang ribet tuh nentuin siapa boleh atur scope orang lain. Akhirnya diputus cuma superadmin yang boleh set bidang lewat resolveBidang, kalau admin/editor maksa kirim bakal kena 403 tapi kalau nggak kirim tetap bisa edit field lain. Default-nya dibikin ALL biar 19 user lama nggak kekunci pas deploy dan semua aman terkunci test.
Nambahin kolom bidang ke tabel users itu bagian gampang. Migration 000057 tinggal ADD COLUMN dengan ENUM dan default. Pertanyaan yang makan waktu justru ini: siapa yang berhak nentuin scope orang lain?
Awalnya saya kira tinggal nurut pola modul lain. Author kan diambil dari session yang lagi login, jadi biarin konsisten aja. Ternyata beda hal: author itu nyatet siapa yang nulis, bidang itu nyatet wilayah kontennya.
Tebakan saya runtuh pas nulis aturannya. Men-set scope user lain itu tindakan meta: mengelola konten dan mengelola siapa yang boleh mengelola itu dua permukaan keamanan yang beda. Kalau perlakuannya disamain, celah privilege escalation-nya udah terbuka sebelum ada yang sadar.
403 yang Pilih Kasus
Penjaganya fungsi resolveBidang(actor, requested, defaultALL). Aturan utamanya tegas: cuma superadmin yang boleh nentuin bidang user.
Detail halusnya: 403 itu pilih kasus. Non-superadmin yang MENGIRIM nilai bidang kena ErrForbidden. Yang nggak ngirim bidang tetap sukses edit email, avatar, atau password user lain. Bukan celah, ini keputusan desain: satu form users dipakai semua role tanpa perlu mode read-only terpisah buat satu field, dan server yang megang garis batasnya, bukan UI. Tiga kasus superadmin juga kekunci di test: create tanpa bidang default-nya ALL, update tanpa bidang berarti tidak diubah, dan nilai di luar whitelist kena ErrInvalidInput. Delapan skenario, satu tabel test.
OWASP nurunkun prinsipnya jelas. Deny by Default bilang aplikasi nggak boleh netral dan harus selalu memutuskan deny atau permit [4]; Least Privilege narik kesimpulannya: hak yang nggak perlu, nggak dikasih [6]. Men-set scope orang lain jelas masuk kategori hak yang jarang, dan server harus menolaknya dengan tegas, bukan mengabaikannya diam-diam.
Default ALL sebagai Jaring Pengaman
Kolomnya sendiri didefinisikan ENUM NOT NULL DEFAULT 'ALL', dan pilihan ini soal operasional. Ada 19 user lama per 29 September 2026. Default ALL bikin mereka nggak ada yang terkunci pas deploy, dan ALTER TABLE additive di MySQL 5.7 umumnya jalan INPLACE yang tetap mendukung DML konkuren, jadi tabel users nggak perlu dikunci [5]. Kebalikannya, default ke bidang tertentu bakal narik 19 orang ke dalam scope yang salah sebelum mereka sempat ngapa-ngapain.
Bagian yang gampang kelupaan tapi pernah bikin ribet duluan: tipe generated. Tiap kontrak OpenAPI berubah, v1.d.ts di frontend saya regenerasi, dan field bidang langsung nongol di tipe request dan response tanpa diasah manual. Urutannya kaku tapi nyelametkan: OpenAPI dulu, generate tipe, baru form nyusul. Salah satu form lupa ngirim field, TypeScript yang teriak di build, bukan user yang kena error 400 di jam sore.
UI ikut nurut. Dropdown bidang dan badge role di users-view cuma muncul buat superadmin. validBidang di backend pakai whitelist konstanta scope: PARIWISATA, OLAHRAGA, BUDAYA, KEPEMUDAAN, ALL. Struct Actor sekarang bawa Bidang, jadi layernya jelas: handler bawa identitas, service yang memutuskan.
Delapan skenario test itu ngerasain kayak spesifikasi yang bisa dijalankan. Satu tabel test nyatet: superadmin create tanpa bidang jatuh ke ALL, superadmin create eksplisit ke OLAHRAGA, superadmin update tanpa bidang nggak ngubah apa-apa, set ALL eksplisit sah, nilai HEX ditolak ErrInvalidInput, admin dan editor kena ErrForbidden pas ngirim nilai, dan non-superadmin tanpa bidang lolos karena emang nggak nyentuh. Kalau besok ada role baru, tabel ini yang jadi kontrak: tambah kasusnya dulu, baru ubah resolveBidang.
Kontrasnya kelihatan pas dibandingin modul kemarin. Di pengumuman, penulis ber-bidang tetap bisa kerja penuh di wilayahnya, cukup diingetin pas nabrak batas. Di user management nggak ada wilayahnya: editor nggak boleh banget nentuin bidang siapa pun, termasuk bidangnya sendiri, karena yang lagi dibahas bukan konten tapi struktur siapa boleh pegang wilayah. Dua modul, satu konvensi scope, dua tingkat kepercayaan yang beda; bedanya disimpen di service masing-masing, bukan diratain demi seragam.
Pendapat saya: perlakukan assignment scope kayak kunci brankas, bukan label di folder. Field biasa boleh diedit siapa saja yang boleh edit barisnya; field ini nggak. Dengan garis yang dipasang jelas di satu fungsi dan dikunci test, form-nya bisa dipakai semua role tanpa saya mikirin siapa lagi yang bisa menggeser scope sendiri dari belakang.
Terakhir, soal jerat kecil yang hampir kejadian: select di form edit sengaja selalu mengirim bidang, berarti superadmin nggak perlu mikir dua kasus. Server yang nanggung kerumitan lewat nilainya yang nullable, klien cukup jujur. Pola server-pemutus-kasus ini yang bikin form seragam aman dipakai lintas role.