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:
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 TABLEGRANT 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.
| Aspek | USING | WITH CHECK |
|---|---|---|
| Memeriksa | baris lama yang sudah ada di tabel | baris baru hasil INSERT/UPDATE |
| Dipakai oleh policy untuk | SELECT, UPDATE, DELETE | INSERT, UPDATE |
| Bila baris gagal | baris tidak terlihat — dianggap tidak pernah ada | seluruh perintah dibatalkan dengan ERROR |
| Bila ruas tidak ditulis | policy INSERT tidak memakainya | policy 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:
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)
RESETMari kita bedah hasilnya per id dari sudut pandang budi_tmj:
- Baris 3-6 (
transaksi_milik):USING (kasir = current_user)meloloskan baris milik budi — id 1, 2, dan 5. - Baris 8-11 (
transaksi_publik):USING (publik)meloloskan baris publik — id 1, 3, dan 5, termasuk id 3 miliksiti_tmj. - 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:
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)
RESETSebelum 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:
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)
RESETDari 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:
CREATE POLICY
SET
UPDATE 1
id | kasir | nilai
----+----------+----------
2 | budi_tmj | 87000.00
(1 row)
RESET
SET
UPDATE 0
RESETMari kita bedah dua percobaan pada file itu:
- Baris 4-8 (
transaksi_ubah): policyFOR UPDATEdengan dua ruas — baris lama harus memenuhiUSING, baris penggantinya harus memenuhiWITH CHECKdengan ekspresi yang sama. - Baris 11-13: budi mengubah id 2 miliknya sendiri. Kedua ruas lolos karena kolom
kasirtetapbudi_tmj; hasilnyaUPDATE 1dan nilai berubah menjadi87000.00, terbukti padaSELECTberikutnya. - Baris 24-26: siti mengubah id 1 — milik budi. Baris itu tidak lolos
USING, sehingga bagi perintahUPDATEbaris tersebut tidak pernah ada: hasilnyaUPDATE 0tanpa 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:
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:
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
RESETMari kita bedah perilaku policy cabang_aktif:
- Baris 7-10:
current_setting('app.cabang', true)membaca parameter sesi kustom. Argumen kedua bernilaitruemembuat fungsi mengembalikanNULL— bukan error — ketika parameternya belum pernah di-set. - Baris 12-16: tanpa setelan, perbandingan
kode_cabang = NULLbernilai tidak benar untuk semua baris, sehinggapetugas_tmjmelihat nol baris — default deny bekerja dengan sendirinya. - Baris 19-29:
SET app.cabang = 'jkt'memunculkan dua baris jakarta, laluSET 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:
| Pola | Bentuk khas | Contoh di course ini |
|---|---|---|
| Baris milik pengguna | FOR SELECT USING (pemilik = current_user) | lihat_milik (chapter Row Level Security) |
| Baca bersama, tulis milik | TO PUBLIC USING (publik) untuk baca, WITH CHECK (pemilik = current_user) untuk tulis | transaksi_publik dan tulis_milik |
| Isolasi tenant | USING (kode_cabang = current_setting('app.cabang', true)) | cabang_aktif |
| Manajer melihat tim | USING (kasir IN (SELECT ... FROM karyawan ...)) | transaksi_tim |
| Pengecualian pemangkas | AS 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:
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-- 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-- 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-- 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-- 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-- 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-- 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-- 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-- 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;