SaaS में PostgreSQL row-level security टेनेंट अलगाव लागू करने में मदद करता है। जानें कब इस्तेमाल करें, नीतियाँ कैसे लिखें, और किन बातों से बचें।

SaaS ऐप में सबसे ख़तरनाक सुरक्षा बग वह होता है जो स्केल होने के बाद दिखता है। आप एक सरल नियम से शुरू करते हैं जैसे “यूज़र केवल अपने टेनेंट का डेटा देख सकता है,” फिर आप जल्दी नया एंडपॉइंट भेजते हैं, कोई रिपोर्टिंग क्वेरी जोड़ते हैं, या एक जॉइन आ जाता है जो चेक को चुपचाप स्किप कर देता है।
केवल ऐप-आधारित प्राधिकरण दबाव में टूट जाता है क्योंकि नियम बिखर जाते हैं। एक कंट्रोलर tenant_id चेक करता है, दूसरा मेंबरशिप, एक बैकग्राउंड जॉब भूल जाता है, और एक “admin export” पाथ महीनों तक "अस्थायी" रहता है। यहां तक कि सावधान टीमें भी कोई जगह मिस कर देती हैं।
PostgreSQL row-level security (RLS) एक विशिष्ट समस्या को हल करता है: यह डेटाबेस को यह लागू करवाता है कि किसी अनुरोध के लिए कौन सी पंक्तियाँ दिखाई देंगी। मानसिक मॉडल सरल है: हर SELECT, UPDATE, और DELETE नीतियों द्वारा ऑटोमैटिकली फ़िल्टर होते हैं, जैसे हर अनुरोध ऑथेंटिकेशन मिडलवेयर द्वारा फ़िल्टर होता है।
"पंक्तियाँ" वाली बात मायने रखती है। RLS हर चीज़ की रक्षा नहीं करता:\n\n- यह अपने आप किसी कॉलम को छुपाता नहीं है (ऐसे मामलों के लिए व्यूज़ या कॉलम प्रिविलेज का उपयोग करें)।\n- यह असुरक्षित फ़ंक्शन्स को सुरक्षित नहीं बनाता (यदि कोई फ़ंक्शन ऊँचे अधिकारों के साथ चलता है तो वह डेटा लीक कर सकता है)।\n- यह बिज़नेस नियमों को मान्य नहीं करता (उदाहरण के लिए, “केवल मालिक बिलिंग सेटिंग्स बदल सकते हैं”)।
एक ठोस उदाहरण: आप एक एंडपॉइंट जोड़ते हैं जो डैशबोर्ड के लिए प्रोजेक्ट्स को इनवॉइसेज़ के साथ जॉइन करके सूचीबद्ध करता है। केवल ऐप ऑथ के साथ, projects को टेनेंट से फ़िल्टर करना आसान है लेकिन invoices को फ़िल्टर करना भूल जाना या ऐसे की पर जॉइन करना जो टेनेंट क्रॉस कर दे, आसान है। RLS के साथ, दोनों तालिकाएँ टेनेंट अलगाव लागू कर सकती हैं, इसलिए क्वेरी सुरक्षित विफल हो जाती है बजाए डेटा लीक होने के।
यहाँ का ट्रेड-ऑफ़ असली है। आप दोहराए गए ऑथोराइज़ेशन कोड को कम लिखते हैं और लीक की संभावनाएँ घटती हैं। पर आपको नया काम भी लेना पड़ता है: नीतियाँ सावधानी से डिज़ाइन करनी पड़ती हैं, उन्हें जल्दी टेस्ट करना होता है, और मानना पड़ता है कि कोई नीति उस क्वेरी को ब्लॉक कर सकती है जिसकी आप उम्मीद कर रहे थे कि वह चलेगी।
RLS तब तक अतिरिक्त काम जैसा लग सकता है जब तक आपकी ऐप कुछ ही एंडपॉइंट्स से आगे न बढ़ जाए। अगर आपकी टेनेंट सीमाएँ कड़ियाँ हैं और बहुत सारे क्वेरी पाथ हैं (लिस्ट स्क्रीन, सर्च, एक्सपोर्ट, एडमिन टूल), तो नियम को डेटाबेस में रखना मतलब है कि आपको हर जगह वही फ़िल्टर याद रखने की ज़रूरत नहीं पड़ेगी।
RLS तब फिट बैठता है जब नियम नीरस और सार्वभौमिक हों: “यूज़र केवल अपनी टेनेंट की पंक्तियाँ देख सकता है” या “यूज़र केवल उन प्रोजेक्ट्स को देख सकता है जिसके वह मेंबर हैं।” ऐसे सेटअप में, नीतियाँ गलतियों को कम करती हैं क्योंकि हर SELECT, UPDATE, और DELETE एक ही गेट से गुजरता है, यहां तक कि जब बाद में कोई क्वेरी जोड़ी जाती है।
यह पढ़ने-भारी ऐप्स में भी मदद करता है जहाँ फ़िल्टरिंग लॉजिक स्थिर रहती है। अगर आपकी API में इनवॉइसेज़ लोड करने के 15 अलग तरीके हैं (स्टेटस, तिथि, ग्राहक, सर्च), तो RLS आपको हर क्वेरी पर टेनेंट फ़िल्टर दोहराने से रोकता है और फीचर पर फोकस करने देता है।
RLS तब दर्द बढ़ाता है जब नियम पंक्ति-आधारित नहीं होते। फ़ील्ड-स्तर के नियम जैसे “आप वेतन देख सकते हैं पर बोनस नहीं” या “इस कॉलम को HR के अलावा मास्क करो” अक्सर अजीब SQL और मेंटेन करने में कठिन अपवाद बन जाते हैं।
यह व्यापक रिपोर्टिंग के लिए भी खराब फ़िट हो सकता है जिसे वास्तव में व्यापक एक्सेस चाहिए। टीमें अक्सर “बस इस एक जॉब के लिए” बायपास रोल बनाती हैं, और यहीं गलतियाँ जमा होने लगती हैं।
फैसला करने से पहले तय करें कि क्या आप डेटाबेस को अंतिम गेटकीपर बनाना चाहते हैं। अगर हाँ, तो डिज़िप्लिन के लिए योजना बनाएं: डेटाबेस व्यवहार को टेस्ट करें (सिर्फ़ API रिस्पॉन्स नहीं), माइग्रेशन को सुरक्षा परिवर्तन समझें, त्वरित बायपास से बचें, तय करें कि बैकग्राउंड जॉब कैसे ऑथेंटिकेट होंगे, और नीतियाँ छोटी और दोहराने योग्य रखें।
यदि आप टूलिंग इस्तेमाल करते हैं जो बैकएंड जेनरेट करती है, तो यह डिलिवरी तेज कर सकती है, पर स्पष्ट रोल, टेस्ट और सरल टेनेंट मॉडल की ज़रूरत नहीं हटती। (उदाहरण के लिए, Koder.ai Go और PostgreSQL का उपयोग करके जेनरेटेड बैकएंड देता है, और आप RLS को बाद में "छिड़कने" के बजाय जानबूझकर डिजाइन करना चाहेंगे।)
RLS सबसे आसान तब होता है जब आपका स्कीमा पहले ही साफ़ बताए कि कौन किसका मालिक है। अगर आप अस्पष्ट मॉडल से शुरू करते हैं और कोशिश करते हैं "नीतियों में ठीक कर देने" की, तो आमतौर पर आप धीमी क्वेरियों और भ्रमित बग्स पाते हैं।
एक टेनेंट की एक कुंजी चुनें (जैसे org_id) और उसे लगातार उपयोग करें। अधिकांश टेनेंट-स्वामित्व वाली तालिकाओं में यह होना चाहिए, भले ही वे किसी दूसरी तालिका को रेफ़रेंस भी करें जिसमें यह पहले ही हो। इससे नीतियों के अंदर जॉइन से बचाव होता है और USING चेक्स सरल रहते हैं।
एक व्यावहारिक नियम: अगर कोई पंक्ति ग्राहक रद्द करने पर गायब हो जानी चाहिए, तो शायद उसे org_id चाहिए।
RLS नीतियाँ सामान्यतः एक सवाल का जवाब देती हैं: “क्या यह यूज़र इस ऑर्ग का मेंबर है, और वह क्या कर सकता है?” इसे एड-हॉक कॉलम्स से निकालना मुश्किल होता है।
कोर तालिकाएँ छोटी और नीरस रखें:\n\n- users (प्रति व्यक्ति एक पंक्ति)\n- orgs (प्रति टेनेंट एक पंक्ति)\n- org_memberships (user_id, org_id, role, status)\n- वैकल्पिक: प्रति-प्रोजेक्ट एक्सेस के लिए project_memberships\n\nइसके साथ, आपकी नीतियाँ एक इंडेक्स्ड लुकअप से मेंबरशिप चेक कर सकती हैं।
हर चीज़ को org_id की ज़रूरत नहीं होती। देशों, उत्पाद श्रेणियों, या प्लान प्रकार जैसी रेफ़रेंस तालिकाएँ अक्सर सभी टेनेंट्स के लिए साझा होती हैं। उन्हें अधिकांश रोल्स के लिए केवल-रीड रखें, और उन्हें किसी एक ऑर्ग से न बाँधें।
टेनेंट-स्वामित्व वाला डेटा (प्रोजेक्ट्स, इनवॉइसेज़, टिकट्स) को साझा तालिकाओं से टेनेंट-विशिष्ट डिटेल्स खींचने से बचाएँ। साझा तालिकाएँ न्यूनतम और स्थिर रखें।
Foreign keys RLS के साथ अभी भी काम करते हैं, पर डिलीट्स आपको आश्चर्यचकित कर सकती हैं अगर डिलीट करने वाला रोल निर्भर पंक्तियों को "देख" नहीं सकता। कैस्केड्स की योजना सावधानी से बनाएं और असली डिलीट फ़्लोज़ का परीक्षण करें।
उन कॉलम्स पर इंडेक्स बनाएं जिन पर आपकी नीतियाँ फ़िल्टर करती हैं, खासकर org_id और मेंबरशिप कुंजियाँ। एक नीति जो दिखती है जैसे WHERE org_id = ... लाखों पंक्तियों वाली तालिका में फुल-टेबल स्कैन नहीं बननी चाहिए।
RLS एक प्रति-तालिका स्विच है। एक बार सक्षम होने पर, PostgreSQL आपकी ऐप को टेनेंट फ़िल्टर याद रखने पर भरोसा करना बंद कर देता है। हर SELECT, UPDATE, और DELETE नीतियों द्वारा फ़िल्टर किया जाता है, और हर INSERT और UPDATE नीतियों द्वारा वैधता जाँची जाती है।
सबसे बड़ा मानसिक बदलाव: RLS ऑन होने पर वे क्वेरियाँ जो पहले डेटा रिटर्न करती थीं, अब बिना एरर के ज़ीरो रो वापस कर सकती हैं। यह PostgreSQL कर रहा होता है—एक्सेस कंट्रोल।
नीतियाँ तालिका से जुड़ी छोटी शर्तें होती हैं। वे दो चेक्स का उपयोग करती हैं:\n\n- USING पढ़ने का फ़िल्टर है। अगर कोई पंक्ति USING से मैच नहीं करती, तो वह SELECT के लिए अदृश्य होती है, और उसे UPDATE या DELETE का लक्ष्य नहीं बनाया जा सकता।\n- WITH CHECK लिखने का गेट है। यह तय करता है कि नए या बदले हुए पंक्तियाँ INSERT या UPDATE के दौरान स्वीकार्य हैं या नहीं।\n\nएक सामान्य SaaS पैटर्न: USING यह सुनिश्चित करता है कि आप केवल अपने टेनेंट की पंक्तियाँ देखें, और WITH CHECK यह सुनिश्चित करता है कि आप गलत टेनेंट आईडी की अटकल लगाकर किसी और के टेनेंट में पंक्ति घुसा न सकें।
जब आप बाद में और नीतियाँ जोड़ते हैं, तो यह मायने रखता है:\n\n- PERMISSIVE (डिफ़ॉल्ट): अगर कोई भी नीति उसे अनुमति देती है तो पंक्ति अनुमति पाती है।\n- RESTRICTIVE: एक पंक्ति तभी अनुमति पाती है जब सभी restrictive नीतियाँ उसे अनुमति दें (permissive व्यवहार के ऊपर)।\n\nअगर आप टेनेंट मैच के साथ रोल चेक्स और प्रोजेक्ट मेंबरशिप की तरह लेयर करने की योजना बनाते हैं, तो restrictive नीतियाँ इरादा स्पष्ट कर सकती हैं, पर यदि आप एक शर्त भूल जाएँ तो यह खुद को लॉक आउट करना भी आसान बना देती हैं।
RLS को एक भरोसेमंद “कौन कॉल कर रहा है” मान चाहिए। सामान्य विकल्प:\n\n- प्रति-रिक्वेस्ट एक सेशन वैरिएबल सेट करना (उदाहरण: app.user_id और app.tenant_id)।\n- JWT क्लेम्स को आपकी API लेयर द्वारा सेशन सेटिंग्स में मैप करना।\n- रोल स्विचिंग (SET ROLE ... प्रति-रिक्वेस्ट), जो काम कर सकता है पर ऑपरेशनल ओवरहेड जोड़ता है।\n\nएक तरीका चुनें और हर जगह लागू करें। सर्विसेज़ के बीच आइडेंटिटी स्रोतों को मिलाना भ्रमित बग्स की तेज़ राह है।
एक पूर्वानुमानित कन्वेंशन का उपयोग करें ताकि स्कीमा डम्प और लॉग पठनीय बने रहें। उदाहरण: {table}__{action}__{rule}, जैसे projects__select__tenant_match।
अगर आप RLS में नए हैं, तो एक तालिका और छोटे प्रूफ से शुरू करें। लक्ष्य पूर्ण कवरेज नहीं है। लक्ष्य यह है कि डेटाबेस क्रॉस-टेनेंट एक्सेस को नामंज़ूर कर दे भले ही ऐप में बग हो।
मान लीजिए एक सरल projects तालिका है। पहले, tenant_id जोड़ें ऐसा तरीका अपनाकर जो लिखने में टूट न डाले।
ALTER TABLE projects ADD COLUMN tenant_id uuid;
-- Backfill existing rows (example: everyone belongs to a default tenant)
UPDATE projects SET tenant_id = '11111111-1111-1111-1111-111111111111'::uuid
WHERE tenant_id IS NULL;
ALTER TABLE projects ALTER COLUMN tenant_id SET NOT NULL;
इसके बाद, ओनरशिप को एक्सेस से अलग करें। एक आम पैटर्न: एक रोल तालिकाओं का मालिक होता है (app_owner), दूसरा रोल API द्वारा उपयोग किया जाता है (app_user)। API रोल को तालिका का मालिक नहीं होना चाहिए, वरना वह नीतियों को बायपास कर सकता है।
ALTER TABLE projects OWNER TO app_owner;
REVOKE ALL ON projects FROM PUBLIC;
GRANT SELECT, INSERT, UPDATE, DELETE ON projects TO app_user;
अब तय करें कि अनुरोध Postgres को किस टेनेंट के लिए सर्व कर रहा है यह कैसे बताएगा। एक सरल तरीका है अनुरोध-स्कोप्ड सेटिंग। आपका ऐप इसे ट्रांज़ैक्शन खोलने के तुरंत बाद सेट करता है।
-- inside the same transaction as the request
SELECT set_config('app.current_tenant', '22222222-2222-2222-2222-222222222222', true);
RLS सक्षम करें और पढ़ने से शुरू करें।
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
CREATE POLICY projects_tenant_select
ON projects
FOR SELECT
TO app_user
USING (tenant_id = current_setting('app.current_tenant')::uuid);
यह दो अलग टेनेंट्स के साथ आज़माकर साबित करें कि रो काउंट बदलता है।
WITH CHECK)पढ़ने की नीतियाँ लिखों की रक्षा नहीं करतीं। WITH CHECK जोड़ें ताकि inserts और updates किसी गलत टेनेंट में पंक्तियाँ न घुसा सकें।
CREATE POLICY projects_tenant_write
ON projects
FOR INSERT, UPDATE
TO app_user
WITH CHECK (tenant_id = current_setting('app.current_tenant')::uuid);
व्यवहार (और फेल्योर) प्रमाणित करने का एक तेज़ तरीका यह है कि आपके पास एक छोटा SQL स्क्रिप्ट हो जिसे आप हर माइग्रेशन के बाद फिर चला सकें:\n\n- BEGIN; SET LOCAL ROLE app_user;\n- SELECT set_config('app.current_tenant', '\u003ctenant A\u003e', true); SELECT count(*) FROM projects;\n- INSERT INTO projects(id, tenant_id, name) VALUES (gen_random_uuid(), '\u003ctenant B\u003e', 'bad'); (यह असफल होना चाहिए)\n- UPDATE projects SET tenant_id = '\u003ctenant B\u003e' WHERE ...; (यह असफल होना चाहिए)\n- ROLLBACK;\n\nअगर आप वह स्क्रिप्ट चला कर हर बार एक समान परिणाम पा रहे हैं, तो आपके पास अन्य तालिकाओं पर RLS विस्तार से पहले एक भरोसेमंद बेसलाइन है।
अधिकांश टीमें वही नीतियाँ अपनाती हैं जब वे हर क्वेरी में एक ही ऑथ चेक दोहराने से थक जाती हैं। अच्छी खबर यह है कि जिन नीति आकारों की आपको ज़रूरत होती है वे आम तौर पर सुसंगत होते हैं।
कुछ तालिकाएँ स्वाभाविक रूप से एक उपयोगकर्ता द्वारा स्वामित्व वाली होती हैं (नोट्स, API टोकन)। अन्य एक टेनेंट के होते हैं जहाँ एक्सेस मेंबरशिप पर निर्भर करता है। इन्हें अलग पैटर्न की तरह ट्रीट करें।
ओनर-ओनली डेटा के लिए, नीतियाँ अक्सर created_by = app_user_id() चेक करती हैं। टेनेंट डेटा के लिए, नीतियाँ अक्सर यह चेक करती हैं कि क्या यूज़र के पास उस ऑर्ग के लिए मेंबरशिप पंक्ति है।
नीतियाँ पठनीय रखने का व्यावहारिक तरीका यह है कि पहचान को छोटे SQL हेल्पर्स में केंद्रीकृत करें और उन्हें पुन:प्रयोग करें:
-- Example helpers
create function app_user_id() returns uuid
language sql stable as $$
select current_setting('app.user_id', true)::uuid
$$;
create function app_is_admin() returns boolean
language sql stable as $$
select current_setting('app.is_admin', true) = 'true'
$$;
पढ़ने अक्सर लिखने से व्यापक होते हैं। उदाहरण के लिए, किसी भी ऑर्ग मेंबर को SELECT करने दें, पर केवल एडिटर्स को UPDATE, और केवल मालिकों को DELETE करने दें।
इसे स्पष्ट रखें: SELECT के लिए एक नीति, INSERT/UPDATE के लिए WITH CHECK के साथ एक नीति, और DELETE के लिए एक अलग (अक्सर अपडेट से कड़ा) नीति।
एडमिन के लिए RLS बंद करने से बचें। इसके बजाय, नीतियों के अंदर एक एस्केप हैच जोड़ें, जैसे app_is_admin(), ताकि आप गलती से किसी साझा सर्विस रोल को पूरा एक्सेस न दे दें।
अगर आप deleted_at या status उपयोग करते हैं, तो इसे SELECT नीति में शामिल करें (deleted_at is null)। वरना कोई व्यक्ति उन फ़्लैग्स को उलट कर पंक्तियाँ "जिंदा" कर सकता है जिन्हें ऐप ने फाइनल माना था।
WITH CHECK को दोस्ताना रखेंINSERT ... ON CONFLICT DO UPDATE को लिखने के बाद पंक्ति पर WITH CHECK संतुष्ट होना चाहिए। अगर आपकी नीति created_by = app_user_id() माँगती है, तो सुनिश्चित करें कि आपका upsert insert पर created_by सेट करे और update पर उसे ओवरराइट न करे।
अगर आप बैकएंड को जेनरेट करते हैं, तो ये पैटर्न आंतरिक टेम्पलेट में बदलने लायक होते हैं ताकि नई तालिकाएँ ब्लैंक स्लेट की बजाय सुरक्षित डिफ़ॉल्ट के साथ शुरू हों।
RLS शानदार है जब तक कि एक छोटा सा विवरण PostgreSQL को "यादृच्छिक रूप से" डेटा छुपाने/दिखाने जैसा नहीं दिखा देता। नीचे दी गई गलतियाँ सबसे अधिक समय बर्बाद करती हैं।
पहला जाल WITH CHECK भूलना है। USING नियंत्रित करता है कि आप क्या देख सकते हैं, ना कि आप क्या बना सकते हैं। WITH CHECK के बिना, ऐप बग किसी गलत टेनेंट में पंक्ति लिख सकता है, और आप इसे नोटिस न भी करें क्योंकि वही यूज़र उसे वापस नहीं पढ़ पाएगा।
एक और सामान्य लीक है "लीकी जॉइन।" आप सही तरीके से projects को फिल्टर करते हैं, फिर invoices, notes, या files के साथ जॉइन करते हैं जो वही सुरक्षा नहीं रखते। समाधान सख्त पर सरल है: हर तालिका जो टेनेंट डेटा प्रकट कर सकती है उसे अपनी नीति चाहिए, और व्यूज़ को इस पर निर्भर नहीं होना चाहिए कि केवल एक तालिका सुरक्षित है।
सामान्य विफलता पैटर्न जल्दी दिखते हैं:\n\n- एक पढ़ने की नीति है, पर लिखने की नीति में WITH CHECK गायब है।\n- कोई नीति ऐसी शर्त बनाती है जो किसी दूसरी तालिका पर जॉइन करती है जो सुरक्षित नहीं है।\n- एक्सेस एक व्यू में लागू है, पर नीचे की तालिका खुली रहती है।\n- आप यह मानते हैं कि "ऐप हमेशा tenant_id सेट करता है," और एक बैकग्राउंड जॉब भूल जाता है।\n- आप सुपरयूज़र रोल के साथ टेस्ट करते हैं, इसलिए आप असली व्यवहार कभी नहीं देखते।
नीतियाँ जो उसी तालिका का संदर्भ लेती हैं (प्रत्यक्ष रूप से या व्यू के माध्यम से) रिकार्शन आश्चर्य पैदा कर सकती हैं। एक नीति मेंबरशिप चेक के लिए एक व्यू क्वेरी कर सकती है जो फिर से सुरक्षित तालिका पढ़ता है, जिससे एरर, धीमी क्वेरी, या ऐसी नीति हो सकती है जो कभी मैच न करे।
रोल सेटअप भी भ्रम का स्रोत है। तालिका के मालिक और उन्नत रोल RLS को बायपास कर सकते हैं, इसलिए आपके टेस्ट पास हो सकते हैं जबकि वास्तविक यूज़र फेल कर रहे हों (या इसके विपरीत)। हमेशा उसी लो-प्रिविलेज रोल के साथ टेस्ट करें जिसका आपका ऐप उपयोग करता है।
SECURITY DEFINER फ़ंक्शन्स के साथ सतर्क रहें। वे फ़ंक्शन मालिक के अधिकारों के साथ चलते हैं, इसलिए एक हेल्पर जैसा current_tenant_id() ठीक हो सकता है, पर एक "सुविधाजनक" फ़ंक्शन जो डेटा पढ़ता है अनजाने में टेनेंट्स के बीच पढ़ सकता है जब तक आप इसे RLS का सम्मान करने के लिए डिज़ाइन न करें।
साथ ही security definer फ़ंक्शन्स के अंदर एक सुरक्षित search_path सेट करें। नहीं तो फ़ंक्शन उसी नाम की किसी दूसरी ऑब्जेक्ट को पकड़ सकता है, और आपकी नीति लॉजिक सेशन स्टेट पर निर्भर करके चुपचाप गलत चीज़ देख सकती है।
RLS बग्स आम तौर पर संदर्भ गायब होने से जुड़ी होती हैं, न कि "खराब SQL" से। एक नीति कागज़ पर सही हो सकती है और फिर भी विफल हो सकती है क्योंकि सेशन रोल वह नहीं है जो आप सोचते थे, या अनुरोध ने कभी टेनेंट और यूज़र वैल्यूज़ नहीं सेट कीं जिन पर नीति निर्भर करती है।
प्रोडक्शन रिपोर्ट को रीप्रोड्यूस करने का भरोसेमंद तरीका है वही सेशन सेटअप लोकल में बनाना और वही ठीक वही क्वेरी चलाना। इसका मतलब आमतौर पर:\n\n- SET ROLE app_user; (या असली API रोल)\n- SELECT set_config('app.tenant_id', 't_123', true); और SELECT set_config('app.user_id', 'u_456', true);\n- वही SQL चलाएं जो आपका ऐप चलाता था (परामीटर्स सहित)\n- पुष्टि करें कि Postgres क्या देख रहा है: SELECT current_user, current_setting('app.tenant_id', true), current_setting('app.user_id', true);
जब आपको संदेह हो कि कौन सी नीति लागू हो रही है, तो अंदाज़े लगाने की बजाय कैटलॉग चेक करें। pg_policies हर नीति दिखाता है, कमांड और USING/WITH CHECK अभिव्यक्तियाँ। इसे pg_class के साथ पेयर करें ताकि पुष्टि हो जाए कि तालिका पर RLS सक्षम है और बायपास नहीं हो रहा।
परफ़ॉर्मेंस समस्याएँ ऑथ समस्याओं जैसी दिख सकती हैं। एक नीति जो मेंबरशिप तालिका से जॉइन करती है या किसी फ़ंक्शन को कॉल करती है सही हो सकती है पर तालिका बड़े होने पर धीमी हो सकती है। रीप्रोड्यूस किए गए क्वेरी पर EXPLAIN (ANALYZE, BUFFERS) का उपयोग करें और देखें कि सीक्वेन्शियल स्कैन, अनपेक्षित नेस्टेड लूप्स, या देर से लागू होने वाले फिल्टर्स कहाँ हैं। (tenant_id, user_id) और मेंबरशिप तालिकाओं पर इंडेक्स की कमी आम कारण हैं।
यह भी मदद करता है कि हर रिक्वेस्ट पर ऐप लेयर में तीन वैल्यूज़ लॉग करें: टेनेंट ID, यूज़र ID, और उस रिक्वेस्ट के लिए प्रयुक्त डेटाबेस रोल। जब ये वही नहीं होते जो आप सोचते थे, तो RLS "गलत" व्यवहार करेगा क्योंकि इनपुट ही गलत हैं।
टेस्ट्स के लिए, कुछ सीड टेनेंट्स रखें और असफलताओं को स्पष्ट बनाएं। छोटी सूट में आम तौर पर शामिल होते हैं: “Tenant A Tenant B को नहीं पढ़ सकता,” “मेंबरशिप न रखने वाला यूज़र प्रोजेक्ट नहीं देख सकता,” “ओनर अपडेट कर सकता है, व्यूअर नहीं,” “INSERT तब रोका जाता है जब तक tenant_id संदर्भ से मेल नहीं खाता,” और “एडमिन ओवरराइड केवल जहाँ बदले हुए इरादे के अनुसार लागू हो।”
RLS को फीचर की तरह नहीं, सीटबेल्ट की तरह ट्रीट करें। छोटी चूकें “हर कोई हर किसी का डेटा देख सकता है” या "सब कुछ ज़ीरो रो लौटाता है" में बदल सकती हैं।
सुनिश्चित करें कि आपकी तालिका डिज़ाइन और नीति नियम आपके टेनेंट मॉडल से मेल खाते हों।\n\n- हर टेनेंट-स्वामित्व वाली तालिका में स्पष्ट टेनेंट कुंजी होनी चाहिए (आम तौर पर tenant_id). अगर नहीं है, तो लिखें कि क्यों (उदाहरण: ग्लोबल रेफ़रेंस तालिकाएँ)।\n- हर टेनेंट-स्वामित्व वाली तालिका पर RLS सक्षम करें, सिर्फ़ "मुख्य" तालिकाओं पर नहीं। अगर कुछ पाथ्स को कभी बायपास नहीं होना चाहिए, तो उन तालिकाओं पर FORCE ROW LEVEL SECURITY पर विचार करें।\n- पढ़ने और लिखने के नियम अलग रखें। पढ़ने USING का उपयोग करें। लिखने के लिए WITH CHECK ज़रूरी है ताकि inserts और updates किसी अन्य टेनेंट में पंक्ति न ले जाएँ।\n- नीति प्रेडिकेट्स को इंडेक्स-अनुकूल रखें। अगर नीतियाँ tenant_id पर फ़िल्टर करती हैं या मेंबरशिप तालिकाओं के ज़रिये जॉइन करती हैं, तो मेल खाते इंडेक्स जोड़ें।
एक सरल सत्यापन परिदृश्य: टेनेंट A का यूज़र अपनी इनवॉइसेज़ पढ़ सकता है, केवल टेनेंट A के लिए इनवॉइस डाल सकता है, और किसी इनवॉइस का tenant_id बदलकर उसे अपडेट नहीं कर सकता।
RLS उतना ही मज़बूत है जितने रोल्स आपका ऐप उपयोग करता है।\n\n- पुष्टि करें कि ऐप कभी superuser, तालिका ओनर, या किसी भी ऐसे रोल के रूप में कनेक्ट न करे जिनके पास bypassrls हो।\n- वास्तविक डेटा वॉल्यूम के साथ कुछ असली क्वेरियाँ चलाएं और क्वेरी योजनाएँ जांचें।\n- कुछ स्वचालित निगेटिव टेस्ट जोड़ें जो साबित करें कि क्रॉस-टेनेंट एक्सेस फेल होती है।
कल्पना कीजिए एक B2B ऐप जहाँ कंपनियाँ (orgs) प्रोजेक्ट्स रखती हैं, और प्रोजेक्ट्स में टास्क होते हैं। यूज़र कई ऑर्ग्स के मेंबर हो सकते हैं, और किसी उपयोगकर्ता का कुछ प्रोजेक्ट्स में मेंबरशिप हो सकती है और कुछ में नहीं। यह RLS के लिए अच्छा फिट है क्योंकि डेटाबेस टेनेंट अलगाव लागू कर सकता है भले ही कोई API एंडपॉइंट फ़िल्टर भूल जाए।
सरल मॉडल: orgs, users, org_memberships (org_id, user_id, role), projects (id, org_id), project_memberships (project_id, user_id), tasks (id, project_id, org_id, ...). tasks पर org_id जानबूझकर है। यह नीतियों को सरल रखता है और जॉइन्स के दौरान आश्चर्यों को कम करता है।
क्लासिक लीक तब होता है जब tasks में केवल project_id हो, और आपकी नीति एक्सेस को projects से जॉइन करके चेक करती है। एक गलती (projects पर परमीसिव पॉलिसी, किसी जॉइन ने कंडीशन ड्रॉप कर दी, या कोई व्यू संदर्भ बदल देता है) tasks को दूसरे ऑर्ग से एक्सपोज़ कर सकती है।
एक सुरक्षित माइग्रेशन पथ बिना प्रोडक्शन ट्रैफ़िक तोड़ने के लिए:
tasks में org_id जोड़ें, मेंबरशिप तालिकाएँ जोड़ें)।\n- tasks.org_id को projects.org_id से बैकफिल करें, फिर NOT NULL जोड़ें।\n- नीतियाँ जोड़ें पर स्टेजिंग में टेस्ट करते समय RLS अक्षम रखें।\n- RLS सक्षम करें, फिर FORCE करें, और तभी पुराने ऐप-साइड फ़िल्टर्स हटाएँ।सपोर्ट एक्सेस आमतौर पर एक संकीर्ण ब्रेक-ग्लास रोल के साथ बेहतर संभाला जाता है, न कि RLS को डिसेबल करके। इसे सामान्य सपोर्ट खातों से अलग रखें और स्पष्ट रखें कि कब इसका उपयोग हुआ।
नीतियों का डॉक्यूमेंट रखें ताकि वे घिसे-पिटे न हों: कौन से सेशन वैरिएबल्स सेट होने चाहिए (user_id, org_id), किन तालिकाओं में org_id होना चाहिए, “मेंबर” का मतलब क्या है, और कुछ SQL उदाहरण जो गलत ऑर्ग से चलाने पर 0 पंक्तियाँ लौटाने चाहिए।
RLS का सहारा तब सरल रहता है जब आप इसे एक प्रोडक्ट चेंज की तरह ट्रीट करें। इसे छोटे हिस्सों में रोल आउट करें, टेस्ट के साथ व्यवहार साबित करें, और प्रत्येक नीति के पीछे स्पष्ट कारण रखें।
एक काम करने वाली रोलआउट योजना:\n\n- एक तालिका से शुरू करें जिसका स्पष्ट टेनेंट स्वामित्व हो (उदाहरण: projects) और उसे लॉक डाउन करें।\n- कुछ रोल्स (owner, member, outsider) के लिए अनुमति और अवरुद्ध पढ़ने/लिखने को कवर करने वाले टेस्ट जोड़ें।\n- बैचों में विस्तार करें (एक बार में एक फ़ीचर एरिया) ताकि आप एक ही सेशन में डिबग कर सकें।\n- रोलआउट के दौरान परमीशन एरर्स की मॉनिटरिंग करें और कम जोखिम विंडो में डिप्लॉय करें।
पहली तालिका स्थिर होने के बाद, नीति परिवर्तन जानबूझकर करें। माइग्रेशन्स में नीति रिव्यू स्टेप जोड़ें, और हर नीति के इरादे पर एक छोटा नोट रखें (कौन क्या देख/क्यों देखेगा) साथ में मेल खाता टेस्ट अपडेट। इससे "बस और एक OR जोड़ो" जैसी नीतियाँ नहीं बनेंगी जो धीरे-धीरे छेद बन जाती हैं।
यदि आप तेज़ी से मूव कर रहे हैं, तो Koder.ai जैसी टूल्स (koder.ai) चैट के जरिए एक Go + PostgreSQL शुरुआत बिंदु जेनरेट करने में मदद कर सकती हैं, और फिर आप वही अनुशासन अपनाकर RLS नीतियाँ और टेस्ट ऊपर जोड़ सकते हैं जैसे कि हाथ से बने बैकएंड में करते हैं।
अंत में, रोलआउट के दौरान सुरक्षा रेल रखें। नीति माइग्रेशन्स से पहले स्नैपशॉट लें, रोलबैक का अभ्यास तब तक करें जब तक यह बोरिंग न लगे, और सपोर्ट के लिए एक छोटा ब्रेक-ग्लास पथ रखें जो पूरे सिस्टम पर RLS न को डिसेबल करे।
RLS PostgreSQL को यह लागू करवाता है कि किसी अनुरोध के लिए कौन सी पंक्तियाँ दिखाई दें या लिखी जा सकें—इसलिए टेनेंट अलगाव इस पर निर्भर नहीं रहता कि हर एंडपॉइंट ने सही WHERE tenant_id = ... फिल्टर लगाया है। सबसे बड़ा लाभ यह है कि "एक छूटा हुआ चेक" गलती कम हो जाती है, जब ऐप बढ़ता है और क्वेरीज़ कई हो जाती हैं।
यह तब फायदेमंद है जब एक्सेस नियम लगातार और पंक्ति-आधारित हों—जैसे टेनेंट अलगाव या मेंबरशिप-आधारित एक्सेस—और आपके पास कई क्वेरी पाथ हों (सर्च, एक्सपोर्ट, एडमिन स्क्रीन, बैकग्राउंड जॉब)। अगर अधिकांश नियम फ़ील्ड-स्तर पर हैं, बहुत अपवाद हैं, या व्यापक रिपोर्टिंग की ज़रूरत है, तो आम तौर पर RLS उपयुक्त नहीं होता।
RLS पंक्ति दृश्यमानता और बेसिक राइट-गेटिंग के लिए है; बाक़ी चीज़ों के लिए अलग टूल्स उपयोग करें। कॉलम प्राइवेसी के लिए आमतौर पर व्यूज़ और कॉलम प्रिविलेज की ज़रूरत होती है, और जटिल बिज़नेस नियम (जैसे बिलिंग स्वामित्व या अप्रूवल फ्लो) अभी भी एप्लिकेशन लॉजिक या सावधानी से डिज़ाइन किए गए डेटाबेस कंस्ट्रेंट्स में रहते हैं।
एक लो-प्रिविलेज रोल बनाकर शुरुआत करें (टेबल ओनर नहीं), RLS सक्षम करें, फिर एक SELECT नीति और INSERT/UPDATE के लिए WITH CHECK वाली नीति जोड़ें। अनुरोध-स्कोप्ड सेशन वैरिएबल (जैसे app.current_tenant) सेट करें और सत्यापित करें कि उसे बदलने पर आप कौन सी पंक्तियाँ देख/लिख सकते हैं।
एक सामान्य तरीका यह है कि हर अनुरोध के लिए सेशन वैरिएबल सेट करें, जैसे app.tenant_id और app.user_id। आपकी नीतियाँ इन वैरिएबल्स पर भरोसा करें। विकल्पों में JWT क्लेम्स को सेशन सेटिंग्स में मैप करना या per-request SET ROLE शामिल हैं। जो भी तरीका चुनें, उसे हर कोड पाथ पर लगातार लागू करें—वेब रिक्वेस्ट, जॉब्स और स्क्रिप्ट सभी।
USING यह नियंत्रित करता है कि मौजूदा पंक्तियाँ कौन सी दिखाई/टार्गेटेबल हैं (SELECT, UPDATE, DELETE)। WITH CHECK यह नियंत्रित करता है कि INSERT या UPDATE के दौरान कौन सी नई या बदली पंक्तियाँ अनुमति पाती हैं—इसलिए यह किसी गलत के साथ किसी और के टेनेंट में लिखने से रोकता है।
क्योंकि केवल USING जोड़ने पर भी एक बग्गी एंडपॉइंट किसी अन्य टेनेंट में पंक्तियाँ लिख सकता है, और वही यूज़र उन्हें पढ़ नहीं पाएगा—इसलिए आप नोटिस नहीं करेंगे। इसलिए पढ़ने के नियम के साथ हमेशा एक मेल खाती WITH CHECK लिखें ताकि गलत डेटा बन ही न सके।
नीतियाँ सरल और तेज़ रखने के लिए टेनेंट की कुंजी (जैसे org_id) को सीधे टेनेंट-स्वामित्व वाली तालिकाओं पर रखें, भले ही वे किसी अन्य तालिका को रेफ़रेंस भी करें। स्पष्ट मेंबरशिप तालिकाएँ रखें (org_memberships, वैकल्पिक project_memberships) ताकि नीतियाँ एक इंडेक्स्ड लुकअप से काम कर सकें बजाए जटिल अनुमान के।
पहले वही सेशन संदर्भ स्थानीय रूप से रीप्रोड्यूस करें जो आपका ऐप प्रयोग करता है: वही रोल और सेशन सेटिंग्स सेट करके वही SQL चलाएँ। फिर सत्यापित करें कि pg_policies में कौन-सी USING और WITH CHECK अभिव्यक्तियाँ लागू हैं। अक्सर RLS "छुपा रहा है" जैसा दिखता है क्योंकि इनपुट संदर्भ गलत होते हैं, न कि SQL गलत।
हां—पर जेनरेटेड कोड को एक शुरुआती बिंदु समझें, सुरक्षा प्रणाली नहीं। यदि आप Koder.ai से Go + PostgreSQL बैकएंड जेनरेट करते हैं, तो आपको अपना टेनेंट मॉडल परिभाषित करना होगा, सेशन आइडेंटिटी को लगातार सेट करना होगा, और नीतियाँ व टेस्ट ज़िम्मेदारी से जोड़ने होंगे ताकि नई तालिकाएँ बिना सुरक्षा के न जाएँ।
tenant_id