← All posts

हमने एक भी पंक्ति माइग्रेट किए बिना एक ख़तरनाक डिफ़ॉल्ट बंद कर दिया

Authagonal·July 29, 2026

जस्ट-इन-टाइम प्रोविज़निंग वह सुविधा है जो एंटरप्राइज़ SSO को जादू जैसा महसूस कराती है। एक नया कर्मचारी अपनी कंपनी के आइडेंटिटी प्रोवाइडर के ज़रिए साइन इन करता है, आपके ऐप में अभी तक उसका कोई खाता नहीं है, और assertion से उसी वक़्त एक खाता बन जाता है। कोई टिकट नहीं खोलता, किसी को न्योता नहीं भेजा जाता, वह व्यक्ति बस काम करने लगता है।

दूसरी दिशा से पढ़ें तो यह वह सुविधा भी है जो उस आइडेंटिटी प्रोवाइडर को नियंत्रित करने वाले किसी भी व्यक्ति को यह दावा करके कि कोई व्यक्ति मौजूद है, आपके ग्राहक के tenant में खाते बनाने देती है। यह ठीक है जब कनेक्शन कसकर किसी एक कंपनी की डायरेक्टरी तक सीमित हो और उसमें मौजूद हर इंसान को पहुँच मिलनी ही चाहिए। यह कम ठीक है जब कनेक्शन एक साझा डायरेक्टरी हो, या ठेकेदारों का tenant हो, या उन फैली हुई फ़ेडरेशनों में से एक हो जहाँ जिन लोगों के लिए आइडेंटिटी प्रोवाइडर गवाही देने को तैयार है उनका समूह उन लोगों के समूह से कहीं बड़ा हो जिन्हें आपका ग्राहक अंदर आने देना चाहता था।

हमारा डिफ़ॉल्ट रूप से चालू था। इसलिए नहीं कि किसी ने तय किया कि ऐसा होना चाहिए, और यही हिस्सा ठहरकर सोचने लायक है। यह डिफ़ॉल्ट रूप से चालू इसलिए था क्योंकि जब फ़ील्ड जोड़ा गया, तो उसे व्यक्त करने वाला boolean DisableJitProvisioning कहलाता था, और बिना सेट किया गया boolean false होता है, और false का मतलब था "disable मत करो"। जिस डिफ़ॉल्ट को किसी ने नहीं चुना, उसका सबसे सुरक्षित पाठ यही है कि वह एक इत्तेफ़ाक है, और यह वाला इत्तेफ़ाक व्यवहार बनकर जम गया था।

जोखिम के बारे में सटीक रहें, क्योंकि यह कभी इतना बुरा नहीं था कि "कोई भी किसी को भी बना सके": प्रोविज़निंग से पहले दो द्वार पहले से ही चलते थे। एक कनेक्शन अनुमत ईमेल डोमेन की सूची रख सकता है, और उनसे बाहर का कोई भी assertion अस्वीकार कर दिया जाता है। एक कनेक्शन आमंत्रण विशेषता की माँग कर सकता है, और बिना आमंत्रण वाला उपयोगकर्ता अस्वीकार कर दिया जाता है। डिफ़ॉल्ट चालू का असली जोखिम वह कनेक्शन था जिसमें इनमें से कोई भी कॉन्फ़िगर नहीं किया गया था, और यही ठीक उस कनेक्शन का रूप है जिसे कोई SSO चालू कराने के लिए जल्दी में खड़ा कर देता है।

डिफ़ॉल्ट को पलटना एक शब्द है। उसे सुरक्षित रूप से पलटना नहीं।

जो बदलाव हर कोई कल्पना करता है वह है फ़ील्ड का नाम बदलकर JitProvisioningEnabled करना और उसका डिफ़ॉल्ट false रखना। नए कनेक्शन डिफ़ॉल्ट रूप से सुरक्षित, बात ख़त्म।

बस इतना है कि वह फ़ील्ड संग्रहित होता है, और स्टोरेज में ऐसे कनेक्शन मौजूद हैं जो उसके होने से पहले लिखे गए थे। उनकी rows में वह column है ही नहीं। उनके साथ क्या होगा यह पूरी तरह इस पर निर्भर करता है कि boolean किस दिशा में इशारा करता है, क्योंकि लापता column दोनों ही सूरतों में false में deserialize होता है। पुराने नकारात्मक नाम के तहत, लापता का मतलब है "disable नहीं है" और प्रोविज़निंग जारी रहती है। नए सकारात्मक नाम के तहत, लापता का मतलब है "enable नहीं है" और प्रोविज़निंग रुक जाती है।

तो एक सीधा नाम-परिवर्तन उन सभी कनेक्शनों के लिए जस्ट-इन-टाइम प्रोविज़निंग को चुपचाप बंद कर देता है जिन्हें ग्राहक ने तब कॉन्फ़िगर किया था जब यह चालू थी। उन्होंने कुछ नहीं चुना, उन्हें बताया नहीं गया, और उन्हें इसकी पहली ख़बर एक ऐसे कर्मचारी से मिलती है जो साइन इन नहीं कर पा रहा, चाहे वह जिस भी घड़ी हो। यह कोई सुरक्षा सुधार नहीं है, यह डिप्लॉय के साथ पहुँची एक outage है।

स्पष्ट जवाब है एक backfill: हर संग्रहित कनेक्शन पर चलें, column को स्पष्ट रूप से लिखें, फिर डिफ़ॉल्ट पलट दें। यह काम करता है, पर यह एक ऐसा माइग्रेशन है जिसे आपको लिखना, परखना, हर tenant के स्टोरेज पर चलाना, और यह सुनिश्चित करना होगा कि यह हर जगह पूरा हो चुका है, इससे पहले कि उस पर निर्भर कोड जारी हो। और यह सब एक boolean के लिए।

दोहरा नकार

हमने माइग्रेशन नहीं लिखा। संग्रहित column हमेशा के लिए अपना पुराना नकारात्मक अर्थ बनाए रखता है, और model को उसके आगे एक सकारात्मक property मिल जाती है:

public bool JitProvisioningEnabled { get; set; }

public bool DisableJitProvisioning
{
    get => !JitProvisioningEnabled;
    set => JitProvisioningEnabled = !value;
}

सकारात्मक property असली वाली है, जिसके पीछे असली स्टोरेज है, और उसका डिफ़ॉल्ट false है, जो नया सुरक्षित डिफ़ॉल्ट है। नकारात्मक नाम अब एक गणित किया हुआ alias है जो दोनों दिशाओं में उलट देता है।

एक पुरानी row का पूरा पीछा करें। column लापता है, इसलिए वह false के रूप में पढ़ा जाता है, इसलिए DisableJitProvisioning का setter false के साथ चलता है, इसलिए JitProvisioningEnabled true हो जाता है। कनेक्शन प्रोविज़निंग जारी रखता है, ठीक वैसे ही जैसे उसके मालिक ने कॉन्फ़िगर किया था, और कुछ भी माइग्रेट नहीं हुआ। अब एक नए कनेक्शन का पूरा पीछा करें। कोई भी दोनों में से कोई property सेट नहीं करता, JitProvisioningEnabled अपने डिफ़ॉल्ट false पर बना रहता है, और कनेक्शन अनजान उपयोगकर्ताओं को तब तक अस्वीकार करता है जब तक कोई खुद इसे चुन न ले।

दोनों व्यवहार एक ही कोड से निकलते हैं, बिना किसी branch के, बिना किसी version flag के, और बिना किसी data को छुए। संग्रहित bit ने कभी अपना अर्थ नहीं बदला। बदला तो सिर्फ़ वह field जिसमें वह आकर टिकता है, और उलटाव उस property setter में होता है जो हर load पर चलता है।

इसकी क़ीमत क्या रही

यह मुफ़्त नहीं है, और बिल API सीमा पर आता है। दोनों properties public हैं, इसलिए दोनों serialize होती हैं, और जो client किसी कनेक्शन को पढ़ता है, कुछ बदलता है, और उसे वापस लिखता है, वह अब दो properties भेज रहा है जो एक ही चीज़ का वर्णन करती हैं। Deserialization उन्हें उसी क्रम में लागू करता है जिस क्रम में वे payload में आती हैं, इसलिए आख़िरी वाली जीतती है। सकारात्मक property को true पर सेट करें और उस object में एक बासी नकारात्मक property छोड़ दें जिसे आपने fetch किया था, और आपका बदलाव चुपचाप एक ऐसे field से पलट दिया जाता है जिसे भेजने का आपने सोचा तक नहीं था।

हमें यह उसी तरह पता चला जैसे ऐसी चीज़ें पता चलती हैं, एक ऐसे test में जिसने flag को चालू किया और फिर जाँचा कि वह चालू है। उससे जो नियम निकला वह यह है कि किसी भी read-modify-write पर दोनों रूपों को स्पष्ट रूप से सेट करें, जो हमारा अपना end-to-end test अब एक टिप्पणी के साथ करता है जो कारण समझाती है। अगर आप यह तरकीब अपनाते हैं, तो इसके लिए गुंजाइश रखें। एक दोतरफ़ा alias आपको एक मुफ़्त माइग्रेशन देता है और बदले में wire पर एक अस्पष्टता वसूल लेता है।

वह बग जो पलटने से उजागर हुआ

यह रहा वह हिस्सा जो booleans से आगे तक लागू होता है। बदलाव करते समय हमें पता चला कि OIDC कनेक्शन बनाने वाले admin endpoint ने इस flag को कभी सेट किया ही नहीं था। ग़लत तरीके से नहीं, ग़लत मान पर नहीं। उसने बस इसे कभी assign ही नहीं किया, और request object में इसे assign करने के लिए कोई field था ही नहीं।

यह तब तक अदृश्य रहा जब तक डिफ़ॉल्ट वही मान था जो हर कोई चाहता था। हर कनेक्शन प्रोविज़निंग करता हुआ निकलता था, और यही वह चीज़ है जो उसे wire करना भूल जाने वाला कोड वैसे भी पैदा करता, इसलिए ध्यान देने को कुछ नहीं था और कोई test नहीं था जो विफल हो सकता। जिस पल डिफ़ॉल्ट पलटा, वही अंतर बन गया "हर नया बना OIDC कनेक्शन प्रोविज़निंग बंद के साथ आता है और उसे चालू करने का कोई तरीका नहीं", जो कोई सूक्ष्म बग बिलकुल नहीं है।

डिफ़ॉल्ट हर उस code path का मान होता है जो field को सेट करना भूल गया। जब तक डिफ़ॉल्ट सुविधाजनक है, वे paths उन paths से अलग नहीं पहचाने जा सकते जिन्होंने इसे जानबूझकर सेट किया। डिफ़ॉल्ट बदलना सिर्फ़ नया व्यवहार नहीं बदलता, यह तस्वीर को उभार देता है, जैसे फ़ोटो डेवलप होकर साफ़ उभरती है: जो कुछ भी चुपचाप डिफ़ॉल्ट पर टिका था वह एक साथ दिखने लगता है, और उसमें से कुछ टूटा हुआ है।

वह checkbox जो हिली ही नहीं

डिफ़ॉल्ट के छिपने की आख़िरी जगह है user interface। हमारे portal में एक checkbox थी जिस पर लिखा था "Disable JIT provisioning", डिफ़ॉल्ट रूप से अनचेक्ड। अब उस पर लिखा है "Enable JIT provisioning", और वह अब भी डिफ़ॉल्ट रूप से अनचेक्ड है। वही widget उसी जगह उसी शुरुआती अवस्था में, और अर्थ ठीक उलटा।

यह सचमुच एक ख़तरनाक किस्म का बदलाव है, इसलिए list view को एक badge मिला। जिस कनेक्शन ने इसे नहीं चुना है वह अब लेबल किया जाता है, ताकि अवस्था बिना कुछ खोले ही दिखे, बजाय इसके कि उसे एक अनचेक्ड box से अनुमान लगाया जाए जो पहले दूसरी चीज़ का मतलब रखता था।

और जब प्रोविज़निंग बंद वाला कोई कनेक्शन किसी अनजान व्यक्ति के लिए assertion पाता है, तो उपयोगकर्ता को किसी stack trace पर नहीं पटका जाता। वे उसी application पर लौट जाते हैं जहाँ से आए थे, एक ऐसे संदेश के साथ जो कहता है कि खाता नहीं मिला और अपने administrator से संपर्क करें, जो अभी-अभी जो हुआ उसका सच्चा और कारगर रूप है।

डिफ़ॉल्ट API सतह हैं, जो चार समूहों को विरासत में मिलते हैं

डिफ़ॉल्ट API सतह हैं। वे उन संग्रहित rows को विरासत में मिलते हैं जो field से पहले की हैं, उन configuration फ़ाइलों को जो इसे छोड़ देती हैं, उन code paths को जो इसे कभी सेट नहीं करतीं, और उन user interface नियंत्रकों को जिनकी अनचेक्ड अवस्था उन्हें encode करती है। किसी एक को हिलाने से पहले, उन चारों समूहों को गिनें और हर एक के लिए तय करें कि उसे नए डिफ़ॉल्ट का पालन करना चाहिए या पुराना व्यवहार बनाए रखना चाहिए। आमतौर पर हर एक के लिए जवाब अलग होता है, और यही डिज़ाइन का काम है।

और अगर आप पाते हैं कि आप किसी boolean को हिलाने के लिए एक data माइग्रेशन लिखने जा रहे हैं, तो पहले देखें कि क्या अर्थ अपनी जगह पर रह सकता है जबकि नाम और डिफ़ॉल्ट उसके आगे खिसक जाएँ। स्टोरेज अपना मन बदलने की महँगी जगह है। property setter सस्ती वाली है।

अगर आप चाहते हैं कि आपका आइडेंटिटी प्रोवाइडर सावधानी से चुने हुए डिफ़ॉल्ट के साथ ही आए, तो Authagonal हर SSO कनेक्शन से प्रोविज़निंग के लिए खुद चुनने को कहता है, और आपको साफ़-साफ़ बताता है कि किन कनेक्शनों ने ऐसा किया है।