Role-based Access Control (RBAC) — Auth

RBAC (Role-Based Access Control) adalah model authorization di mana izin diberikan berdasarkan role (peran) pengguna, bukan per individu. Mengapa RBAC?…

RBAC (Role-Based Access Control) memberi izin lewat role, bukan ke tiap orang satu per satu. Bayangin kunci-kunci sekolah. Penjaga sekolah gak memotong kunci baru untuk tiap guru yang datang. Dia punya gantungan kunci per jabatan: gantungan "guru" berisi kunci kelas dan ruang guru, gantungan "kepala sekolah" berisi semua itu ditambah kunci ruang arsip. Guru baru cukup diberi gantungan "guru". Di RBAC, gantungan itu role, tiap kunci itu permission, dan pintunya route di server.

🧪 Tebakan cepat: di Portal Sekolah, tombol "Isi nilai" disembunyikan dari Siti karena dia siswa. Apakah itu cukup untuk mencegah Siti mengisi nilai? Tebak dulu, jawabannya ada di bagian "Menyembunyikan tombol vs menolak di server".

User, role, permission

Lesson pertama track ini sudah memberi Portal Sekolah tiga role. Sekarang kita susun lebih rapi: tiap role memegang izinnya sendiri, lalu mewarisi izin role di bawahnya.

Role Izin sendiri Mewarisi Semua izinnya
student (Siti) grades:read grades:read
teacher (Pak Andi) grades:write student grades:read, grades:write
admin (Bu Dewi) users:delete teacher grades:read, grades:write, users:delete

Kenapa lewat role? Bayangin Portal punya 1.000 siswa. Kalau izin diberikan ke tiap user, menambah satu izin untuk semua siswa berarti 1.000 perubahan, dan pasti ada yang terlewat. Dengan role, cukup ubah role student sekali, dan semua siswa ikut di request berikutnya. Mengaudit juga gampang: "siapa yang boleh menghapus user?" dijawab dengan melihat satu daftar.

Ada juga istilah permission-based access control (PBAC): yang dicek adalah izin spesifik seperti grades:write, bukan nama role-nya. Kebanyakan aplikasi menggabungkan keduanya, persis seperti Portal: user punya role, role berisi permission, dan kode mengecek permission-nya. Jadi tulis can("grades:write"), bukan role === "teacher". Kalau besok wakil kepala sekolah juga boleh mengisi nilai, kodenya gak perlu diubah.

Pewarisan role

Pewarisan disimpan terpisah dari daftar izin. Untuk menghitung semua izin sebuah role, ambil izinnya sendiri, lalu telusuri role yang diwarisinya, terus sampai habis:

class RBAC {
  constructor() {
    this.roles = {}; // { role: [izin sendiri] }
    this.hierarchy = {}; // { role: [role yang diwarisi] }
  }

  addRole(roleName, permissions) {
    this.roles[roleName] = permissions;
  }

  setHierarchy(roleName, inheritsFrom) {
    if (!this.hierarchy[roleName]) this.hierarchy[roleName] = [];
    this.hierarchy[roleName].push(inheritsFrom);
  }

  getAllPermissions(roleName) {
    const all = new Set(this.roles[roleName] || []);
    for (const parent of this.hierarchy[roleName] || []) {
      this.getAllPermissions(parent).forEach((p) => all.add(p)); // warisan berantai
    }
    return [...all].sort();
  }

  can(roleName, permission) {
    return this.getAllPermissions(roleName).includes(permission);
  }
}

const rbac = new RBAC();
rbac.addRole("student", ["grades:read"]);
rbac.addRole("teacher", ["grades:write"]);
rbac.addRole("admin", ["users:delete"]);
rbac.setHierarchy("teacher", "student");
rbac.setHierarchy("admin", "teacher");

console.log(rbac.can("teacher", "grades:read")); // true, diwarisi dari student
console.log(rbac.can("student", "grades:write")); // false, warisan cuma satu arah
console.log(rbac.getAllPermissions("admin").join(",")); // grades:read,grades:write,users:delete

Jejak getAllPermissions("admin"):

Langkah Role Izin sendiri Kumpulan sejauh ini
1 admin users:delete users:delete
2 teacher (diwarisi admin) grades:write users:delete, grades:write
3 student (diwarisi teacher) grades:read users:delete, grades:write, grades:read
urutkan grades:read, grades:write, users:delete

Pewarisan cuma satu arah: teacher mewarisi student, bukan sebaliknya.

Cek di server: middleware

Pengecekan dipasang di depan route, sebelum handler jalan. Jadi handler gak perlu ingat mengecek sendiri:

function requirePermission(permission) {
  return (req, res, next) => {
    if (!req.user) return res.status(401).json({ message: "Unauthenticated." });
    if (!rbac.can(req.user.role, permission)) {
      return res.status(403).json({ message: "This action is unauthorized." });
    }
    next();
  };
}

app.post("/grades", requirePermission("grades:write"), storeGrade);
app.delete("/users/:id", requirePermission("users:delete"), deleteUser);

Bu Dewi mengirim POST /grades: admin gak punya grades:write sendiri, tapi mewarisinya lewat teacher, jadi 201 Created. Siti mengirim yang sama: 403 Forbidden.

Menyembunyikan tombol vs menolak di server

Di halaman rapor, React menyembunyikan tombol "Isi nilai" kalau can("grades:write") bernilai false. Itu tampilan yang baik: Siti gak melihat tombol yang gak bisa dia pakai. Tapi request gak butuh tombol. Siti bisa mengirim POST /grades dari DevTools, Postman, atau curl. Yang menghentikannya cuma pengecekan di server, yang menjawab 403. Jadi menyembunyikan tombol saja gak cukup. Itu jawaban tebakannya. Server adalah sumber kebenaran untuk setiap keputusan izin. Di Lab, matikan pengecekan server dan kirim lagi sebagai Siti: nilainya tersimpan, padahal tombolnya gak ada.

Di Laravel: Gate, middleware can, Policy

Laravel punya alat untuk tiap bagian ini:

// app/Providers/AppServiceProvider.php, di method boot()
foreach (['grades:read', 'grades:write', 'users:delete'] as $permission) {
    Gate::define($permission, fn (User $user) => in_array($permission, Rbac::permissionsOf($user->role), true));
}

// routes/api.php
Route::post('/grades', [GradeController::class, 'store'])->middleware(['auth:api', 'can:grades:write']);
Route::delete('/users/{user}', [UserController::class, 'destroy'])->middleware(['auth:api', 'can:users:delete']);

Untuk izin yang bergantung pada datanya, misalnya siswa cuma boleh membaca rapornya sendiri, pakai Policy. Policy menerima user dan data yang mau diakses:

// app/Policies/UserPolicy.php
public function viewGrades(User $user, User $student): bool
{
    return $user->can('grades:read') && ($user->role !== 'student' || $user->is($student));
}

// di controller GET /students/{student}/grades
Gate::authorize('viewGrades', $student); // gagal: 403

Aturan yang lebih kaya (kelas yang diajar, jam sekolah) dibahas di lesson RBAC & ABAC Implementation.

⚠️ Jebakan yang sering ketemu

TL;DR: RBAC: user punya role, role punya permission, dan izin diubah di role, bukan di tiap user (1.000 siswa, satu perubahan). Role bisa mewarisi role lain: getAllPermissions mengambil izin sendiri plus warisannya secara rekursif, tanpa kembar (Set), diurutkan (.sort()), dan can mengecek permission di hasil itu. Kode mengecek permission (grades:write), bukan nama role. Pengecekan yang menentukan ada di server: middleware menjawab 403, dan menyembunyikan tombol cuma tampilan. Di Laravel: Gate::define, middleware can:..., dan Policy untuk izin yang bergantung pada data.

Yang akan kamu pelajari