المعاملات والأمان في البرامج المخزنة
برنامج مخزن آمن: لا يحفظ نصف عملية، ولا يفتح الجداول لمن لا يحتاجها. ⏱ نحو 40 دقيقة.
- تشرح سلوك «الكل أو لا شيء» في البرامج المخزنة.
- تستخدم
COMMITداخل الإجراء للمعالجة على دفعات. - تكتب دالة
SECURITY DEFINERآمنة. - تمنح صلاحية التنفيذ وتسحبها بـ
GRANTوREVOKE.
١الكل أو لا شيء
كل استدعاء لدالة أو إجراء يعمل داخل معاملة Transaction: إن فشلت خطوة واحدة أُلغيت كل الخطوات قبلها. هذا الإجراء ينقل كمية من منتج إلى آخر، والمنتج 12 لا يملك إلا 3 قطع:
CREATE PROCEDURE move_stock(from_id int, to_id int, n int)LANGUAGE plpgsqlAS $$BEGIN UPDATE products SET stock = stock + n WHERE id = to_id; -- الإضافة أولًا UPDATE products SET stock = stock - n WHERE id = from_id; -- قد تخالف CHECK (stock >= 0)END;$$; DO $$BEGIN CALL move_stock(12, 11, 5);EXCEPTION WHEN check_violation THEN RAISE NOTICE 'فشل النقل: المخزون غير كافٍ';END $$; SELECT id, name, stock FROM products WHERE id IN (11, 12) ORDER BY id;
| id | name | stock |
|---|---|---|
| 11 | مكعبات بناء | 9 |
| 12 | لغز ألف قطعة | 3 |
الخطوة الأولى أضافت 5 قطع إلى المنتج 11، ثم فشلت الثانية. النتيجة: مخزون 11 بقي 9 كما كان. لم يُحفظ نصف العملية.
٢COMMIT داخل الإجراء: المعالجة على دفعات
الدالة لا تستطيع COMMIT، أما الإجراء فيستطيع إذا استُدعي بـ CALL خارج أي معاملة. الفائدة: عملية طويلة على ملايين الصفوف تُحفظ دفعةً دفعة، فلا يضيع كل العمل إن انقطعت في منتصفها، ولا تقفل الجدول طويلًا:
CREATE TABLE archive_log (batch int, rows_moved int); CREATE PROCEDURE archive_in_batches()LANGUAGE plpgsqlAS $$DECLARE n int;BEGIN FOR b IN 1..3 LOOP INSERT INTO archive_log VALUES (b, b * 100); COMMIT; -- الدفعة b حُفظت نهائيًّا END LOOP;END;$$; CALL archive_in_batches();SELECT * FROM archive_log;
| batch | rows_moved |
|---|---|
| 1 | 100 |
| 2 | 200 |
| 3 | 300 |
CREATE FUNCTION f() RETURNS int LANGUAGE plpgsql AS $$BEGIN INSERT INTO categories (name) VALUES ('عروض'); COMMIT; RETURN 1;END; $$;SELECT f();
f()، السطر 4 داخلها (COMMIT)CREATE PROCEDURE p() LANGUAGE plpgsql AS $$BEGIN INSERT INTO categories (name) VALUES ('عروض'); COMMIT;END; $$;CALL p();SELECT count(*) FROM categories;
| count |
|---|
| 6 |
COMMIT للإجراءات فقط.
٣الأمان: صلاحية التنفيذ بدل صلاحية الجداول
تطبيق نقطة البيع يحتاج أن يعرف مخزون المنتج، ولا نريد أن يقرأ جدول المنتجات كاملًا أو يعدّله. الحل: دالة بصلاحيات مالكها (SECURITY DEFINER)، ونعطي التطبيق صلاحية تنفيذها فقط:
CREATE ROLE pos_app; -- دور تطبيق نقطة البيع CREATE FUNCTION stock_of(pid int) RETURNS intLANGUAGE sql STABLESECURITY DEFINER -- تعمل بصلاحيات مالكهاSET search_path = public -- تثبيت المخطط: ممارسة أمانAS $$ SELECT stock FROM products WHERE id = pid $$; REVOKE ALL ON FUNCTION stock_of(int) FROM PUBLIC;GRANT EXECUTE ON FUNCTION stock_of(int) TO pos_app; SET ROLE pos_app; -- نتصرف الآن كالتطبيقSELECT stock_of(1) AS مخزون_الحاسب;
| مخزون_الحاسب |
|---|
| 7 |
الدالة نجحت لأنها تعمل بصلاحيات مالكها.
CREATE ROLE pos_app;SET ROLE pos_app;SELECT * FROM products;
والدور نفسه لا يقرأ الجدول مباشرة.
| المعنى | |
|---|---|
SECURITY INVOKER (الافتراضي) | تعمل بصلاحيات من يستدعيها |
SECURITY DEFINER | تعمل بصلاحيات مالكها؛ أضف معها SET search_path |
GRANT EXECUTE ON FUNCTION f TO r | يسمح للدور r بتنفيذها |
REVOKE ALL ON FUNCTION f FROM PUBLIC | يسحب التنفيذ الممنوح للجميع افتراضيًّا |
لاحظ: الافتراضي معكوس. Oracle و MySQL تعملان بصلاحيات المالك ما لم تقل غير ذلك، و PostgreSQL بصلاحيات المستدعي.
توقّع وتحقّق
فشلت الخطوة الثالثة من إجراء. ماذا يحدث للخطوتين الأولى والثانية؟
أين يُسمح بـ COMMIT؟
SECURITY DEFINER يعني أن البرنامج يعمل بصلاحيات:
الافتراضي في PostgreSQL:
الخلاصة
- فشل خطوة في البرنامج المخزن يُلغي كل خطواته.
COMMITمسموح في الإجراء المستدعى بـCALLفقط، لا في الدالة.SECURITY DEFINER: صلاحيات المالك، معSET search_path.REVOKE…FROM PUBLICثمGRANT EXECUTEللدور المطلوب.
تحدٍّ برمجي: دالة الأسعار لتطبيق الجوال
متقدمتطبيق الجوال يتصل بالقاعدة بالدور mobile_app. نريده أن يعرف سعر المنتج بعد الضريبة فقط، دون أي صلاحية على الجداول.
- أنشئ الدور
mobile_app. - دالة
public_price(pid int)تُرجعround(price * 1.15, 2)للمنتج، بصلاحيات مالكها وSET search_path = public. - اسحب التنفيذ من PUBLIC وامنحه لـ mobile_app.
👀 اعرض الناتج المطلوب
- الكود يعمل بلا أخطاء
- الدالة بصلاحيات مالكها
- mobile_app يحصل على السعر: المنتج 4 ← 86.25
- mobile_app لا يقرأ جدول المنتجات
- سحب الصلاحية من PUBLIC
- تثبيت search_path
💡 تلميح 1
💡 تلميح 2
حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
CREATE ROLE mobile_app; CREATE FUNCTION public_price(pid int) RETURNS numericLANGUAGE sql STABLESECURITY DEFINERSET search_path = publicAS $$ SELECT round(price * 1.15, 2) FROM products WHERE id = pid $$; REVOKE ALL ON FUNCTION public_price(int) FROM PUBLIC;GRANT EXECUTE ON FUNCTION public_price(int) TO mobile_app;