Chapter Row Level Security menyalakan saklar ENABLE ROW LEVEL SECURITY, membuktikan tabel tanpa policy menjadi lubang hitam, lalu menulis dua policy paling sederhana: satu USING untuk membaca, satu WITH CHECK untuk menulis. Aturan bisnis nyata jarang sesederhana itu: kasir boleh melihat transaksi miliknya atau transaksi publik, tetapi tidak boleh melihat yang rahasia; manajer perlu mengawasi transaksi timnya; aplikasi multi-cabang harus melihat cabang aktifnya saja lewat satu pool koneksi. Setiap kata “atau” dan “tetapi” itu adalah urusan objek POLICY, bukan urusan saklar RLS. Chapter ini membongkar anatomi policy: dua ruas penyaringnya, mode permissive dan restrictive untuk penggabungan beberapa policy, subquery untuk struktur organisasi, parameter sesi untuk pola tenant, sampai cara mendebugnya.

Bahan Latihan: Tim Kasir dan Tabel Transaksi #

File pembuka menyiapkan empat role dan tiga tabel untuk sepanjang chapter. Dua role kasir — budi_tmj dan siti_tmj — menjadi pelaku utama, manajer_tmj menyelia keduanya, dan petugas_tmj kelak membaca tabel cabang. Tabel karyawan mencatat siapa menyelia siapa sebagai peta organisasi. Tabel transaksi berisi lima baris dengan penanda publik dan rahasia yang sengaja saling tumpang tindih supaya efek penggabungan policy terlihat baris demi baris. Tabel cabang menampung dua kode cabang untuk pola tenant:

-- Bahan latihan Policies: tim kasir dengan manajer, tabel transaksi
-- berkolom penanda, dan tabel cabang untuk pola tenant.
DROP ROLE IF EXISTS budi_tmj;
DROP ROLE IF EXISTS siti_tmj;
DROP ROLE IF EXISTS manajer_tmj;
DROP ROLE IF EXISTS petugas_tmj;

CREATE ROLE budi_tmj LOGIN;
CREATE ROLE siti_tmj LOGIN;
CREATE ROLE manajer_tmj LOGIN;
CREATE ROLE petugas_tmj LOGIN;

-- Peta organisasi: siapa menyelia siapa.
CREATE TABLE karyawan (
    nama    text PRIMARY KEY,
    penyelia text NOT NULL
);

INSERT INTO karyawan (nama, penyelia) VALUES
    ('budi_tmj', 'manajer_tmj'),
    ('siti_tmj', 'manajer_tmj');

-- Tabel utama: baris bisa publik, bisa rahasia.
CREATE TABLE transaksi (
    id      integer PRIMARY KEY,
    kasir   text NOT NULL,
    nilai   numeric(10,2) NOT NULL,
    publik  boolean NOT NULL DEFAULT false,
    rahasia boolean NOT NULL DEFAULT false
);

INSERT INTO transaksi (id, kasir, nilai, publik, rahasia) VALUES
    (1, 'budi_tmj', 85000.00, true,  false),
    (2, 'budi_tmj', 35000.00, false, false),
    (3, 'siti_tmj', 40000.00, true,  false),
    (4, 'siti_tmj', 15000.00, false, true),
    (5, 'budi_tmj', 99000.00, true,  true);

-- Tabel untuk pola tenant: data per kode cabang.
CREATE TABLE cabang (
    id           integer PRIMARY KEY,
    kode_cabang  text NOT NULL,
    keterangan   text NOT NULL
);

INSERT INTO cabang (id, kode_cabang, keterangan) VALUES
    (1, 'jkt', 'penjualan jakarta'),
    (2, 'jkt', 'retur jakarta'),
    (3, 'bdg', 'penjualan bandung');

GRANT SELECT, UPDATE ON transaksi TO budi_tmj, siti_tmj;
GRANT SELECT ON transaksi TO manajer_tmj;
GRANT SELECT ON karyawan TO manajer_tmj;
GRANT SELECT ON cabang TO petugas_tmj;

-- RLS dinyalakan pada tabel utama; policy dibuat bertahap
-- di file-file berikutnya.
ALTER TABLE transaksi ENABLE ROW LEVEL SECURITY;

Output yang Dihasilkan:

text
DROP ROLE
DROP ROLE
DROP ROLE
DROP ROLE
CREATE ROLE
CREATE ROLE
CREATE ROLE
CREATE ROLE
CREATE TABLE
INSERT 0 2
CREATE TABLE
INSERT 0 5
CREATE TABLE
INSERT 0 3
GRANT
GRANT
GRANT
GRANT
ALTER TABLE

GRANT tetap dibutuhkan karena RLS tidak menggantikan privilege, melainkan menyaring di bawahnya — tanpa GRANT SELECT, role tidak pernah sampai ke tahap penyaringan. Sementara itu, RLS pada transaksi sudah menyala tanpa satu policy pun: bagi kedua kasir, SELECT atas transaksi saat ini menghasilkan nol baris — lubang hitam dari chapter sebelumnya. Sepanjang demo kita menguji sebagai role lain lewat SET ROLE; server latihan memakai trust authentication sehingga seluruh percobaan berjalan murni di level SQL tanpa sandi.

USING dan WITH CHECK: Dua Ruas dengan Dua Tugas #

Mari kita petakan dulu dua ruas penyaring yang menjadi jantung setiap policy. USING memeriksa baris lama — baris yang sudah ada dan sedang dibaca atau dijadikan sasaran perubahan. WITH CHECK memeriksa baris baru — keadaan yang hendak masuk sebagai hasil INSERT atau UPDATE. Ringkasnya: SELECT hanya melihat baris yang lolos USING; INSERT dan UPDATE hanya boleh menghasilkan baris yang lolos WITH CHECK.

AspekUSINGWITH CHECK
Memeriksabaris lama yang sudah ada di tabelbaris baru hasil INSERT/UPDATE
Dipakai oleh policy untukSELECT, UPDATE, DELETEINSERT, UPDATE
Bila baris gagalbaris tidak terlihat — dianggap tidak pernah adaseluruh perintah dibatalkan dengan ERROR
Bila ruas tidak ditulispolicy INSERT tidak memakainyapolicy UPDATE dan ALL memakai ekspresi USING ulang

Dua perilaku pada dua baris terakhir tabel itu paling sering mengejutkan pemula: kegagalan USING tidak memunculkan error — barisnya sekadar menghilang dari hasil query — sedangkan kegagalan WITH CHECK menghentikan perintah dengan ERROR.

CREATE POLICY juga mengenal tiga pengaturan lain. Klausa AS PERMISSIVE | AS RESTRICTIVE menentukan cara policy digabungkan dengan policy lain — PERMISSIVE adalah default. Klausa FOR SELECT | INSERT | UPDATE | DELETE | ALL membatasi perintah yang tersentuh — defaultnya ALL. Klausa TO mendaftar role penerima; tanpa klausa ini policy berlaku untuk PUBLIC, artinya seluruh role.

Policy Permissive: Banyak Aturan Digabung dengan OR #

Policy permissive bersifat longgar secara sengaja: untuk kombinasi tabel, perintah, dan role yang sama, seluruh policy permissive digabung dengan operator OR — baris cukup lolos satu policy untuk terlihat. Demo pertama memanfaatkan hak itu: kasir melihat baris miliknya sendiri atau baris publik, lewat dua policy terpisah yang keduanya permissive:

-- Dua policy PERMISSIVE untuk operasi SELECT digabungkan dengan OR:
-- kasir melihat baris miliknya SENDIRI atau baris berstatus publik.
CREATE POLICY transaksi_milik ON transaksi
    FOR SELECT
    TO budi_tmj, siti_tmj
    USING (kasir = current_user);

CREATE POLICY transaksi_publik ON transaksi
    FOR SELECT
    TO budi_tmj, siti_tmj
    USING (publik);

SET ROLE budi_tmj;

-- Baris milik budi: 1, 2, 5. Baris publik: 1, 3, 5.
-- Gabungan OR: 1, 2, 3, 5.
SELECT id, kasir, nilai, publik
FROM transaksi
ORDER BY id;

RESET ROLE;

Output yang Dihasilkan:

text
CREATE POLICY
CREATE POLICY
SET
 id |  kasir   |  nilai   | publik 
----+----------+----------+--------
  1 | budi_tmj | 85000.00 | t
  2 | budi_tmj | 35000.00 | f
  3 | siti_tmj | 40000.00 | t
  5 | budi_tmj | 99000.00 | t
(4 rows)

RESET

Mari kita bedah hasilnya per id dari sudut pandang budi_tmj:

  1. Baris 3-6 (transaksi_milik): USING (kasir = current_user) meloloskan baris milik budi — id 1, 2, dan 5.
  2. Baris 8-11 (transaksi_publik): USING (publik) meloloskan baris publik — id 1, 3, dan 5, termasuk id 3 milik siti_tmj.
  3. Baris 13-19: gabungan OR menjumlahkan kedua himpunan menjadi 1, 2, 3, 5 — persis empat baris pada output; id 4 tidak muncul karena bukan milik budi dan tidak publik.

Policy Restrictive: Pemangkas dengan AND #

Kata “tetapi” pada aturan kita — kasir tidak boleh melihat baris rahasia — tidak bisa ditulis sebagai policy permissive, karena policy permissive hanya menambah izin. Untuk pemangkasan, PostgreSQL menyediakan AS RESTRICTIVE: policy restrictive diikat operator AND di atas hasil gabungan permissive, sehingga baris harus lolos semua policy restrictive sekaligus:

-- Policy RESTRICTIVE tidak menambah akses; ia hanya MEMANGKAS
-- hasil gabungan policy permissive dengan operator AND.
CREATE POLICY transaksi_rahasia ON transaksi
    AS RESTRICTIVE
    FOR SELECT
    TO budi_tmj, siti_tmj
    USING (NOT rahasia);

SET ROLE budi_tmj;

-- Sebelumnya budi melihat 1, 2, 3, 5.
-- Baris rahasia (id 5) kini dipangkas oleh AND (NOT rahasia).
SELECT id, kasir, nilai, publik, rahasia
FROM transaksi
ORDER BY id;

RESET ROLE;

Output yang Dihasilkan:

text
CREATE POLICY
SET
 id |  kasir   |  nilai   | publik | rahasia 
----+----------+----------+--------+---------
  1 | budi_tmj | 85000.00 | t      | f
  2 | budi_tmj | 35000.00 | f      | f
  3 | siti_tmj | 40000.00 | t      | f
(3 rows)

RESET

Sebelum policy ini dipasang, budi melihat id 1, 2, 3, 5. Setelahnya, id 5 lenyap: baris itu milik budi dan publik sehingga lolos kedua policy permissive, tetapi NOT rahasia bernilai salah sehingga policy restrictive memangkasnya. Tiga baris tersisa — dan pemangkasan ini pun senyap: perintah tetap sukses, hanya jumlah barisnya yang menyusut.

Ekspresi Subquery: Manajer Melihat Timnya #

Ekspresi policy adalah ekspresi SQL biasa, jadi subquery boleh masuk ke dalamnya. Pola ini penting untuk struktur organisasi: manajer tidak memiliki satu baris transaksi pun, tetapi berhak mengawasi transaksi kasir yang ia supervisi. Daftar bawahan hidup di tabel karyawan dan policy cukup membacanya lewat subquery:

-- Ekspresi policy boleh memakai subquery: manajer melihat
-- seluruh baris transaksi anggota tim yang ia supervisi.
CREATE POLICY transaksi_tim ON transaksi
    FOR SELECT
    TO manajer_tmj
    USING (kasir IN (SELECT nama
                     FROM karyawan
                     WHERE penyelia = current_user));

SET ROLE manajer_tmj;

-- Seluruh lima baris milik budi dan siti terlihat manajer:
-- keduanya tercatat sebagai bawahannya di tabel karyawan.
SELECT id, kasir, nilai
FROM transaksi
ORDER BY id;

RESET ROLE;

Output yang Dihasilkan:

text
CREATE POLICY
SET
 id |  kasir   |  nilai   
----+----------+----------
  1 | budi_tmj | 85000.00
  2 | budi_tmj | 35000.00
  3 | siti_tmj | 40000.00
  4 | siti_tmj | 15000.00
  5 | budi_tmj | 99000.00
(5 rows)

RESET

Dari balik SET ROLE manajer_tmj, subquery mengembalikan budi_tmj dan siti_tmj, sehingga kelima baris lolos operator IN. Fakta pentingnya: ekspresi policy dijalankan dengan hak akses role yang menjalankan query — karena itu manajer membutuhkan GRANT SELECT pada karyawan dari file pembuka; tanpa grant itu, pembacaan transaksi oleh manajer berhenti dengan permission denied for table karyawan. Selain itu, klausa TO membatasi cakupan: bagi manajer satu-satunya policy yang berlaku adalah transaksi_tim, sehingga baris rahasia id 4 dan 5 tetap terlihat tanpa dipangkas.

UPDATE: USING Menyeleksi, WITH CHECK Memvalidasi #

Operasi UPDATE melibatkan baris lama dan baris baru sekaligus, sehingga policy untuk UPDATE boleh memuat dua ruas: USING menyeleksi baris yang boleh diubah, WITH CHECK memvalidasi baris penggantinya. File berikut memakai ekspresi yang sama untuk keduanya — kasir hanya boleh mengubah barisnya sendiri, dan hasilnya tetap tercatat atas nama si kasir:

-- Policy untuk UPDATE memakai dua ruas sekaligus:
-- USING menyaring baris lama yang boleh diubah,
-- WITH CHECK memvalidasi keadaan baris baru.
CREATE POLICY transaksi_ubah ON transaksi
    FOR UPDATE
    TO budi_tmj, siti_tmj
    USING (kasir = current_user)
    WITH CHECK (kasir = current_user);

-- Budi mengubah nilai transaksi miliknya sendiri.
SET ROLE budi_tmj;

UPDATE transaksi SET nilai = 87000.00 WHERE id = 2;

SELECT id, kasir, nilai
FROM transaksi
WHERE id = 2;

RESET ROLE;

-- Jebakan "UPDATE 0": siti mencoba mengubah baris milik budi.
-- Baris itu tidak lolos USING sehingga tidak pernah terlihat --
-- perintah sah, tidak ada error, tetapi nol baris berubah.
SET ROLE siti_tmj;

UPDATE transaksi SET nilai = 99000.00 WHERE id = 1;

RESET ROLE;

Output yang Dihasilkan:

text
CREATE POLICY
SET
UPDATE 1
 id |  kasir   |  nilai   
----+----------+----------
  2 | budi_tmj | 87000.00
(1 row)

RESET
SET
UPDATE 0
RESET

Mari kita bedah dua percobaan pada file itu:

  1. Baris 4-8 (transaksi_ubah): policy FOR UPDATE dengan dua ruas — baris lama harus memenuhi USING, baris penggantinya harus memenuhi WITH CHECK dengan ekspresi yang sama.
  2. Baris 11-13: budi mengubah id 2 miliknya sendiri. Kedua ruas lolos karena kolom kasir tetap budi_tmj; hasilnya UPDATE 1 dan nilai berubah menjadi 87000.00, terbukti pada SELECT berikutnya.
  3. Baris 24-26: siti mengubah id 1 — milik budi. Baris itu tidak lolos USING, sehingga bagi perintah UPDATE baris tersebut tidak pernah ada: hasilnya UPDATE 0 tanpa pesan error.

Jebakan “query sukses tetapi nol baris berubah” yang dijanjikan di penutup chapter Row Level Security persis seperti percobaan ketiga: pengirim query tidak bisa membedakan “baris tidak ada” dari “baris tidak boleh saya sentuh”. Perlakukan UPDATE 0 pada tabel ber-RLS sebagai sinyal yang perlu diperiksa, bukan keberhasilan.

WITH CHECK Menolak Baris Baru #

Kegagalan WITH CHECK bersuara jauh lebih keras. Budi mencoba memindahkan kepemilikan barisnya sendiri kepada siti lewat UPDATE:

-- WITH CHECK sebagai penjaga terakhir: budi mencoba memindahkan
-- kepemilikan barisnya kepada siti lewat UPDATE.
SET ROLE budi_tmj;

UPDATE transaksi SET kasir = 'siti_tmj' WHERE id = 2;

Output yang Dihasilkan:

text
psql:update-ditolak.sql:5: ERROR:  new row violates row-level security policy for table "transaksi"

Baris lama id 2 lolos USING karena memang milik budi, tetapi baris barunya berisi kasir = 'siti_tmj' dan gagal pada WITH CHECK. PostgreSQL membatalkan seluruh perintah dengan ERROR: new row violates row-level security policy for table "transaksi" — kolom kasir di tabel tidak berubah sama sekali. Kontras kedua jenis kegagalan layak dihafal: USING gagal diam-diam (baris menghilang, UPDATE 0), WITH CHECK gagal eksplisit (error, perintah dibatalkan).

Pola Tenant: current_setting() sebagai Konteks Aplikasi #

Aplikasi multi-cabang biasanya memakai satu role database untuk seluruh pool koneksi, sehingga identitas cabang tidak bisa diandalkan dari current_user. Pola lazim untuk kebutuhan ini: parameter sesi kustom — aplikasi mengirim SET app.cabang = 'jkt' setelah koneksi dibuka, lalu policy membandingkan kolom tabel dengan nilai parameter itu lewat fungsi current_setting():

-- Pola tenant: satu tabel multi-cabang, cabang aktif ditentukan
-- lewat parameter sesi kustom app.cabang.
ALTER TABLE cabang ENABLE ROW LEVEL SECURITY;

-- current_setting dengan argumen true mengembalikan NULL
-- bila parameter belum di-set -- bukan error.
CREATE POLICY cabang_aktif ON cabang
    FOR SELECT
    TO petugas_tmj
    USING (kode_cabang = current_setting('app.cabang', true));

SET ROLE petugas_tmj;

-- Tanpa setelan: perbandingan dengan NULL bernilai tidak benar,
-- sehingga tidak ada baris yang lolos -- default deny.
SELECT count(*) AS tanpa_setelan FROM cabang;

-- Setelan sesi menentukan cabang mana yang terlihat.
SET app.cabang = 'jkt';

SELECT id, kode_cabang, keterangan
FROM cabang
ORDER BY id;

SET app.cabang = 'bdg';

SELECT id, kode_cabang, keterangan
FROM cabang
ORDER BY id;

RESET ROLE;

-- Parameter kustom dikembalikan ke kondisi kosong.
RESET app.cabang;

Output yang Dihasilkan:

text
ALTER TABLE
CREATE POLICY
SET
 tanpa_setelan 
---------------
             0
(1 row)

SET
 id | kode_cabang |    keterangan     
----+-------------+-------------------
  1 | jkt         | penjualan jakarta
  2 | jkt         | retur jakarta
(2 rows)

SET
 id | kode_cabang |    keterangan     
----+-------------+-------------------
  3 | bdg         | penjualan bandung
(1 row)

RESET
RESET

Mari kita bedah perilaku policy cabang_aktif:

  1. Baris 7-10: current_setting('app.cabang', true) membaca parameter sesi kustom. Argumen kedua bernilai true membuat fungsi mengembalikan NULL — bukan error — ketika parameternya belum pernah di-set.
  2. Baris 12-16: tanpa setelan, perbandingan kode_cabang = NULL bernilai tidak benar untuk semua baris, sehingga petugas_tmj melihat nol baris — default deny bekerja dengan sendirinya.
  3. Baris 19-29: SET app.cabang = 'jkt' memunculkan dua baris jakarta, lalu SET app.cabang = 'bdg' menggantinya dengan satu baris bandung. Query yang dikirim aplikasi tidak berubah sama sekali — hanya konteks sesi yang berganti.

Pola Umum dan Catatan Performa #

Lima policy pada chapter ini ditambah dua dari chapter sebelumnya membentuk gudang pola yang bisa dirakit sesuai kebutuhan:

PolaBentuk khasContoh di course ini
Baris milik penggunaFOR SELECT USING (pemilik = current_user)lihat_milik (chapter Row Level Security)
Baca bersama, tulis milikTO PUBLIC USING (publik) untuk baca, WITH CHECK (pemilik = current_user) untuk tulistransaksi_publik dan tulis_milik
Isolasi tenantUSING (kode_cabang = current_setting('app.cabang', true))cabang_aktif
Manajer melihat timUSING (kasir IN (SELECT ... FROM karyawan ...))transaksi_tim
Pengecualian pemangkasAS RESTRICTIVE USING (NOT rahasia)transaksi_rahasia

Soal performa: ekspresi policy dievaluasi per baris dan menjadi bagian rencana eksekusi query — kita akan melihatnya sebagai Filter pada EXPLAIN di bagian berikutnya. Konsekuensinya, kolom yang dirujuk ekspresi policy selayaknya kolom klausa WHERE: beri index agar planner bisa memakai index scan alih-alih menyapu seluruh tabel. Ekspresi subquery seperti transaksi_tim menuntut kehati-hatian lebih karena dievaluasi berulang untuk tiap baris — pada tabel besar, ukur dulu dampaknya.

Mendebug Policy: pg_policies, \dp, dan EXPLAIN #

Ketika sebuah tabel sudah menampung lima policy, kita butuh cara membaca kembali seluruh definisinya. psql merangkumnya dengan cepat: \dp transaksi menambahkan kolom Policies pada daftar privilege tabel, dan \d+ transaksi mencantumkan bagian Policies berisi nama, mode, perintah, role penerima, serta ekspresi tiap policy. Untuk query yang bisa disaring dan diurutkan, catalog pg_policies adalah sumber resminya. File penutup membaca catalog itu, memperlihatkan ekspresi gabungan lewat EXPLAIN, lalu membersihkan seluruh role latihan:

-- Mendebug policy: baca seluruh definisinya dari pg_policies,
-- lalu lihat ekspresi gabungannya di rencana eksekusi.
SELECT policyname, permissive, roles, cmd, qual, with_check
FROM pg_policies
WHERE tablename = 'transaksi'
ORDER BY policyname;

-- EXPLAIN memperlihatkan policy sebagai Filter pada tabel:
-- permissive digabung OR, restrictive dikunci AND.
SET ROLE budi_tmj;

EXPLAIN (COSTS OFF) SELECT id FROM transaksi;

RESET ROLE;

-- Pembersihan tuntas seluruh role latihan chapter ini.
DROP OWNED BY budi_tmj, siti_tmj, manajer_tmj, petugas_tmj;

DROP ROLE budi_tmj;
DROP ROLE siti_tmj;
DROP ROLE manajer_tmj;
DROP ROLE petugas_tmj;

SELECT count(*) AS sisa_role_latihan
FROM pg_roles
WHERE rolname IN ('budi_tmj', 'siti_tmj', 'manajer_tmj', 'petugas_tmj');

Output yang Dihasilkan:

text
    policyname     | permissive  |        roles        |  cmd   |                     qual                     |       with_check       
-------------------+-------------+---------------------+--------+----------------------------------------------+------------------------
 transaksi_milik   | PERMISSIVE  | {budi_tmj,siti_tmj} | SELECT | (kasir = CURRENT_USER)                       | 
 transaksi_publik  | PERMISSIVE  | {budi_tmj,siti_tmj} | SELECT | publik                                       | 
 transaksi_rahasia | RESTRICTIVE | {budi_tmj,siti_tmj} | SELECT | (NOT rahasia)                                | 
 transaksi_tim     | PERMISSIVE  | {manajer_tmj}       | SELECT | (kasir IN ( SELECT karyawan.nama            +| 
                   |             |                     |        |    FROM karyawan                            +| 
                   |             |                     |        |   WHERE (karyawan.penyelia = CURRENT_USER))) | 
 transaksi_ubah    | PERMISSIVE  | {budi_tmj,siti_tmj} | UPDATE | (kasir = CURRENT_USER)                       | (kasir = CURRENT_USER)
(5 rows)

SET
                            QUERY PLAN                            
------------------------------------------------------------------
 Seq Scan on transaksi
   Filter: ((NOT rahasia) AND (publik OR (kasir = CURRENT_USER)))
(2 rows)

RESET
DROP OWNED
DROP ROLE
DROP ROLE
DROP ROLE
DROP ROLE
 sisa_role_latihan 
-------------------
                 0
(1 row)

Lima baris pertama output adalah isi pg_policies: kolom permissive membedakan PERMISSIVE dari RESTRICTIVE, roles berbentuk array nama role, cmd mencatat perintah yang disasar, qual menyimpan ekspresi USING, dan with_check hanya terisi pada transaksi_ubah — satu-satunya policy yang menulis klausa WITH CHECK. Berkat ORDER BY policyname, query ini deterministik dan cocok menjadi pemeriksaan otomatis di skrip migrasi.

Baris Filter pada EXPLAIN membuktikan aturan penggabungan sejak awal chapter dalam satu ekspresi: ((NOT rahasia) AND (publik OR (kasir = CURRENT_USER))). Dibaca dari luar ke dalam: policy restrictive NOT rahasia diikat AND di tingkat teratas, kedua policy permissive digabung OR di dalam kurung. Rencana ini diambil dari balik SET ROLE budi_tmj, jadi hanya policy yang menyebut budi yang tergabung — transaksi_tim milik manajer tidak menyentuhnya.

Anatomi policy kini lengkap — dua ruas penyaring, dua mode penggabungan, subquery, konteks sesi, dan alat baca-debug. Semuanya menjawab satu pertanyaan: baris mana yang boleh dilihat dan ditulis oleh role tertentu, dengan asumsi bahwa role yang tercatat di klausa TO memang benar identitasnya. Bagaimana sebuah koneksi membuktikan dirinya sebagai budi_tmj — lewat trust, sandi, atau scram-sha-256 — adalah urusan authentication, pembuka chapter berikutnya.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

debug-policy.sql

Unduh berkas
sql
-- Mendebug policy: baca seluruh definisinya dari pg_policies,
-- lalu lihat ekspresi gabungannya di rencana eksekusi.
SELECT policyname, permissive, roles, cmd, qual, with_check
FROM pg_policies
WHERE tablename = 'transaksi'
ORDER BY policyname;

-- EXPLAIN memperlihatkan policy sebagai Filter pada tabel:
-- permissive digabung OR, restrictive dikunci AND.
SET ROLE budi_tmj;

EXPLAIN (COSTS OFF) SELECT id FROM transaksi;

RESET ROLE;

-- Pembersihan tuntas seluruh role latihan chapter ini.
DROP OWNED BY budi_tmj, siti_tmj, manajer_tmj, petugas_tmj;

DROP ROLE budi_tmj;
DROP ROLE siti_tmj;
DROP ROLE manajer_tmj;
DROP ROLE petugas_tmj;

SELECT count(*) AS sisa_role_latihan
FROM pg_roles
WHERE rolname IN ('budi_tmj', 'siti_tmj', 'manajer_tmj', 'petugas_tmj');

policy-permissive.sql

Unduh berkas
sql
-- Dua policy PERMISSIVE untuk operasi SELECT digabungkan dengan OR:
-- kasir melihat baris miliknya SENDIRI atau baris berstatus publik.
CREATE POLICY transaksi_milik ON transaksi
    FOR SELECT
    TO budi_tmj, siti_tmj
    USING (kasir = current_user);

CREATE POLICY transaksi_publik ON transaksi
    FOR SELECT
    TO budi_tmj, siti_tmj
    USING (publik);

SET ROLE budi_tmj;

-- Baris milik budi: 1, 2, 5. Baris publik: 1, 3, 5.
-- Gabungan OR: 1, 2, 3, 5.
SELECT id, kasir, nilai, publik
FROM transaksi
ORDER BY id;

RESET ROLE;

policy-restrictive.sql

Unduh berkas
sql
-- Policy RESTRICTIVE tidak menambah akses; ia hanya MEMANGKAS
-- hasil gabungan policy permissive dengan operator AND.
CREATE POLICY transaksi_rahasia ON transaksi
    AS RESTRICTIVE
    FOR SELECT
    TO budi_tmj, siti_tmj
    USING (NOT rahasia);

SET ROLE budi_tmj;

-- Sebelumnya budi melihat 1, 2, 3, 5.
-- Baris rahasia (id 5) kini dipangkas oleh AND (NOT rahasia).
SELECT id, kasir, nilai, publik, rahasia
FROM transaksi
ORDER BY id;

RESET ROLE;

policy-subquery.sql

Unduh berkas
sql
-- Ekspresi policy boleh memakai subquery: manajer melihat
-- seluruh baris transaksi anggota tim yang ia supervisi.
CREATE POLICY transaksi_tim ON transaksi
    FOR SELECT
    TO manajer_tmj
    USING (kasir IN (SELECT nama
                     FROM karyawan
                     WHERE penyelia = current_user));

SET ROLE manajer_tmj;

-- Seluruh lima baris milik budi dan siti terlihat manajer:
-- keduanya tercatat sebagai bawahannya di tabel karyawan.
SELECT id, kasir, nilai
FROM transaksi
ORDER BY id;

RESET ROLE;

policy-tenant.sql

Unduh berkas
sql
-- Pola tenant: satu tabel multi-cabang, cabang aktif ditentukan
-- lewat parameter sesi kustom app.cabang.
ALTER TABLE cabang ENABLE ROW LEVEL SECURITY;

-- current_setting dengan argumen true mengembalikan NULL
-- bila parameter belum di-set -- bukan error.
CREATE POLICY cabang_aktif ON cabang
    FOR SELECT
    TO petugas_tmj
    USING (kode_cabang = current_setting('app.cabang', true));

SET ROLE petugas_tmj;

-- Tanpa setelan: perbandingan dengan NULL bernilai tidak benar,
-- sehingga tidak ada baris yang lolos -- default deny.
SELECT count(*) AS tanpa_setelan FROM cabang;

-- Setelan sesi menentukan cabang mana yang terlihat.
SET app.cabang = 'jkt';

SELECT id, kode_cabang, keterangan
FROM cabang
ORDER BY id;

SET app.cabang = 'bdg';

SELECT id, kode_cabang, keterangan
FROM cabang
ORDER BY id;

RESET ROLE;

-- Parameter kustom dikembalikan ke kondisi kosong.
RESET app.cabang;

policy-update.sql

Unduh berkas
sql
-- Policy untuk UPDATE memakai dua ruas sekaligus:
-- USING menyaring baris lama yang boleh diubah,
-- WITH CHECK memvalidasi keadaan baris baru.
CREATE POLICY transaksi_ubah ON transaksi
    FOR UPDATE
    TO budi_tmj, siti_tmj
    USING (kasir = current_user)
    WITH CHECK (kasir = current_user);

-- Budi mengubah nilai transaksi miliknya sendiri.
SET ROLE budi_tmj;

UPDATE transaksi SET nilai = 87000.00 WHERE id = 2;

SELECT id, kasir, nilai
FROM transaksi
WHERE id = 2;

RESET ROLE;

-- Jebakan "UPDATE 0": siti mencoba mengubah baris milik budi.
-- Baris itu tidak lolos USING sehingga tidak pernah terlihat --
-- perintah sah, tidak ada error, tetapi nol baris berubah.
SET ROLE siti_tmj;

UPDATE transaksi SET nilai = 99000.00 WHERE id = 1;

RESET ROLE;

siapkan-policy.sql

Unduh berkas
sql
-- Bahan latihan Policies: tim kasir dengan manajer, tabel transaksi
-- berkolom penanda, dan tabel cabang untuk pola tenant.
DROP ROLE IF EXISTS budi_tmj;
DROP ROLE IF EXISTS siti_tmj;
DROP ROLE IF EXISTS manajer_tmj;
DROP ROLE IF EXISTS petugas_tmj;

CREATE ROLE budi_tmj LOGIN;
CREATE ROLE siti_tmj LOGIN;
CREATE ROLE manajer_tmj LOGIN;
CREATE ROLE petugas_tmj LOGIN;

-- Peta organisasi: siapa menyelia siapa.
CREATE TABLE karyawan (
    nama    text PRIMARY KEY,
    penyelia text NOT NULL
);

INSERT INTO karyawan (nama, penyelia) VALUES
    ('budi_tmj', 'manajer_tmj'),
    ('siti_tmj', 'manajer_tmj');

-- Tabel utama: baris bisa publik, bisa rahasia.
CREATE TABLE transaksi (
    id      integer PRIMARY KEY,
    kasir   text NOT NULL,
    nilai   numeric(10,2) NOT NULL,
    publik  boolean NOT NULL DEFAULT false,
    rahasia boolean NOT NULL DEFAULT false
);

INSERT INTO transaksi (id, kasir, nilai, publik, rahasia) VALUES
    (1, 'budi_tmj', 85000.00, true,  false),
    (2, 'budi_tmj', 35000.00, false, false),
    (3, 'siti_tmj', 40000.00, true,  false),
    (4, 'siti_tmj', 15000.00, false, true),
    (5, 'budi_tmj', 99000.00, true,  true);

-- Tabel untuk pola tenant: data per kode cabang.
CREATE TABLE cabang (
    id           integer PRIMARY KEY,
    kode_cabang  text NOT NULL,
    keterangan   text NOT NULL
);

INSERT INTO cabang (id, kode_cabang, keterangan) VALUES
    (1, 'jkt', 'penjualan jakarta'),
    (2, 'jkt', 'retur jakarta'),
    (3, 'bdg', 'penjualan bandung');

GRANT SELECT, UPDATE ON transaksi TO budi_tmj, siti_tmj;
GRANT SELECT ON transaksi TO manajer_tmj;
GRANT SELECT ON karyawan TO manajer_tmj;
GRANT SELECT ON cabang TO petugas_tmj;

-- RLS dinyalakan pada tabel utama; policy dibuat bertahap
-- di file-file berikutnya.
ALTER TABLE transaksi ENABLE ROW LEVEL SECURITY;

update-ditolak.sql

Unduh berkas
sql
-- WITH CHECK sebagai penjaga terakhir: budi mencoba memindahkan
-- kepemilikan barisnya kepada siti lewat UPDATE.
SET ROLE budi_tmj;

UPDATE transaksi SET kasir = 'siti_tmj' WHERE id = 2;