Skip to main content
BlogERP

Role-Based Access Control di ERP Kustom: NestJS Guards, Tabel Izin PostgreSQL, dan Kepatuhan UU PDP

Cara merancang dan mengimplementasikan sistem RBAC kelas produksi untuk ERP kustom — mencakup skema izin PostgreSQL, NestJS custom guards, pencarian izin dengan cache Redis, dan pertimbangan kepatuhan UU PDP Indonesia.

Matthews Wong

Software Engineer

Juni 20259 menit baca
Role-Based Access Control di ERP Kustom: NestJS Guards, Tabel Izin PostgreSQL, dan Kepatuhan UU PDP

Foto oleh Unsplash

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 > EMPLOYEE

Cache 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.

FAQ

Pertanyaan Umum

Jawaban singkat untuk pertanyaan yang paling sering diajukan pembaca tentang topik ini.

Mengapa warisan peran (role inheritance) sengaja tidak digunakan dalam desain RBAC ini?

Tim memilih untuk tidak mengimplementasikan warisan peran agar model izin tetap eksplisit dan sepenuhnya dapat diaudit. Setiap izin ditetapkan langsung ke peran, sehingga tidak ada rantai hak tersembunyi yang bisa memberikan akses tak terduga. Pendekatan ini memudahkan untuk menjawab pertanyaan 'siapa tepatnya yang bisa menyetujui voucher pembayaran?' tanpa harus menelusuri pohon izin.

Bagaimana cache Redis meningkatkan performa pemeriksaan izin, dan apa trade-off-nya?

Tanpa cache, setiap permintaan API melakukan join database penuh untuk menyelesaikan izin pengguna, menambahkan latensi 5–10 ms per panggilan — latensi yang berlipat ketika satu halaman membuat lebih dari sepuluh panggilan API. Redis menyimpan cache set izin yang telah diselesaikan dengan TTL lima menit, sehingga pemeriksaan berikutnya membutuhkan waktu kurang dari 1 ms. Trade-off-nya adalah ada jendela singkat di mana perubahan peran belum tercermin; cache harus dibatalkan segera saat peran pengguna diperbarui.

Apa peran audit trail granted_by dan granted_at dalam kepatuhan UU PDP?

UU PDP Indonesia (UU No. 27 Tahun 2022) mewajibkan organisasi untuk membuktikan bahwa akses ke data pribadi — termasuk data karyawan, informasi gaji, dan detail kontak pelanggan — dibatasi hanya bagi mereka yang memiliki kebutuhan sah. Kolom granted_by dan granted_at di tabel user_roles menciptakan catatan bertanda waktu untuk setiap penugasan peran, menyediakan bukti audit yang diharapkan oleh regulator.

Mengapa Row-Level Security (RLS) PostgreSQL direkomendasikan selain NestJS guards untuk ERP multi-cabang?

NestJS guards menerapkan aturan akses di lapisan aplikasi, tetapi bug atau miskonfigurasi aplikasi dapat membypass-nya. Kebijakan RLS PostgreSQL diterapkan di tingkat database dan tetap aktif terlepas dari perilaku aplikasi. Untuk ERP multi-cabang — di mana misalnya invoice cabang Jakarta tidak boleh terlihat oleh staf cabang Surabaya — RLS memberikan batas penegakan yang tidak bisa ditembus oleh kesalahan lapisan aplikasi mana pun.

Apa pendekatan yang direkomendasikan untuk memberikan akses database ERP produksi kepada developer?

Developer tidak boleh diberikan akses SUPER_ADMIN ke database produksi. Post ini merekomendasikan penggunaan peran database read-only untuk debugging, mewajibkan proses persetujuan formal untuk setiap akses tulis produksi, dan merotasi kredensial segera setelah setiap penggunaan. Ini secara langsung mengatasi apa yang digambarkan sebagai kegagalan kontrol akses paling umum dalam proyek ERP kustom.