Sebelumnya kita memberi hak pada tabel, padahal tabel tidak berdiri sendiri: setiap tabel hidup di dalam sebuah schema, dan schema itu adalah gerbang yang harus dilewati lebih dulu. Pemeriksaan akses PostgreSQL bekerja berlapis — database, lalu schema, lalu objek — sehingga GRANT SELECT pada tabel tidak bernilai apa-apa bila role tidak boleh “masuk” ke schema-nya. Perubahan besar PostgreSQL 15 juga terjadi tepat di level ini: hak membuat objek di schema public dicabut dari PUBLIC, mematahkan kebiasaan lama yang menganggap schema bawaan sebagai halaman bebas tulis. Chapter ini menyelami kedua hak schema (USAGE dan CREATE), membangun pola schema bersama dengan ALTER DEFAULT PRIVILEGES agar tabel baru otomatis terbaca anggota tim, lalu menutup dengan catatan keamanan search_path yang sering diabaikan.
Schema sebagai Ruang Nama #
Fungsi pertama schema bersifat organisatoris: memisahkan objek ke ruang nama masing-masing sehingga dua tabel boleh bernama sama tanpa bertabrakan. File pembuka membuat schema stok dan katalog yang masing-masing memuat tabel barang dengan struktur berbeda:
-- Schema sebagai ruang nama: dua schema boleh memakai
-- nama tabel yang sama tanpa saling bertabrakan.
DROP ROLE IF EXISTS kasir_tmj;
CREATE ROLE kasir_tmj LOGIN;
CREATE SCHEMA stok;
CREATE SCHEMA katalog;
CREATE TABLE stok.barang (
id integer PRIMARY KEY,
nama text NOT NULL,
sisa integer NOT NULL
);
INSERT INTO stok.barang (id, nama, sisa) VALUES
(1, 'Kopi Gayo', 40);
CREATE TABLE katalog.barang (
id integer PRIMARY KEY,
nama text NOT NULL,
harga numeric(10,2) NOT NULL
);
INSERT INTO katalog.barang (id, nama, harga) VALUES
(1, 'Kopi Gayo', 85000.00);
-- Nama yang sama, dua schema berbeda: nama yang membedakan
-- hanyalah prefiks skema di depannya.
SELECT 'stok' AS schema, id, nama, sisa::text AS nilai
FROM stok.barang
UNION ALL
SELECT 'katalog', id, nama, harga::text
FROM katalog.barang
ORDER BY schema, id;
-- Tanpa prefiks skema, nama tabel dicari lewat search_path.
SHOW search_path;Output yang Dihasilkan:
DROP ROLE
CREATE ROLE
CREATE SCHEMA
CREATE SCHEMA
CREATE TABLE
INSERT 0 1
CREATE TABLE
INSERT 0 1
schema | id | nama | nilai
---------+----+-----------+----------
katalog | 1 | Kopi Gayo | 85000.00
stok | 1 | Kopi Gayo | 40
(2 rows)
search_path
-----------------
"$user", public
(1 row)Query UNION ALL membaca kedua tabel barang sekaligus dan menunjukkan dua baris yang identik di kolom nama namun berbeda maknanya — satu sisa stok, satu harga katalog. Yang membedakan keduanya hanyalah prefiks schema di depan nama tabel. Baris SHOW search_path menampilkan urutan pencarian nama tanpa prefiks: "$user" (schema yang namanya sama dengan role) lalu public; nama barang tanpa prefiks akan dicari di sana-situ, dan bila tidak ketemu, PostgreSQL menolaknya dengan relation does not exist.
USAGE: Gerbang yang Harus Dibuka Sebelum Tabel Terlihat #
Hak USAGE pada schema adalah izin memasuki ruang nama itu. Tanpanya, seluruh objek di dalamnya — tabel, view, sequence, function — efektif tidak ada bagi role tersebut, seberapa pun banyaknya GRANT yang diberikan pada objeknya. File berikut sengaja membangun kontradiksi itu lalu menabrakkannya:
-- Jebakan klasik: GRANT SELECT pada tabel TIDAK CUKUP.
-- Tanpa USAGE pada schema-nya, objek di dalamnya tetap tak terlihat.
GRANT SELECT ON stok.barang TO kasir_tmj;
-- Bukti kontradiksinya: privilege tabel dimiliki, hak schema tidak.
SELECT has_table_privilege('kasir_tmj', 'stok.barang', 'SELECT') AS hak_tabel,
has_schema_privilege('kasir_tmj', 'stok', 'USAGE') AS hak_schema;
SET ROLE kasir_tmj;
SELECT id, nama, sisa
FROM stok.barang
ORDER BY id;Output yang Dihasilkan:
psql:tanpa-usage.sql:13: ERROR: permission denied for schema stok
LINE 2: FROM stok.barang
^Baris pemeriksaan catalog memperlihatkan kontradiksinya secara eksplisit: has_table_privilege menjawab t (hak SELECT pada stok.barang sungguh dimiliki), tetapi has_schema_privilege menjawab f (hak masuk schema stok tidak ada). Hasilnya, SELECT dari balik SET ROLE kasir_tmj ditolak bukan dengan pesan permission denied for table, melainkan permission denied for schema stok — penolakan terjadi di gerbang sebelum pemeriksaan tabel sempat berjalan. Kesalahpahaman umum “sudah di-GRANT kok masih ditolak” hampir selalu berakar di sini.
Penawalnya satu baris:
-- Satu baris GRANT USAGE membuka gerbangnya: kini hak SELECT
-- pada tabel benar-benar bisa dipakai.
GRANT USAGE ON SCHEMA stok TO kasir_tmj;
SET ROLE kasir_tmj;
SELECT id, nama, sisa
FROM stok.barang
ORDER BY id;
RESET ROLE;Output yang Dihasilkan:
GRANT
SET
id | nama | sisa
----+-----------+------
1 | Kopi Gayo | 40
(1 row)
RESETGRANT USAGE ON SCHEMA stok TO kasir_tmj membuka gerbangnya, dan query SELECT yang sama persis kini berhasil mengembalikan satu baris. Tidak ada perubahan pada grant tabel — yang berubah hanyalah izin melintasi schema. Pola lengkap untuk memberi akses baca tabel bagi role eksternal selalu dua baris: USAGE pada schema, SELECT pada tabel.
CREATE pada Schema dan Perubahan PostgreSQL 15 #
Hak kedua pada schema adalah CREATE: izin membuat objek baru di dalamnya. Inilah hak yang mengalami perubahan besar sejak PostgreSQL 15. Selama belasan tahun sebelumnya, setiap role bisa membuat tabel di schema public karena hak itu diberikan kepada PUBLIC secara bawaan — desain yang ramah bagi pemula namun berbahaya bagi server multi-pengguna, karena siapa pun bisa menyusupkan objek palsu ke ruang nama bersama. Sejak PostgreSQL 15, hak CREATE pada public dicabut dari PUBLIC:
-- Sejak PostgreSQL 15, schema public tidak lagi memberikan hak CREATE
-- kepada PUBLIC. Role non-pemilik kini ditolak saat membuat tabel di sana.
SET ROLE kasir_tmj;
CREATE TABLE public.catatan_umum (id integer);Output yang Dihasilkan:
psql:public-tanpa-create.sql:5: ERROR: permission denied for schema public
LINE 1: CREATE TABLE public.catatan_umum (id integer);
^kasir_tmj — role login biasa tanpa grant schema — ditolak dengan permission denied for schema public saat mencoba membuat tabel di sana. Kita sudah melihat bukti katalognya di chapter Privileges
: ACL public kini hanya memuat =U/pg_database_owner untuk PUBLIC, tanpa huruf C.
Schema Bersama dan ALTER DEFAULT PRIVILEGES #
Tim kecil biasanya ingin satu schema kerja bersama: semua anggota boleh membuat objek, dan objek baru milik siapa pun langsung bisa dibaca rekan-rekannya. Kebutuhan pertama dipenuhi GRANT USAGE, CREATE ON SCHEMA; kebutuhan kedua — yang mustahil dipenuhi GRANT biasa karena tabelnya belum ada — ditangani ALTER DEFAULT PRIVILEGES, aturan yang diterapkan otomatis pada objek baru:
-- Pola schema bersama: USAGE + CREATE untuk anggota,
-- dan ALTER DEFAULT PRIVILEGES agar tabel baru otomatis terbaca.
CREATE SCHEMA bersama;
-- Hak pada schema: boleh masuk (USAGE) dan boleh membuat objek (CREATE).
GRANT USAGE, CREATE ON SCHEMA bersama TO kasir_tmj;
-- Aturan default untuk objek BARU yang dibuat postgres di schema ini:
-- SELECT otomatis diberikan kepada kasir_tmj tanpa GRANT manual.
ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA bersama
GRANT SELECT ON TABLES TO kasir_tmj;
-- kasir membuat tabel sendiri di schema bersama.
SET ROLE kasir_tmj;
CREATE TABLE bersama.catatan (
id integer PRIMARY KEY,
isi text NOT NULL
);
INSERT INTO bersama.catatan (id, isi) VALUES
(1, 'catatan dari kasir');
RESET ROLE;
-- postgres membuat tabel baru; default privileges langsung bekerja.
CREATE TABLE bersama.harga_pasar (
id integer PRIMARY KEY,
komoditas text NOT NULL,
harga numeric(10,2) NOT NULL
);
INSERT INTO bersama.harga_pasar (id, komoditas, harga) VALUES
(1, 'Beras', 13500.00);
SET ROLE kasir_tmj;
-- Tabel milik postgres bisa dibaca kasir tanpa satu pun GRANT manual:
-- hak SELECT-nya datang dari ALTER DEFAULT PRIVILEGES.
SELECT id, komoditas, harga
FROM bersama.harga_pasar
ORDER BY id;
-- Tabel buatan sendiri tentu saja bisa dibaca: kasir adalah pemiliknya.
SELECT id, isi
FROM bersama.catatan
ORDER BY id;
RESET ROLE;
-- Bukti dari catalog: grant SELECT atas nama kasir_tmj tertulis
-- pada tabel harga_pasar yang baru dibuat postgres.
SELECT grantee, privilege_type
FROM information_schema.role_table_grants
WHERE table_schema = 'bersama'
AND table_name = 'harga_pasar'
ORDER BY grantee, privilege_type;
-- ACL schema bersama: kasir memegang UC (USAGE + CREATE) dari postgres.
SELECT nspname, nspacl
FROM pg_namespace
WHERE nspname = 'bersama';Output yang Dihasilkan:
CREATE SCHEMA
GRANT
ALTER DEFAULT PRIVILEGES
SET
CREATE TABLE
INSERT 0 1
RESET
CREATE TABLE
INSERT 0 1
SET
id | komoditas | harga
----+-----------+----------
1 | Beras | 13500.00
(1 row)
id | isi
----+--------------------
1 | catatan dari kasir
(1 row)
RESET
grantee | privilege_type
-----------+----------------
kasir_tmj | SELECT
postgres | DELETE
postgres | INSERT
postgres | REFERENCES
postgres | SELECT
postgres | TRIGGER
postgres | TRUNCATE
postgres | UPDATE
(8 rows)
nspname | nspacl
---------+----------------------------------------------
bersama | {postgres=UC/postgres,kasir_tmj=UC/postgres}
(1 row)Mari kita bedah alurnya:
GRANT USAGE, CREATE ON SCHEMA bersama TO kasir_tmj— dua hak schema sekaligus: boleh masuk dan boleh membuat objek. ACL di baris terakhir buktinya:kasir_tmj=UC/postgres.ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA bersama GRANT SELECT ON TABLES TO kasir_tmj— aturan untuk semua tabel yang akan dibuat olehpostgresdi schemabersama: hakSELECTdiberikan otomatis kekasir_tmj. KlausaFOR ROLEmenentukan pemilik objek mana yang aturannya berlaku — aturan ini tidak menyentuh tabel buatan role lain.CREATE TABLE bersama.catatandari balikSET ROLE kasir_tmj— membuktikan hakCREATEbekerja; tabel itu dimiliki kasir.CREATE TABLE bersama.harga_pasardijalankanpostgres, lalu dibaca kasir tanpa satu punGRANTmanual — haknya datang dari aturan default. Catalogrole_table_grantsmengonfirmasi bariskasir_tmj | SELECTpada tabel itu.
Perhatikan kontras di dua SELECT terakhir file: harga_pasar terbaca karena default privileges dari pemiliknya, sedangkan catatan terbaca karena kepemilikan — kasir memang pemilik tabel buatannya sendiri. Dua jalan menuju hasil yang sama, dengan mekanisme yang sama sekali berbeda.
Catatan Keamanan search_path #
search_path adalah kenyamanan yang menyimpan jebakan. Query tanpa prefiks schema — SELECT * FROM barang — diselesaikan lewat schema pertama di search_path yang memuat objek bernama barang. Di tangan penyerang yang bisa membuat objek di schema awal pencarian, resolusi ini bisa diarahkan ke objek palsu: function jahat bernama upper() di schema yang lebih awal akan menang atas function bawaan di pg_catalog. Pertahanannya dua lapis:
- Kualifikasi penuh di kode aplikasi —
stok.barang,pg_catalog.upper()— mematikan resolusi ambigu sejak awal. SET search_pathyang eksplisit — misalnyaALTER ROLE aplikasi_kasir SET search_path = stok, pg_catalog— memastikan urutan pencarian hanya memuat schema yang Anda maksud, tanpa"$user"yang bisa berubah mengikuti nama role.
Kombinasi keduanya adalah standar untuk kode SECURITY DEFINER dan aplikasi produksi; kita akan bertemu lagi dengan tema ini saat membahas function keamanan di bagian stored programs.
Membersihkan Role Latihan #
Pembersihan akhir memakai pola biasa dengan satu tambahan yang perlu dicermati — DROP OWNED BY untuk kasir_tmj kini juga menjatuhkan tabel bersama.catatan karena kasir memilikinya:
-- Pembersihan akhir: DROP OWNED BY melepas hak kasir di database ini
-- sekaligus menjatuhkan tabel miliknya, lalu role-nya dibuang.
DROP OWNED BY kasir_tmj;
DROP ROLE kasir_tmj;
SELECT count(*) AS sisa_role_latihan
FROM pg_roles
WHERE rolname = 'kasir_tmj';Output yang Dihasilkan:
DROP OWNED
DROP ROLE
sisa_role_latihan
-------------------
0
(1 row)Setelah DROP OWNED BY, seluruh jejak kasir di database ini — hak schema, grant default, tabel miliknya — bersih sekaligus, dan DROP ROLE berjalan tanpa hambatan. Schema privileges kini terkuasai dari gerbang USAGE, izin CREATE, perubahan public PostgreSQL 15, sampai pola schema bersama. Namun semua yang kita bangun sejauh ini bekerja pada tingkat objek: yang boleh membaca tabel, boleh membaca seluruh tabel. Pertanyaan berikutnya berada di dalam tabel itu sendiri — baris mana yang boleh terlihat oleh siapa — dan itu wilayah Row Level Security
.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
beri-usage.sql
Unduh berkas-- Satu baris GRANT USAGE membuka gerbangnya: kini hak SELECT
-- pada tabel benar-benar bisa dipakai.
GRANT USAGE ON SCHEMA stok TO kasir_tmj;
SET ROLE kasir_tmj;
SELECT id, nama, sisa
FROM stok.barang
ORDER BY id;
RESET ROLE;
bersihkan.sql
Unduh berkas-- Pembersihan akhir: DROP OWNED BY melepas hak kasir di database ini
-- sekaligus menjatuhkan tabel miliknya, lalu role-nya dibuang.
DROP OWNED BY kasir_tmj;
DROP ROLE kasir_tmj;
SELECT count(*) AS sisa_role_latihan
FROM pg_roles
WHERE rolname = 'kasir_tmj';
buat-schema.sql
Unduh berkas-- Schema sebagai ruang nama: dua schema boleh memakai
-- nama tabel yang sama tanpa saling bertabrakan.
DROP ROLE IF EXISTS kasir_tmj;
CREATE ROLE kasir_tmj LOGIN;
CREATE SCHEMA stok;
CREATE SCHEMA katalog;
CREATE TABLE stok.barang (
id integer PRIMARY KEY,
nama text NOT NULL,
sisa integer NOT NULL
);
INSERT INTO stok.barang (id, nama, sisa) VALUES
(1, 'Kopi Gayo', 40);
CREATE TABLE katalog.barang (
id integer PRIMARY KEY,
nama text NOT NULL,
harga numeric(10,2) NOT NULL
);
INSERT INTO katalog.barang (id, nama, harga) VALUES
(1, 'Kopi Gayo', 85000.00);
-- Nama yang sama, dua schema berbeda: nama yang membedakan
-- hanyalah prefiks skema di depannya.
SELECT 'stok' AS schema, id, nama, sisa::text AS nilai
FROM stok.barang
UNION ALL
SELECT 'katalog', id, nama, harga::text
FROM katalog.barang
ORDER BY schema, id;
-- Tanpa prefiks skema, nama tabel dicari lewat search_path.
SHOW search_path;
public-tanpa-create.sql
Unduh berkas-- Sejak PostgreSQL 15, schema public tidak lagi memberikan hak CREATE
-- kepada PUBLIC. Role non-pemilik kini ditolak saat membuat tabel di sana.
SET ROLE kasir_tmj;
CREATE TABLE public.catatan_umum (id integer);
schema-bersama.sql
Unduh berkas-- Pola schema bersama: USAGE + CREATE untuk anggota,
-- dan ALTER DEFAULT PRIVILEGES agar tabel baru otomatis terbaca.
CREATE SCHEMA bersama;
-- Hak pada schema: boleh masuk (USAGE) dan boleh membuat objek (CREATE).
GRANT USAGE, CREATE ON SCHEMA bersama TO kasir_tmj;
-- Aturan default untuk objek BARU yang dibuat postgres di schema ini:
-- SELECT otomatis diberikan kepada kasir_tmj tanpa GRANT manual.
ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA bersama
GRANT SELECT ON TABLES TO kasir_tmj;
-- kasir membuat tabel sendiri di schema bersama.
SET ROLE kasir_tmj;
CREATE TABLE bersama.catatan (
id integer PRIMARY KEY,
isi text NOT NULL
);
INSERT INTO bersama.catatan (id, isi) VALUES
(1, 'catatan dari kasir');
RESET ROLE;
-- postgres membuat tabel baru; default privileges langsung bekerja.
CREATE TABLE bersama.harga_pasar (
id integer PRIMARY KEY,
komoditas text NOT NULL,
harga numeric(10,2) NOT NULL
);
INSERT INTO bersama.harga_pasar (id, komoditas, harga) VALUES
(1, 'Beras', 13500.00);
SET ROLE kasir_tmj;
-- Tabel milik postgres bisa dibaca kasir tanpa satu pun GRANT manual:
-- hak SELECT-nya datang dari ALTER DEFAULT PRIVILEGES.
SELECT id, komoditas, harga
FROM bersama.harga_pasar
ORDER BY id;
-- Tabel buatan sendiri tentu saja bisa dibaca: kasir adalah pemiliknya.
SELECT id, isi
FROM bersama.catatan
ORDER BY id;
RESET ROLE;
-- Bukti dari catalog: grant SELECT atas nama kasir_tmj tertulis
-- pada tabel harga_pasar yang baru dibuat postgres.
SELECT grantee, privilege_type
FROM information_schema.role_table_grants
WHERE table_schema = 'bersama'
AND table_name = 'harga_pasar'
ORDER BY grantee, privilege_type;
-- ACL schema bersama: kasir memegang UC (USAGE + CREATE) dari postgres.
SELECT nspname, nspacl
FROM pg_namespace
WHERE nspname = 'bersama';
tanpa-usage.sql
Unduh berkas-- Jebakan klasik: GRANT SELECT pada tabel TIDAK CUKUP.
-- Tanpa USAGE pada schema-nya, objek di dalamnya tetap tak terlihat.
GRANT SELECT ON stok.barang TO kasir_tmj;
-- Bukti kontradiksinya: privilege tabel dimiliki, hak schema tidak.
SELECT has_table_privilege('kasir_tmj', 'stok.barang', 'SELECT') AS hak_tabel,
has_schema_privilege('kasir_tmj', 'stok', 'USAGE') AS hak_schema;
SET ROLE kasir_tmj;
SELECT id, nama, sisa
FROM stok.barang
ORDER BY id;