← All posts

أوقفنا إعدادًا افتراضيًا خطيرًا دون ترحيل صف واحد

Authagonal·July 29, 2026

التزويد الآني (Just-in-time provisioning) هو الميزة التي تجعل الدخول الموحّد للمؤسسات يبدو كالسحر. يسجّل موظف جديد دخوله عبر موفّر الهوية الخاص بشركته، ولا يوجد له حساب في تطبيقك بعد، فيُنشأ له حساب على الفور انطلاقًا من التأكيد (assertion). لا أحد يفتح تذكرة، ولا أحد يُدعى، بل يباشر الشخص عمله ببساطة.

وهي أيضًا، إذا قرأناها من الاتجاه الآخر، ميزة تتيح لمن يتحكم في موفّر الهوية ذاك أن ينشئ حسابات داخل مستأجر عميلك بمجرد التأكيد على أن شخصًا ما موجود. وهذا لا بأس به حين يكون الاتصال محصورًا بإحكام في دليل شركة واحدة، وكل إنسان فيه يُفترض أن يحصل على الوصول. لكنه أقل قبولًا حين يكون الاتصال دليلًا مشتركًا، أو مستأجرًا لمقاولين، أو أحد تلك الاتحادات المترامية حيث تكون مجموعة الأشخاص الذين يستعد موفّر الهوية للشهادة لهم أكبر بكثير من مجموعة الأشخاص الذين قصد عميلك السماح لهم بالدخول.

كان إعدادنا الافتراضي مفعّلًا. لا لأن أحدًا قرّر أن يكون كذلك، وهذا هو الجزء الجدير بالتوقف عنده. صار مفعّلًا افتراضيًا لأنه حين أُضيف الحقل، كان المتغيّر المنطقي الذي يعبّر عنه يحمل الاسم DisableJitProvisioning، والمتغيّر المنطقي غير المضبوط قيمته false، وقيمة false تعني "لا تُعطّل". والقراءة الأكثر أمانًا لإعداد افتراضي لم يختره أحد هي أنه محض مصادفة، وهذه المصادفة كانت قد ترسّخت بوصفها سلوكًا.

ولكي نكون دقيقين بشأن حجم الانكشاف، لأنه لم يكن يومًا بسوء "أي أحد يستطيع إنشاء أي أحد": كانت هناك بوابتان تعملان بالفعل قبل التزويد. فالاتصال قد يحمل قائمة بنطاقات البريد الإلكتروني المسموح بها، وأي تأكيد خارجها يُرفض. والاتصال قد يشترط سمة دعوة، وأي مستخدم غير مدعوّ يُرفض. أما الخطر الحقيقي في حالة التفعيل الافتراضي فكان اتصالًا لم يُضبط فيه أي من هذين، وهو تحديدًا شكل الاتصال الذي يعدّه أحدهم على عجل لتشغيل الدخول الموحّد.

قلب الإعداد الافتراضي كلمة واحدة. أما قلبه بأمان فليس كذلك.

التغيير الذي يتخيّله الجميع هو إعادة تسمية الحقل إلى JitProvisioningEnabled وجعل قيمته الافتراضية false. تصبح الاتصالات الجديدة آمنة افتراضيًا، وانتهى الأمر.

لكن هذا الحقل مُخزَّن، وهناك اتصالات في التخزين كُتبت قبل وجوده. صفوفها لا تحتوي على العمود إطلاقًا. وما يحدث لها يتوقف كليًا على الاتجاه الذي يشير إليه المتغيّر المنطقي، لأن العمود المفقود يُفكَّك ترميزه إلى false في كلتا الحالتين. في ظل الاسم السلبي القديم، الغياب يعني "غير مُعطَّل" فيستمر التزويد. وفي ظل اسم إيجابي جديد، الغياب يعني "غير مُفعَّل" فيتوقف التزويد.

وهكذا فإن إعادة تسمية مباشرة تُطفئ بصمت التزويد الآني لكل اتصال ضبطه العميل حين كان مفعّلًا. لم يختاروا شيئًا، ولم يُخبَروا، وأول ما يبلغهم عنه موظف لا يستطيع تسجيل الدخول، في أي ساعة يقع فيها ذلك. هذا ليس تحسينًا أمنيًا، بل هو انقطاع يأتي مع النشر.

الجواب البديهي هو التعبئة الرجعية (backfill): المرور على كل اتصال مخزَّن، وكتابة العمود صراحةً، ثم قلب الإعداد الافتراضي. إنه ينجح، لكنه ترحيل عليك أن تكتبه وتختبره وتشغّله على تخزين كل مستأجر، وأن تتأكد من اكتماله في كل مكان قبل أن تُشحن الشيفرة التي تعتمد عليه. من أجل قيمة منطقية واحدة.

النفي المزدوج

لم نكتب الترحيل. العمود المخزَّن يحتفظ بمعناه السلبي القديم إلى الأبد، ويكتسب النموذج خاصية إيجابية أمامه:

public bool JitProvisioningEnabled { get; set; }

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

الخاصية الإيجابية هي الحقيقية، ولها تخزين فعلي خلفها، وقيمتها الافتراضية false، وهو الإعداد الافتراضي الآمن الجديد. أما الاسم السلبي فصار الآن اسمًا بديلًا محسوبًا يعكس القيمة في الاتجاهين.

تتبّع صفًا قديمًا حتى النهاية. العمود مفقود، فيُقرأ على أنه false، فتعمل دالة الضبط الخاصة بـ DisableJitProvisioning بقيمة false، فتصبح JitProvisioningEnabled تساوي true. يواصل الاتصال التزويد، تمامًا كما ضبطه مالكه، ولم يُرحَّل أي شيء. والآن تتبّع اتصالًا جديدًا حتى النهاية. لا أحد يضبط أيًا من الخاصيتين، فتبقى JitProvisioningEnabled على قيمتها الافتراضية false، ويرفض الاتصال المستخدمين المجهولين حتى يختار أحدهم التفعيل.

يخرج السلوكان من الشيفرة نفسها دون تفرّع، ودون علم إصدار، ودون المساس بأي بيانات. البِت المخزَّن لم يغيّر معناه قط. الذي تغيّر هو الحقل الذي يستقر فيه فقط، ويحدث العكس داخل دالة ضبط خاصية تعمل عند كل تحميل.

ما كلّفه هذا

هذا ليس مجانيًا، والفاتورة تصل عند حدود الـ API. كلتا الخاصيتين عامّتان، فتُسلسَلان معًا، وأي عميل يقرأ اتصالًا ويغيّر شيئًا ثم يعيد كتابته صار الآن يرسل خاصيتين تصفان الشيء نفسه. ويطبّق فكّ التسلسل الخاصيتين بالترتيب الذي تظهران به في الحمولة، فتفوز الأخيرة. اضبط الخاصية الإيجابية على true مع ترك خاصية سلبية قديمة في الكائن الذي جلبته، فيُلغى تغييرك بصمت بفعل حقل لم تظن أنك ترسله.

اكتشفنا ذلك بالطريقة التي تُكتشَف بها مثل هذه الأمور، في اختبار فعّل الراية ثم تحقّق من أنها مفعّلة. والقاعدة التي خرجت منه هي ضبط الصيغتين معًا صراحةً في أي عملية قراءة وتعديل وكتابة، وهو ما يفعله اختبارنا الشامل الآن مع تعليق يشرح السبب. إن تبنّيت هذه الحيلة، فاحسب لها حسابًا. الاسم البديل ثنائي الاتجاه يمنحك ترحيلًا مجانيًا ويحمّلك التباسًا على السلك.

الخلل الذي كشفه تبديل الإعداد الافتراضي

وهنا الجزء الذي يتجاوز نطاق القيم المنطقية. أثناء إجراء التغيير، اكتشفنا أن نقطة النهاية الإدارية الخاصة بإنشاء اتصال OIDC لم تكن قد ضبطت هذه الراية إطلاقًا. لا بشكل خاطئ، ولا على قيمة خاطئة. هي ببساطة لم تُسندها قط، ولم يكن لكائن الطلب حقل تُسندها إليه.

ظل ذلك خفيًا ما دام الإعداد الافتراضي هو القيمة التي يريدها الجميع. كان كل اتصال يخرج وهو يزوّد، وهو ما كانت الشيفرة التي نسيت توصيله ستنتجه على أي حال، فلم يكن هناك ما يُلاحَظ ولا اختبار يمكن أن يفشل. وفي اللحظة التي انقلب فيها الإعداد الافتراضي، صارت الثغرة نفسها "كل اتصال OIDC جديد يُنشأ بتزويد مُطفأ ولا سبيل لتشغيله"، وهذا ليس خللًا خفيًا على الإطلاق.

الإعداد الافتراضي هو قيمة كل مسار شيفرة نسي ضبط الحقل. وما دام الإعداد الافتراضي مريحًا، فإن تلك المسارات لا تتمايز عن المسارات التي ضبطته عن قصد. تغيير الإعداد الافتراضي لا يغيّر السلوك الجديد فحسب، بل يُظهِر الصورة كما تُحمَّض الصورة الفوتوغرافية: كل ما كان يعتمد بصمت على الإعداد الافتراضي يصبح مرئيًا دفعةً واحدة، وبعضه معطوب.

مربع الاختيار الذي لم يتحرك

آخر مكان يختبئ فيه الإعداد الافتراضي هو واجهة المستخدم. كان في بوابتنا مربع اختيار مكتوب عليه "تعطيل التزويد الآني"، غير محدَّد افتراضيًا. وصار الآن مكتوبًا عليه "تفعيل التزويد الآني"، وما زال غير محدَّد افتراضيًا. الأداة نفسها في الموضع نفسه بالحالة الابتدائية نفسها، والمعنى معاكس.

هذا نوع من التغيير خطير حقًا، لذا أضفنا شارة إلى عرض القائمة. فأي اتصال لم يُفعَّل صار موسومًا الآن، حتى تكون الحالة مرئية دون فتح أي شيء، بدلًا من استنتاجها من مربع غير محدَّد كان يعني الشيء الآخر.

وحين يستقبل اتصال بتزويد مُطفأ تأكيدًا لشخص مجهول، لا يُلقى المستخدم على أثر تتبّع الأخطاء (stack trace). بل يعود إلى التطبيق الذي جاء منه برسالة تقول إن الحساب لم يُعثَر عليه وإن عليه الاتصال بمسؤوله، وهي النسخة الصادقة والقابلة للتنفيذ لما حدث للتو.

الإعدادات الافتراضية جزء من واجهة الـ API، ترثها أربع فئات

الإعدادات الافتراضية جزء من واجهة الـ API. ترثها الصفوف المخزَّنة التي تسبق وجود الحقل، وملفات الضبط التي تغفله، ومسارات الشيفرة التي لا تضبطه أبدًا، وعناصر واجهة المستخدم التي تُرمّزه حالتها غير المحدَّدة. قبل أن تحرّك واحدًا منها، أحصِ تلك الفئات الأربع وقرّر، لكل منها، هل ينبغي أن تتبع الإعداد الافتراضي الجديد أم أن تحتفظ بالسلوك القديم. غالبًا ما يكون الجواب مختلفًا لكل فئة، وذلك هو العمل التصميمي.

وإذا وجدت نفسك على وشك كتابة ترحيل بيانات لتحريك قيمة منطقية، فانظر أولًا إن كان المعنى يستطيع البقاء في مكانه بينما يتحرك الاسم والإعداد الافتراضي أمامه. التخزين هو المكان المكلف لتغيير رأيك. أما دالة ضبط الخاصية فهي المكان الرخيص.

إن كنت تفضّل أن يأتي موفّر الهوية لديك بالإعدادات الافتراضية المدروسة وقد اختِيرت مسبقًا، فإن Authagonal يجعل كل اتصال دخول موحّد يختار التزويد صراحةً، ويخبرك بوضوح أي الاتصالات فعلت ذلك.