Ringkasan Utama
Role-based access control di ERP custom memberi user sejumlah role, dan role memegang permission berupa pasangan resource dan action. Penegakannya di NestJS guard, bukan di antarmuka, adalah yang mencegah karyawan menyetujui klaim pengeluarannya sendiri, sekaligus bukti yang diharapkan kepatuhan UU PDP dari sebuah sistem di Indonesia.
Kontrol akses adalah salah satu aspek paling kritis dan paling sering kurang dirancang dalam ERP kustom. Dalam pekerjaan konsultasi ERP freelance saya, kontrol akses saya rancang dari hari pertama sebagai perhatian utama, bukan ditambahkan belakangan.
Merancang Model Izin
Saya menggunakan model RBAC standar: pengguna ditugaskan satu atau lebih peran, peran memiliki izin, dan izin adalah pasangan (resource, action). Izin untuk membuat invoice adalah resource='invoice', action='create'.
Resource dan Aksi
Resource memetakan ke entitas ERP utama: 'invoice', 'leave_request', 'ap_voucher', 'employee', 'customer', 'vendor', 'report'. Aksi adalah: 'create', 'read', 'update', 'delete', 'approve', 'reject', 'void', 'export'. Tidak semua aksi berlaku untuk semua resource.
Desain Hierarki Peran
Saya mendefinisikan empat level peran: SUPER_ADMIN (akses penuh), FINANCE_MANAGER (akses AR/AP penuh), FINANCE_STAFF (buat dan baca invoice, tanpa persetujuan), dan EMPLOYEE (buat permintaan cuti, baca hanya catatan sendiri).
ERP RBAC Data Model (PostgreSQL)
┌─────────────┐ ┌──────────────────┐ ┌──────────────┐
│ users │──────►│ user_roles │◄──────│ roles │
│─────────────│ N:M │──────────────────│ N:M │──────────────│
│ id │ │ user_id (FK) │ │ id │
│ name │ │ role_id (FK) │ │ name │
│ email │ │ granted_by │ │ description │
│ dept_id │ │ granted_at │ └──────┬───────┘
│ is_active │ └──────────────────┘ │
└─────────────┘ │ N:M
▼
┌────────────────────┐
│ role_permissions │
│────────────────────│
│ role_id (FK) │
│ permission_id (FK) │
└──────────┬─────────┘
│
▼
┌────────────────────┐
│ permissions │
│────────────────────│
│ id │
│ resource (e.g. │
│ 'invoice') │
│ action (e.g. │
│ 'create','read', │
│ 'approve','void')│
└────────────────────┘
Role Hierarchy Example:
SUPER_ADMIN > FINANCE_MANAGER > FINANCE_STAFF > EMPLOYEECache pencarian izin di Redis dengan TTL singkat (5 menit). Dengan cache Redis, pemeriksaan izin menambahkan kurang dari 1ms setelah permintaan pertama. Batalkan cache segera ketika peran pengguna berubah.
Skema PostgreSQL untuk RBAC
Skema RBAC terdiri dari empat tabel: users, roles, permissions, user_roles, dan role_permissions. Saya menambahkan kolom granted_by dan granted_at ke user_roles sehingga selalu ada audit trail tentang siapa yang menugaskan peran mana.
Implementasi Guard NestJS
RBAC guard menggunakan antarmuka CanActivate NestJS. Ia membaca dekorator @RequirePermission(resource, action), mengekstrak ID pengguna dari JWT, dan memeriksa PermissionService. Jika pengguna tidak memiliki izin, ia melempar ForbiddenException dengan pesan spesifik.
// NestJS: Custom RBAC guard with PostgreSQL permission lookup
// 1. Permission decorator
export const RequirePermission = (resource: string, action: string) =>
SetMetadata('permission', { resource, action });
// 2. Guard implementation
@Injectable()
export class RbacGuard implements CanActivate {
constructor(
private reflector: Reflector,
private permissionService: PermissionService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const permission = this.reflector.get<{ resource: string; action: string }>(
'permission',
context.getHandler(),
);
if (!permission) return true; // No permission required
const request = context.switchToHttp().getRequest();
const userId = request.user?.id;
if (!userId) throw new UnauthorizedException();
const hasAccess = await this.permissionService.userHasPermission(
userId,
permission.resource,
permission.action,
);
if (!hasAccess) {
throw new ForbiddenException(
`Missing permission: ${permission.resource}:${permission.action}`
);
}
return true;
}
}
// 3. Permission service with cached lookups
@Injectable()
export class PermissionService {
constructor(
@InjectRepository(UserRole) private userRoleRepo: Repository<UserRole>,
private cacheManager: Cache,
) {}
async userHasPermission(
userId: string,
resource: string,
action: string,
): Promise<boolean> {
const cacheKey = `perm:${userId}:${resource}:${action}`;
const cached = await this.cacheManager.get<boolean>(cacheKey);
if (cached !== undefined) return cached;
const result = await this.userRoleRepo.query(`
SELECT 1
FROM user_roles ur
JOIN role_permissions rp ON rp.role_id = ur.role_id
JOIN permissions p ON p.id = rp.permission_id
WHERE ur.user_id = $1
AND p.resource = $2
AND p.action = $3
LIMIT 1
`, [userId, resource, action]);
const hasAccess = result.length > 0;
await this.cacheManager.set(cacheKey, hasAccess, 300); // 5 min TTL
return hasAccess;
}
}
// 4. Usage on controller
@Controller('invoices')
@UseGuards(JwtAuthGuard, RbacGuard)
export class InvoiceController {
@Post()
@RequirePermission('invoice', 'create')
create(@Body() dto: CreateInvoiceDto) { ... }
@Post(':id/approve')
@RequirePermission('invoice', 'approve')
approve(@Param('id') id: string) { ... }
@Post(':id/void')
@RequirePermission('invoice', 'void')
void(@Param('id') id: string) { ... }
}Pertimbangan Kepatuhan UU PDP
UU PDP Indonesia (UU No. 27 Tahun 2022) membebankan kewajiban pada organisasi yang memproses data pribadi. RBAC adalah kontrol kunci untuk kepatuhan UU PDP — Anda harus dapat menunjukkan bahwa akses ke data pribadi dibatasi hanya bagi mereka yang memiliki kebutuhan yang sah.
Jangan berikan semua pengembang akses SUPER_ADMIN ke database produksi. Gunakan peran database read-only untuk debugging, wajibkan proses persetujuan formal untuk akses tulis produksi, dan rotasi kredensial segera setelah setiap penggunaan.
Row-Level Security untuk ERP Multi-Tenant atau Multi-Cabang
Jika ERP melayani beberapa cabang yang seharusnya tidak melihat data satu sama lain, Anda memerlukan row-level security selain RBAC. PostgreSQL mendukung kebijakan RLS di tingkat database — ini diberlakukan bahkan jika lapisan aplikasi memiliki bug.
Pengujian Kontrol Akses
Logika kontrol akses harus memiliki cakupan test yang komprehensif. Untuk setiap endpoint yang dilindungi, tulis test untuk pengguna dengan peran yang benar, pengguna dengan peran yang tidak cukup (403), permintaan tidak diautentikasi (401), dan SUPER_ADMIN selalu bisa mengakses.

