المعمارية English

arc42 §6 — عرض وقت التشغيل: تدفقات البيانات وتدفقات الأمن

الحالة: مسودة v0.1 · التاريخ: 2026-07-20 يرتبط بـ: 00-requirements-baseline.md · 01-quality-attribute-scenarios.md · arc42/03-context-and-containers.md

تغطي هذه الوثيقة سيناريوهات وقت التشغيل الستة التي تحمل المخاطر المعمارية للنظام. أما تدفقات CRUD الروتينية فقد أُهملت عن قصد — إذ لا تُفعِّل أي خاصية جديرة بالاهتمام.

# السيناريو لماذا هو ذو دلالة معمارية
1 المصادقة ودورة حياة الـ token نقطة الدخول إلى حدود الثقة (trust boundary)؛ QAS-SEC-04
2 رفع المستندات وpipeline الاعتماد مدخلات غير موثوقة؛ QAS-SEC-02 · QAS-SEC-03 · QAS-CMP-02
3 الحجز ← الدفع ← الاستشارة خلل «المال قبل الاتفاق» المُسجَّل بوصفه CONF-02؛ QAS-AVL-01 · QAS-AVL-03
4 التقييم ← التصحيح ← إصدار الشهادة سلسلة النزاهة؛ QAS-INT-01 · QAS-INT-02 · QAS-INT-03
5 البحث في الـ marketplace وترتيب الإدراج المميّز QAS-PERF-02 البحث بالعربية؛ العدالة التجارية
6 الوصول إلى البيانات ضمن نطاق المستأجر QAS-SEC-01 — أعلى السيناريوهات أولوية في النظام

Flow 1 — المصادقة ودورة حياة الـ Token#

sequenceDiagram
    autonumber
    participant M as 📱 Mobile Client
    participant ING as Ingress + WAF
    participant API as Qwizin API
    participant G as Google Identity
    participant SMS as SMS Provider
    participant R as Redis
    participant DB as PostgreSQL

    rect rgb(240,240,255)
    Note over M,DB: Registration — phone path
    M->>API: POST /v1/auth/register {phone, name, type}
    API->>API: Validate, rate-limit by IP + phone
    API->>R: Store OTP hash, TTL 5min, attempt counter
    API->>SMS: Send OTP
    Note over API,SMS: ⚠ Toll-fraud control: per-phone,<br/>per-IP, per-country quotas
    M->>API: POST /v1/auth/verify {phone, otp}
    API->>R: Verify hash + increment attempts
    Note over API,R: Lock after N attempts.<br/>Constant-time compare.
    API->>DB: Create user (state: pending_profile)
    API-->>M: {access_token 15m, refresh_token}
    end

    rect rgb(240,255,240)
    Note over M,DB: Registration — Google path
    M->>G: Native Sign-In → ID token
    M->>API: POST /v1/auth/google {id_token}
    API->>G: Fetch JWKS (cached)
    API->>API: ⚠ Verify signature, iss, aud, exp, nonce
    Note over API: NEVER trust client-supplied<br/>profile claims. Server verifies<br/>the token itself.
    API->>DB: Find or create by verified sub
    API-->>M: {access_token, refresh_token}
    end

    rect rgb(255,245,235)
    Note over M,DB: Refresh with reuse detection — QAS-SEC-04
    M->>API: POST /v1/auth/refresh {refresh_token}
    API->>DB: Look up token → family_id, used_at
    alt Token already consumed (replay / theft)
        API->>DB: 🚨 Revoke ENTIRE token family
        API->>API: Emit security event, notify user
        API-->>M: 401 — re-authentication required
    else Token valid
        API->>DB: Mark consumed, issue successor in same family
        API-->>M: {new access_token, new refresh_token}
    end
    end

التدفق: ثلاثة مسارات — التسجيل عبر الهاتف بـ OTP، والتسجيل عبر Google بالتحقق من الـ ID token في الخادم، ثم تجديد الـ token مع كشف إعادة الاستخدام الذي يُبطل عائلة الـ tokens بأكملها.

الضوابط (Controls)#

الضابط المبرّر
مدة access token ≤ 15 دقيقة؛ refresh طويل العمر لكنه متدوِّر يحدّ من نافذة استغلال الـ token المسروق دون إجبار مستخدم الجوال على إعادة تسجيل الدخول باستمرار
كشف إعادة الاستخدام يُبطل العائلة بأكملها تقديم refresh token سبق استهلاكه يعني إما سرقة وإما خللًا في العميل. وكلاهما يستوجب قتل العائلة — وهذا هو أعلى ضوابط المصادقة قيمةً على الإطلاق.
التحقق من Google ID token في جانب الخادم الثغرة الشائعة هي الوثوق بـ email أو sub مُرسَل من العميل. تحقّق من التوقيع ومن الـ audience في الخادم، دائمًا.
OTP: مُجزَّأ (hashed) في التخزين، مقارنة بزمن ثابت، إقفال بعد عدد محاولات، حصص لكل هاتف/IP/دولة نقاط نهاية الـ OTP هدف قياسي لاحتيال الاتصالات (toll fraud) — يستنزف المهاجم ميزانية الـ SMS دون أن ينشئ حسابًا أصلًا
تغيير كلمة المرور / إيقاف الحساب من المسؤول ← إبطال كل العائلات مطلوب لأجل QAS-SEC-04
حسابات المسؤولين: MFA إلزامي يستطيع المسؤول اعتماد الحسابات ورؤية جميع المستأجرين — وهو أعلى الأهداف قيمةً في النظام

ملاحظة على فجوة إطار العمل: يوفّر Laravel Sanctum انتهاء صلاحية الـ tokens لكنه لا يوفّر تدوير رمز التحديث (refresh-token rotation) بشكل أصيل. لذا يجب تنفيذ منظومة التدوير مع كشف إعادة الاستخدام الموضحة أعلاه تنفيذًا صريحًا. هذه فجوة معروفة ومُتحقَّق منها، ولا يصح افتراض أنها تأتي مجانًا.


Flow 2 — رفع المستندات وPipeline الاعتماد#

هذا هو المسار الأساسي للمدخلات غير الموثوقة في النظام. يرفع ACT-CO / ACT-SP / ACT-CON السجلات التجارية والبطاقات الضريبية؛ ويراجعها ACT-ADM.

sequenceDiagram
    autonumber
    participant U as 🏢 Company Admin
    participant API as Qwizin API
    participant OBJ as Object Storage
    participant Q as Queue
    participant SC as Malware Scanner
    participant ADM as 🛡️ Platform Admin
    participant DB as PostgreSQL

    rect rgb(255,240,240)
    Note over U,DB: 🔒 TRUST BOUNDARY — untrusted file enters
    U->>API: POST /v1/onboarding/documents (multipart)
    API->>API: AuthZ: is this user's own org?
    API->>API: Validate size, count, declared type
    API->>API: ⚠ Magic-byte inspection<br/>(NOT extension or client MIME)
    API->>OBJ: PUT → quarantine/ bucket
    Note over OBJ: Quarantine is a SEPARATE bucket.<br/>No read access from admin UI.<br/>Encrypted at rest.
    API->>DB: document(state: uploaded, version: n)
    API->>Q: dispatch ScanDocument
    API-->>U: 202 Accepted — "under review"
    Note over API,U: Async by design: user is not<br/>blocked on the scan (QAS-SEC-03)
    end

    Q->>SC: Scan object
    alt Malware detected
        SC-->>DB: state: rejected_malware
        Note over DB: Admin NEVER sees the file.<br/>Security event raised.
        DB->>U: Notify — resubmit required
    else Clean
        SC->>OBJ: Move quarantine/ → documents/
        SC->>DB: state: pending_review
        DB->>ADM: Enqueue in review queue
    end

    rect rgb(240,240,255)
    Note over ADM,DB: Admin review — QAS-SEC-02
    ADM->>API: GET /v1/admin/documents/{id}
    API->>API: AuthZ: admin role + review permission
    API->>OBJ: Generate signed URL, TTL ≤ 5 min
    API->>DB: 📝 Audit: admin X viewed doc Y at T
    API-->>ADM: {signed_url}
    ADM->>OBJ: GET via signed URL
    Note over ADM,OBJ: Direct fetch. Never proxied<br/>through app domain.<br/>Content-Disposition: attachment
    end

    rect rgb(240,255,240)
    Note over ADM,DB: Decision — QAS-CMP-02
    alt Approved
        ADM->>API: POST /v1/admin/documents/{id}/approve
        API->>DB: BEGIN TX
        API->>DB: document.state = approved
        API->>DB: 📝 Audit: actor, timestamp, prior→new, reason
        API->>DB: Re-evaluate org activation state
        API->>DB: COMMIT
        Note over DB: Org activates ONLY when ALL<br/>required docs approved (CON-04)
        DB->>U: Notify — account active
    else Rejected
        ADM->>API: POST .../reject {reason}
        API->>DB: state = rejected + reason + audit
        DB->>U: Notify with reason → resubmission allowed
        Note over DB: Resubmission creates version n+1.<br/>⚠ NEVER overwrites version n —<br/>the evidence behind the original<br/>decision must survive (QAS-CMP-02)
    end
    end

التدفق: الملف يدخل إلى bucket الحجر (quarantine) أولًا، ولا يُرقَّى إلا بعد فحص البرمجيات الخبيثة؛ ثم يراجعه المسؤول عبر signed URL قصير الأجل، وينتهي القرار إلى اعتماد أو رفض مع إصدار نسخة جديدة عند إعادة التقديم.

ثوابت الـ Pipeline (Pipeline invariants)#

  1. الحجر أولًا. تهبط الملفات في bucket معزول ولا تُرقَّى إلا بعد الفحص. يجب ألا يكون المراجع البشري هو آلية كشف البرمجيات الخبيثة إطلاقًا.
  2. البايتات الأولى (magic bytes) لا الامتدادات. الامتداد .pdf والنوع application/pdf المُعلن من العميل كلاهما تحت سيطرة المهاجم. فحص المحتوى وحده هو الجدير بالثقة.
  3. لا تُقدَّم أبدًا من نطاق التطبيق. روابط موقَّعة (signed URLs) من نطاق التخزين مع Content-Disposition: attachment. تقديم ملفات المستخدمين من نطاق التطبيق يحوّل ملفًا مخزَّنًا إلى XSS مخزَّن ضد جلسة المسؤول — وهي أعلى الجلسات قيمةً في النظام.
  4. مدة صلاحية قصيرة. ≤ 5 دقائق تحدّ من نطاق التعرّض عند تسرّب الرابط (QAS-SEC-02).
  5. نسخ غير قابلة للتغيير. إعادة التقديم تُلحِق نسخة جديدة. سجل تدقيق يشير إلى مستند جرى تعديله لا يُثبت شيئًا.
  6. قبول لا متزامن. يجب ألا يحجب الفحصُ المستخدمَ (QAS-SEC-03).

Flow 3 — الحجز ← الدفع ← الاستشارة#

⚠️ ينفّذ هذا التدفق الخلل المُسجَّل بوصفه CONF-02. يفرض SRC-BRD §10.4 هذا الترتيب: حجز ← دفع ← موافقة المستشار. أي أن المال يُحصَّل قبل أن يوافق المستشار. والتصميم أدناه يحتوي هذا الخلل بدلًا من تمريره.

sequenceDiagram
    autonumber
    participant C as 👤 Customer
    participant API as Qwizin API
    participant PAY as 💳 Payment Gateway
    participant CON as 🎓 Consultant
    participant VID as 🎥 Video Provider
    participant SCH as Scheduler
    participant Q as Queue

    rect rgb(240,240,255)
    Note over C,PAY: Booking + payment AUTHORIZATION (not capture)
    C->>API: POST /v1/bookings {consultant, slot, type}
    API->>API: Validate slot free, check consultant active
    API->>API: 🔒 Pessimistic lock on slot
    API->>API: booking(state: pending_payment), slot held
    API-->>C: {booking_id, amount, idempotency_key}

    C->>API: POST /v1/bookings/{id}/pay {idempotency_key}
    API->>PAY: AUTHORIZE (hold) — not capture
    Note over API,PAY: ⚠ AUTHORIZE ONLY.<br/>Capture happens after the<br/>consultant accepts. This is the<br/>fix for CONF-02.
    PAY-->>API: {auth_id, status}
    API->>API: booking(state: awaiting_consultant)
    API->>CON: 🔔 Booking request — respond within SLA
    end

    rect rgb(255,245,235)
    Note over CON,PAY: Consultant decision — bounded by timer
    alt Consultant accepts within SLA
        CON->>API: POST /v1/bookings/{id}/accept
        API->>PAY: CAPTURE {auth_id}
        PAY-->>API: captured
        API->>API: booking(state: confirmed)
        API->>Q: Schedule reminders (T-24h, T-1h)
        API->>C: 🔔 Confirmed
    else Consultant rejects
        CON->>API: POST /v1/bookings/{id}/reject
        API->>PAY: VOID {auth_id}
        Note over API,PAY: Void, not refund — no funds<br/>ever moved. Cleaner for the<br/>customer and cheaper in fees.
        API->>API: booking(state: rejected), release slot
        API->>C: 🔔 Declined — no charge
    else SLA expires (no response)
        SCH->>API: Expire booking
        API->>PAY: VOID {auth_id}
        API->>API: booking(state: expired), release slot
        API->>C: 🔔 Expired — no charge
        Note over API: ⚠ Slot MUST be released.<br/>Consultant non-response must not<br/>silently consume inventory.
    end
    end

    rect rgb(240,255,240)
    Note over C,VID: Consultation — QAS-AVL-01
    SCH->>API: T-5min: provision session
    API->>VID: createRoom(booking_id) [via port]
    VID-->>API: {room_ref}
    C->>API: POST /v1/bookings/{id}/join
    API->>API: AuthZ: participant + within window
    API->>VID: issueToken(room, identity, role)
    API-->>C: {join_token, room_ref}
    C-->>VID: 🎥 WebRTC media — DIRECT, bypasses API
    Note over C,VID: Media never traverses the API tier.<br/>API controls session lifecycle only.
    VID->>API: webhook: participant_joined / left / ended
    API->>API: Record actual duration + outcome
    end

    rect rgb(255,240,240)
    Note over C,PAY: Failure path
    alt Session fails to establish
        API->>API: Detect: no participants joined in window
        API->>API: Flag for resolution
        API->>PAY: REFUND (policy-driven)
        Note over API,PAY: ⚠ Policy undefined in sources.<br/>Who bears a failed session?<br/>→ OQ-05
    end
    end

التدفق: الحجز يُنشئ حجزًا على المبلغ (authorize) فقط؛ ولا يقع التحصيل (capture) إلا عند قبول المستشار، بينما الرفض أو انتهاء مهلة الـ SLA يؤدي إلى إلغاء الحجز (void) وتحرير الموعد.

معالجة CONF-02#

المشكلة في SRC-BRD §10.4 استجابة التصميم
يُخصم من العميل قبل أن يوافق المستشار حجز على المبلغ (authorize)، والتحصيل (capture) عند القبول فقط. لا تُؤخذ الأموال أبدًا مقابل خدمة لم يُتفق عليها.
لا سلوك محدَّد عند رفض المستشار إلغاء الحجز (void). لا حاجة لاسترداد — فلا مال تحرّك أصلًا.
لا سلوك محدَّد عند عدم استجابة المستشار مؤقّت انتهاء صلاحية الحجز + void تلقائي + تحرير الموعد. بدون ذلك، يستهلك المستشار غير المستجيب مخزونه بصمت ويحتجز أموال العميل.
لا مسار نزاع لجلسة فاشلة مُسجَّل بوصفه OQ-05 — وهو قرار سياسة أعمال، لا قرار معماري. المعمارية توفّر الآلية؛ وعلى الجهة الراعية أن تضع السياسة.

متطلب على البوابة ناتج عن هذا التدفق: يجب أن يدعم مزوّد الدفع الفصل بين authorize/capture وأن يدعم عمليات الـ void. وليست كل بوابات الدفع في السعودية تدعم ذلك، خصوصًا لـ mada. وهذا الآن معيار اختيار صارم يغذّي OQ-01.

Idempotency#

كل استدعاء يُغيّر حالة الدفع يحمل مفتاح idempotency مُرسَلًا من العميل (QAS-AVL-03). أما الـ webhooks الواردة من البوابة فيُتحقَّق من توقيعها وتُعالَج بخاصية تكرارية آمنة (idempotency) — فالبوابات تعيد المحاولة، ومعالجة عملية capture مرتين تعني خصمًا مزدوجًا.


Flow 4 — التقييم ← التصحيح ← إصدار الشهادة#

سلسلة النزاهة. الشهادات تُقرّ بكفاءة في سلامة الغذاء، ولذلك يجب أن تكون كل حلقة فيها مقاومة للعبث.

sequenceDiagram
    autonumber
    participant E as 👷 Employee
    participant API as Qwizin API
    participant DB as PostgreSQL
    participant Q as Queue
    participant PDF as Document Renderer
    participant OBJ as Object Storage
    participant V as 🔍 Public Verifier

    rect rgb(240,240,255)
    Note over E,DB: Delivery — QAS-INT-01
    E->>API: POST /v1/assessments/{id}/start
    API->>API: AuthZ: assigned + attempts remaining
    API->>DB: attempt(state: in_progress, started_at)
    API->>DB: Fetch questions
    API->>API: ⚠ STRIP all correctness indicators
    API-->>E: {questions, options} — NO answer key
    Note over API,E: 🚨 The correct answer NEVER<br/>reaches the client in any form:<br/>not a flag, not ordering, not<br/>a hash, not in metadata.<br/>Contract-tested in CI.
    end

    rect rgb(240,255,240)
    Note over E,DB: Submission & server-side grading — QAS-INT-02
    E->>API: POST /v1/assessments/{id}/submit {answers}
    API->>API: Validate attempt open + not expired
    API->>DB: BEGIN TX
    API->>DB: Load answer key (server-side only)
    API->>API: 🧮 Grade — score computed here, never received
    API->>DB: Load passing_score (admin-configurable, CON-08)
    API->>DB: attempt(answers, score, passed) — APPEND-ONLY
    Note over DB: Every attempt persisted immutably.<br/>Client-submitted scores are<br/>never trusted or even accepted.
    alt Passed
        API->>DB: certificate(state: pending, attempt_id) 🔗
        Note over DB: Certificate transactionally bound<br/>to a specific graded attempt
        API->>Q: dispatch GenerateCertificate
    end
    API->>DB: COMMIT
    API-->>E: {score, passed} — result never lost even if<br/>PDF generation later fails (QAS-AVL-04)
    end

    rect rgb(255,245,235)
    Note over Q,OBJ: Async generation — QAS-SCL-03, QAS-LOC-01
    Q->>PDF: Render {holder, competency, date, cert_id, QR}
    Note over PDF: ⚠ Arabic: contextual glyph joining,<br/>bidi layout, embedded fonts.<br/>Golden-image tested.
    PDF->>PDF: Embed QR → verification URL
    PDF->>PDF: Apply cryptographic signature
    PDF->>OBJ: Store immutably
    PDF->>DB: certificate(state: issued, hash)
    DB->>E: 🔔 Certificate ready
    end

    rect rgb(240,240,255)
    Note over V,DB: Third-party verification — QAS-INT-03
    V->>API: GET /verify/{cert_id} (public, unauthenticated)
    API->>DB: Look up by unguessable ID
    API->>API: Evaluate CURRENT validity:<br/>revoked? company suspended? expired?
    API-->>V: {holder, competency, issued, status}
    Note over API,V: Minimal disclosure — enough to<br/>verify, not enough to enumerate<br/>or harvest personal data.<br/>Rate-limited.
    end

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

ضوابط النزاهة#

التهديد الضابط
حصاد مفتاح الإجابات من استجابة الـ API تُنزع مؤشرات الصحة في الخادم؛ واختبار عقد (contract test) في CI يؤكد غيابها من مخطط التسليم
العميل يُرسل درجة مزوَّرة الدرجة تُحسب في الخادم من الإجابات؛ ولا وجود أصلًا لحقل درجة مُرسَل من العميل في الـ API
إعادة المحاولة حتى النجاح ثم إظهار النجاح فقط كل المحاولات تُضاف بأسلوب append-only وتُحفظ؛ وحدود المحاولات قابلة للضبط
تزوير شهادة في محرّر رسوميات QR ← نقطة نهاية تحقق عامة؛ توقيع تشفيري؛ معرّفات يتعذّر تخمينها
بقاء الشهادة «صالحة» بعد إبطالها التحقق يُعيد الحالة الحالية محسوبةً لحظيًا، لا لقطة مخبوزة داخل ملف الـ PDF
تعداد الشهادات لحصاد الأسماء معرّفات غير تسلسلية يتعذّر تخمينها؛ تحديد معدل الطلبات؛ حمولة استجابة أدنى ما يمكن

اعتماد مفتوح: OQ-07 — عند إيقاف شركة، هل تبقى شهادات موظفيها الصادرة صالحة؟ نقطة نهاية التحقق تُعيد الحالة الحالية، فلا بد أن تملك جوابًا. هذا سؤال سياسة أعمال ذو أثر تقني ظاهر مباشرةً، ولهذا يحتاج قرارًا من الجهة الراعية.


Flow 5 — البحث في الـ Marketplace وترتيب الإدراج المميّز#

sequenceDiagram
    autonumber
    participant U as 👤 User
    participant API as Qwizin API
    participant SRCH as Search Engine
    participant DB as PostgreSQL
    participant Q as Queue

    rect rgb(240,255,240)
    Note over DB,SRCH: Indexing pipeline — async
    DB->>Q: Provider created / updated / subscription changed
    Q->>Q: ⚠ Debounce: subscription state changes<br/>are frequent and bursty
    Q->>SRCH: Upsert document
    Note over SRCH: Indexed: name, description,<br/>categories, city, ar+en variants,<br/>subscription_tier, is_featured
    end

    rect rgb(240,240,255)
    Note over U,SRCH: Query — QAS-PERF-02
    U->>API: GET /v1/marketplace?q=مورد أغذية&city=..&cat=..
    API->>API: Normalise Arabic:<br/>أإآ→ا · ة→ه · ى→ي · strip diacritics · strip tatweel
    API->>SRCH: Query with filters + featured boost
    SRCH->>SRCH: Relevance score
    SRCH->>SRCH: Apply featured multiplier
    Note over SRCH: ⚠ Boost is a MULTIPLIER on<br/>relevance, not an override.<br/>An irrelevant featured provider<br/>must NOT outrank a relevant one.
    SRCH-->>API: Ranked IDs
    API->>DB: Hydrate current data by ID
    Note over API,DB: Index is for ranking only.<br/>Authoritative data from DB —<br/>a stale index must never show<br/>a suspended provider as active.
    API->>API: Filter: active + approved + subscription current
    API-->>U: Results + featured badge
    end

    rect rgb(255,240,240)
    alt Search engine unavailable — QAS-AVL-04
        API->>DB: Fallback: DB-backed filtered listing
        API-->>U: Degraded results, not an error page
    end
    end

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

ملاحظات التصميم (Design notes)#

  • تطبيع النص العربي إلزامي لا اختياري. المستخدمون السعوديون يكتبون أ/ا/إ بالتبادل ونادرًا ما يستخدمون التشكيل. وبدون هذا الطي (folding) ينهار الاسترجاع ويبدو الدليل فارغًا — والفشل هنا صامت وكامل. هذا يقيّد اختيار محرك البحث، وهو سيناريو (H,H).
  • فهرسة ثنائية اللغة. تُفهرس المتغيرات العربية والإنجليزية كحقول محلَّلة منفصلة كي يُطابق الاستعلام بأي من اللغتين (OQ-12).
  • تضخيم الإدراج المميّز محدود. يشترط SRC-MOM REQ-06 أن يتصدّر المزوّدون المميّزون غيرهم. ويُنفَّذ ذلك بوصفه معاملًا ضاربًا في درجة الملاءمة: فهو يُعيد ترتيب النتائج المتقاربة، ولا يُظهر نتائج غير ملائمة. التضخيم غير المحدود يدمّر فائدة البحث، ويدمّر في النهاية قيمة المنتج المميّز نفسه.
  • الفهرس ليس مصدرًا موثوقًا أبدًا. الترشيح اللاحق مقابل قاعدة البيانات يمنع فهرسًا قديمًا من كشف مزوّد موقوف أو منتهي الاشتراك — وهو خطر تجاري وامتثالي، لا مجرد خطأ في الصحة.
  • فهرسة مُهدَّأة (debounced). تغيّرات حالة الاشتراك تأتي على دفعات (تجديدات، انتهاءات)؛ وفهرستها بشكل متزامن ستُرهق المحرك.

Flow 6 — الوصول إلى البيانات ضمن نطاق المستأجر ⭐#

QAS-SEC-01 هو أعلى السيناريوهات أولوية في النظام. ويصف هذا التدفق الآلية، لأن الآلية هي ما يمكن اختباره.

flowchart TB
    REQ["Incoming authenticated request"] --> AUTH["Authenticate<br/>→ resolve user + org context"]
    AUTH --> CTX["Bind tenant context<br/>to the request scope"]
    CTX --> POL["Policy / Gate check<br/><i>coarse-grained capability</i>"]
    POL --> QRY["Repository / query executed"]

    QRY --> SCOPE{"Global tenant scope<br/>auto-applied?"}
    SCOPE -->|"Yes — default"| SAFE["✅ Query constrained<br/>to tenant"]
    SCOPE -->|"Explicitly bypassed"| GUARD{"Bypass on the<br/>allow-list?<br/><i>admin / system context</i>"}
    GUARD -->|Yes| AUDIT["✅ Permitted<br/>📝 audit-logged"]
    GUARD -->|No| FAIL["🚨 FAIL CLOSED<br/>Exception + alert"]

    SAFE --> RESP["Response"]
    AUDIT --> RESP
    FAIL --> ERR["500 + security event<br/><i>never silently unscoped</i>"]

    style FAIL fill:#c94a4a,stroke:#8b2f2f,color:#fff
    style ERR fill:#c94a4a,stroke:#8b2f2f,color:#fff
    style SAFE fill:#2d8659,stroke:#1c5638,color:#fff
    style AUDIT fill:#2d8659,stroke:#1c5638,color:#fff

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

لماذا يقع الإنفاذ في طبقة الاستمرارية#

التصميم المُغري هو التخويل لكل نقطة نهاية على حدة: كل controller يفحص $employee->company_id === $user->company_id. وهذا يفشل فشلًا متوقّعًا، لسبب بنيوي — فهو صحيح فقط إذا تذكّره كل مطوّر في كل نقطة نهاية، إلى الأبد. وأول فحص منسي هو اختراق عابر للمستأجرين، وسيُنسى، لأن لا شيء يفرضه.

بدلًا من ذلك:

خمس طبقات، مرتّبة بحسب مدى تأخر كل منها في التقاط الخطأ. الطبقتان 1 و5 هما اللتان تصمدان أمام دوران الموظفين؛ أما 2–4 فهي تسهيلات تجعل الفعل الصحيح سهلًا.

# الطبقة المسؤولية
1 PostgreSQL Row-Level Security خط الدفاع الأخير. سياسات RLS على كل جدول ضمن نطاق المستأجر تُرشِّح استنادًا إلى متغيّر جلسة. وهذه الطبقة الوحيدة التي ترفض عند الشك (fail closed) بغض النظر عن أخطاء التطبيق — فنطاق منسي، أو استعلام DB::table() خام، أو استدعاء withoutGlobalScopes()، يُعيد صفر صفوف بدلًا من بيانات شركة أخرى.
2 TenantContext الذي يرمي استثناءً كائن واحد قابل للحقن يحمل الشركة الحالية. وقراءته وهو غير مضبوط تُثير استثناءً — فهو لا يُعيد null أبدًا ولا يتحوّل تلقائيًا إلى «بلا ترشيح». قابلية العدم (nullability) هنا هي الطريق الذي يُشحن عبره WHERE company_id IS NULL الذي لا يطابق شيئًا (أو شرط مُسقَط بالكامل).
3 Eloquent global scope راحة وأداء استعلامي معًا — فالـ RLS يُرشِّح، لكن الـ scope يتيح لمخطِّط الاستعلام استخدام فهرس company_id. وليس حدًا أمنيًا. ومعاملته على أنه كذلك هو الخطأ الكلاسيكي.
4 اختبار معماري يؤكد أن كل model يحوي جدوله عمود company_id يستخدم trait تحديد النطاق. يلتقط الـ model الذي يضيفه أحدهم العام القادم — أي في اللحظة التي تتآكل فيها الحدود بالضبط.
5 حزمة CI عدائية تُهيّئ شركتين ببيانات متطابقة المظهر، وتُصادِق باسم A، وتُعدِّد كل نقطة نهاية مستخدمةً معرّفات B، وتؤكد 403/404 وأن أي جسم استجابة لا يحوي معرّفات B. أي نقطة نهاية جديدة بلا تغطية ← يفشل البناء.

⚠ نمطا فشل في RLS يجب تصميم الحل لهما الآن#

كلاهما حالتان تكون فيهما السياسة مكتوبة بشكل صحيح ومع ذلك تتسرّب البيانات:

  1. إعادة استخدام الاتصال. مع تجميع الاتصالات (connection pooling) في وضع المعاملات — أو مع العمّال الدائمين تحت Octane — يمكن لمتغيّر جلسة ضُبط لطلب واحد أن يبقى إلى الطلب التالي، فيُقدِّم بيانات مستأجر تحت سياق مستأجر آخر. التخفيف: اضبط المتغيّر بدلالات محصورة في المعاملة داخل معاملة صريحة، وأعِد ضبطه في middleware الإنهاء. هذا هو ناقل التسريب الفعلي، لا السياسة.
  2. المهام في الطوابير بلا طلب. المهمة الخلفية تعمل بلا سياق مستأجر، فيبقى متغيّر الجلسة غير مضبوط. كل مهمة ضمن نطاق مستأجر يجب أن تُعيد تأسيس السياق من معرّف محمول في حمولتها — والمهمة التي تفشل في ضبطه يجب أن ترمي استثناءً، لا أن تتحوّل إلى قيمة افتراضية.

هذان السببان هما لماذا يكون RLS ضروريًا لكن غير كافٍ، ولماذا تختبر الطبقة 5 السلوك القابل للملاحظة بدل التفاصيل الداخلية.

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

الفائدة الامتثالية#

العزل المفروض على مستوى قاعدة البيانات أسهل دفاعًا أمام الجهة التنظيمية بفارق ملموس عن العزل المبني على عُرف داخل التطبيق. و«الإخفاق في تطبيق الضمانات التقنية والتنظيمية» من بين الفئات الواردة في نشاط الإنفاذ المُعلن لدى SDAIA — و«لدينا سياسات RLS في طبقة قاعدة البيانات» يُوثَّق في التدقيق أفضل بكثير من «لدينا trait يُفترض بالمطورين استخدامه».

سلوك التتالي (Cascade)#

عند إيقاف شركة أو رفضها، يجب أن ينتشر التغيير إلى وصول الموظفين، والتقييمات الجارية، والشهادات الصادرة. OQ-07 لا يزال دون حسم — لكن الآلية أعلاه تضمن أن نقطة الانتشار مكان واحد (تحديد سياق المستأجر)، لا نقاط متناثرة عبر كل module.


اهتمامات وقت التشغيل الشاملة#

الارتباط وقابلية التتبّع (QAS-OPS-01)#

يُسنَد لكل طلب معرّف ارتباط (correlation ID) عند الـ ingress ينتشر عبر الـ API، وإلى المهام في الطوابير، وإلى استدعاءات الـ sidecar، وإلى استدعاءات المزوّدين الخارجيين، وإلى كل سطر سجل. وبدون ذلك، فإن تتبّع سلسلة لا متزامنة — رفع ← فحص ← مراجعة ← اعتماد ← إشعار — يعني مطابقة الطوابع الزمنية يدويًا عبر أربعة مكوّنات في الساعة 02:00.

الحدود اللامتزامنة#

متزامن (في مسار الطلب) لا متزامن (في الطوابير)
المصادقة والتخويل فحص البرمجيات الخبيثة
حجز موعد الحجز توليد الشهادات/ملفات الـ PDF
حجز مبلغ الدفع (authorize) الإشعارات (بريد/SMS/push)
تصحيح التقييم فهرسة البحث
استعلام البحث تسوية المدفوعات (reconciliation)
كتابة بيانات المستند الوصفية معالجة الـ webhooks

القاعدة: لا يقع أي اعتماد غير حَرِج في المسار المتزامن لتدفق حَرِج (QAS-AVL-04). التصحيح متزامن لأن المستخدم ينتظر النتيجة ولأنه رخيص؛ وتوليد الشهادة لا متزامن لأنه مكلف ولأن النتيجة مُخزَّنة بأمان بالفعل.

Idempotency#

كل التغييرات المُطلَقة خارجيًا — استدعاءات الدفع من العميل، وwebhooks البوابة، وwebhooks مزوّد الفيديو — تكون idempotent بالمفتاح. الأنظمة الخارجية تعيد المحاولة؛ والمعالجة غير الـ idempotent تعني خصمًا مزدوجًا وحجوزات مكرّرة.


البنود المفتوحة المؤثرة على سلوك وقت التشغيل#

ID السؤال التدفق المتأثر
OQ-01 يجب أن تدعم البوابة الفصل بين authorize/capture/void Flow 3 — معيار اختيار صارم
OQ-03 نموذج إتاحة أوقات المستشار Flow 3 — توليد المواعيد
OQ-05 سياسة الاسترداد للجلسات الفاشلة وSLA عدم استجابة المستشار Flow 3
OQ-07 تتالي إيقاف الشركة إلى الشهادات Flows 4، 6
OQ-08 تسجيل الاستشارات Flow 3 — يضيف مسار إخراج (egress) واحتفاظ
OQ-09 تأكيد قابلية التحقق من الشهادات Flow 4
OQ-12 نطاق المحتوى ثنائي اللغة Flow 5 — تصميم حقول الفهرس
القائد التقني · أمير هارون مسودة v0.1 · بحث وتصميم فقط