Corak RLS Supabase untuk Membina SaaS Multi-Penyewa di Malaysia
Panduan praktikal tentang enam corak RLS Supabase untuk multi-penyewa yang kami guna di JRV Systems bagi melindungi data SaaS di Malaysia, dari JWT hingga ujian.
Kenapa RLS Penting untuk SaaS Multi-Penyewa
Dalam aplikasi Perisian-sebagai-Perkhidmatan (SaaS) berkonsepkan multi-penyewa, beberapa pelanggan (penyewa) menggunakan perisian yang sama, dengan data mereka disimpan dalam pangkalan data yang dikongsi. Bagi mana-mana perniagaan di Malaysia, dari sebuah klinik kecil yang menggunakan SaaS kami hinggalah ke syarikat besar yang menggunakan sistem bil khas, pengasingan data bukanlah sekadar satu ciri—ia adalah satu keperluan undang-undang dan komersial. Seorang penyewa tidak sepatutnya boleh melihat data penyewa lain.
Di sinilah Supabase dan implementasi Row Level Security (RLS) dari PostgreSQL menjadi sangat berkuasa. Daripada menulis logik akses data dalam kod aplikasi, kami menguatkuasakannya terus di dalam pangkalan data. Ini mewujudkan sempadan keselamatan yang kukuh dan konsisten. Di JRV Systems, kami telah melancarkan beberapa sistem multi-penyewa, dan ini adalah corak RLS Supabase untuk multi-penyewa yang menjadi teras kerja kami setiap hari.
Corak 1: Pengasingan Penyewa Melalui Tuntutan JWT
Ini adalah asas kepada hampir semua tetapan RLS multi-penyewa. Apabila pengguna log masuk, kami memasukkan pengecam organisasi mereka terus ke dalam JSON Web Token (JWT) mereka. Supabase memudahkannya dengan menggunakan objek app_metadata, yang tidak boleh disunting oleh klien.
Katakan kita ada jadual invoices. Setiap invois dimiliki oleh satu tenant_id.
Polisi RLS memastikan pengguna hanya boleh melihat invois yang sepadan dengan tenant_id dalam token mereka.
- Jenis Polisi:
SELECT,INSERT,UPDATE,DELETE - Ekspresi Polisi:
(auth.jwt() -> 'app_metadata' ->> 'tenant_id')::uuid = tenant_id
Satu baris ini adalah peraturan keselamatan yang paling kritikal. Untuk INSERT atau UPDATE, kami menggunakan ekspresi yang sama dalam klausa WITH CHECK untuk menghalang pengguna daripada secara tidak sengaja atau sengaja menulis data ke ruang penyewa lain. Ini adalah corak pertama yang kami laksanakan dalam mana-mana projek baharu, sama ada untuk e-dagang atau papan pemuka.
Corak 2: Pemilikan Baris oleh ID Pengguna
Dalam satu penyewa yang sama, anda sering memerlukan satu lagi lapisan pemilikan. Contohnya, seorang pengurus jualan boleh melihat semua tawaran untuk syarikat (Corak 1), tetapi seorang jurujual tertentu hanya boleh menyunting tawaran yang ditugaskan kepadanya. Ini adalah pemilikan pada peringkat baris.
Di sini, kita andaikan jadual (cth., deals) mempunyai lajur user_id yang menyimpan auth.uid() Supabase pemilik rekod tersebut.
- Jenis Polisi:
UPDATE - Ekspresi Polisi Gabungan:
((auth.jwt() -> 'app_metadata' ->> 'tenant_id')::uuid = tenant_id) AND (auth.uid() = user_id)
Polisi ini menggabungkan pengasingan penyewa dengan pemilikan pengguna. Seorang pengguna mesti tergolong dalam penyewa yang betul dan menjadi pemilik baris tersebut untuk boleh mengemas kininya. Untuk polisi SELECT, anda mungkin boleh melonggarkan syarat kedua untuk membenarkan pengurus melihat semua tawaran dalam penyewaan mereka.
Corak 3: Logik Graf-Peranan Melalui Fungsi Keselamatan
Apabila kebenaran menjadi lebih kompleks ('admin', 'pengurus', 'penonton'), menulis kesemuanya terus ke dalam polisi RLS menjadi serabut dan berulang. Corak yang lebih kemas adalah dengan merangkumkan logik ini di dalam fungsi PostgreSQL.
Bayangkan jadual memberships yang menghubungkan user_id, tenant_id, dan medan teks role. Kita boleh cipta satu fungsi untuk memeriksa peranan pengguna:
create or replace function is_tenant_member(check_tenant_id uuid, required_role text) returns boolean as $$
select exists (
select 1 from memberships
where tenant_id = check_tenant_id
and user_id = auth.uid()
and role = required_role
);
$$ language sql security definer;
Kini, polisi RLS untuk tindakan sensitif, seperti memadam invois, menjadi ringkas dan mudah dibaca:
- Jenis Polisi:
DELETEpada jadualinvoices - Ekspresi Polisi:
is_tenant_member(tenant_id, 'admin')
Beginilah cara kami menguruskan akses berperingkat dalam papan pemuka yang kami bina. Ia memastikan polisi sentiasa kemas dan memusatkan logik kebenaran di satu tempat.
Corak 4: Menyekat Rekod Padam-Separa
Dalam banyak aplikasi, terutamanya yang mengendalikan data kewangan atau perubatan di Malaysia, rekod tidak pernah benar-benar dipadam dari pangkalan data. Sebaliknya, ia "dipadam-separa" (soft-delete) dengan menetapkan cap masa pada deleted_at. RLS amat sesuai untuk menyembunyikan rekod ini daripada pertanyaan API biasa.
Untuk jadual seperti patients, polisi SELECT utamanya adalah:
- Jenis Polisi:
SELECT - Ekspresi Polisi:
((auth.jwt() -> 'app_metadata' ->> 'tenant_id')::uuid = tenant_id) AND (deleted_at is null)
Ini memastikan operasi harian hanya melihat rekod yang aktif. Pentadbir atau antara muka arkib khas mungkin menggunakan pertanyaan yang memintas RLS (menggunakan kunci service_role) atau memanggil fungsi security definer untuk melihat atau memulihkan rekod yang dipadam-separa, seterusnya mengekalkan sejarah data yang lengkap.
Corak 5: Jejak Audit Kekal
Apabila tindakan penting berlaku, seperti bil dibayar atau data pesakit dikemas kini, kami mencatatnya dalam jadual audit_log. Log ini mesti kalis-manipulasi. RLS boleh menguatkuasakan sifat kekal ini.
Kami menerapkan satu set polisi pada jadual audit_log:
- Polisi
INSERT: Mana-mana pengguna yang disahkan dalam penyewa boleh mencipta entri log.USING: ((auth.jwt() -> 'app_metadata' ->> 'tenant_id')::uuid = tenant_id) - Polisi
SELECT: Hanya pengguna dengan peranan 'admin' boleh melihat log audit.USING: is_tenant_member(tenant_id, 'admin') - Polisi
UPDATE: Tiada siapa boleh mengubah entri log.WITH CHECK: false - Polisi
DELETE: Tiada siapa boleh memadam entri log.USING: false
Gabungan ini memastikan bahawa sebaik sahaja rekod audit ditulis, ia tidak boleh diubah atau dialih keluar melalui API awam, memberikan tahap kepercayaan yang tinggi terhadap sejarah sistem.
Corak 6: Ujian Tempatan dengan Supabase CLI
Akhir sekali, corak yang paling penting bukanlah satu polisi, tetapi satu proses: ujian. Anda tidak boleh tersilap dalam hal keselamatan. Supabase CLI menggunakan pgTAP, sebuah rangka kerja ujian untuk PostgreSQL, bagi mengesahkan polisi RLS anda berfungsi seperti yang dijangkakan.
Dalam direktori supabase/tests anda, anda boleh mencipta fail ujian untuk mensimulasikan senario:
- Persediaan: Cipta dua penyewa dan seorang pengguna dalam setiap satu.
- Penyamaran: Gunakan
set roledanset request.jwt.claimsuntuk menjalankan pertanyaan sebagai pengguna tertentu. - Pengesahan: Periksa sama ada pengguna hanya boleh melihat data dari penyewanya sendiri.
Contoh ujian mungkin kelihatan seperti ini:
-- tests/rls/invoices_rls_test.sql
select plan(1);
-- Tukar kepada pengguna dari penyewa 1
set role authenticated;
select set_config('request.jwt.claims', '{"app_metadata": {"tenant_id": "..."}}', true);
-- Sahkan pengguna ini tidak boleh melihat invois dari penyewa 2
select is_empty(
'select id from invoices where tenant_id = ''<tenant_2_id>''',
'Pengguna dari penyewa 1 tidak boleh memilih invois dari penyewa 2.'
);
Menjalankan supabase test db akan melaksanakan semakan ini. Pengesahan automatik ini adalah sebahagian daripada aliran kerja kami yang tidak boleh dirunding di JRV Systems. Ia memberikan keyakinan yang kami perlukan untuk melancarkan aplikasi multi-penyewa yang selamat dan boleh dipercayai untuk klien kami.