Matriks RBAC Dinamis & Anti-Eskalasi
Sistem hak akses yang memetakan setiap peran ke daftar fitur beserta empat aksi baku (read, add, edit, delete) langsung dari database.
Membaca sekitar 2 menit
Sistem hak akses yang memetakan setiap peran ke daftar fitur beserta empat aksi baku (read, add, edit, delete) langsung dari database.
Izin diperiksa berlapis di SSR page (requirePageAccess), Server Action (requirePermission), hingga penyembunyian tombol di klien (usePermission), dilengkapi transaksi anti-eskalasi saat pengubahan matriks.
Tanpa validasi kepemilikan hak di server, seorang admin biasa bisa membuat peran baru yang memiliki izin melebihi dirinya sendiri.
Masalahnya dulu
Banyak aplikasi memulai pengelolaan akses dengan menulis kondisi statis seperti if (user.role === 'ADMIN'). Begitu organisasi bertambah besar dan meminta peran baru seperti 'Auditor Keuangan' yang hanya boleh melihat log audit dan mengedit item tanpa boleh menghapus, kode statis langsung berantakan. Namun, ketika kita membuat matriks izin yang bisa diedit dari dashboard, muncul celah keamanan klasik: bagaimana mencegah seorang admin membuat peran baru dengan izin Super Admin lalu memasangkannya ke akun temannya?
Bagaimana matriks izin dan cegah-eskalasi dirancang
Sistem membagi otorisasi menjadi tiga tabel relasional: roles, features, dan role_permissions dengan empat kolom boolean baku: is_read, is_add, is_edit, dan is_delete. Daftar seluruh modul fitur dideklarasikan terpusat di common/constants/rbac.constant.ts. Saat seorang pengguna membuka halaman pengaturan matriks izin dan menyimpan perubahan, service saveRolePermissions memeriksa matriks izin milik si pengubah terlebih dahulu. Ia dilarang keras memberikan centang aksi apa pun yang dirinya sendiri tidak memilikinya.
Kamu tidak boleh bisa memberikan kunci ruangan yang kamu sendiri tidak punya izin untuk memasukinya.
Implementasi di ngodingan ini
Berikut adalah potongan logika pemuatan izin ter-cache di common/lib/auth.ts serta transaksi validasi anti-eskalasi di modules/dashboard/role-permission/role-permission.service.ts:
// common/lib/auth.ts
export const getPermissionMap = cache(
async (roleUuid: string): Promise<PermissionMap> => {
const rows = await prisma.rolePermission.findMany({
where: { roleUuid },
select: {
isRead: true, isAdd: true, isEdit: true, isDelete: true,
feature: { select: { key: true, availableActions: true } },
},
});
const map: PermissionMap = {};
for (const row of rows) {
map[row.feature.key] = ACTIONS.filter(
(a) => row[ACTION_COLUMN[a]] && row.feature.availableActions.includes(a)
);
}
return map;
}
);
// modules/dashboard/role-permission/role-permission.service.ts
if (role.name === ROLE_SUPER_ADMIN && !actor.isSuperAdmin) {
throw new AppError("Hanya Super Admin yang dapat mengubah hak akses role Super Admin");
}
if (role.uuid === actor.roleUuid && !actor.isSuperAdmin) {
throw new AppError("Kamu tidak dapat mengubah hak akses role yang sedang kamu pakai");
}
if (actorPermissions) {
const allowed = actorPermissions[feature.key] ?? [];
const notAllowed = entry.actions.filter((a) => !allowed.includes(a));
if (notAllowed.length > 0) {
throw new AppError(
`Kamu tidak dapat memberikan aksi ${notAllowed.join(", ")} pada "${feature.name}" karena kamu sendiri tidak memilikinya.`
);
}
}Jebakan dan detail yang harus dijaga
- Membungkus getPermissionMap dengan React.cache(): Dalam satu kali render halaman dashboard, izin bisa dicek oleh layout sidebar, guard halaman, dan beberapa komponen sekaligus. Dengan cache(), query database hanya dijalankan tepat satu kali per request.