الصلاحيات
ما يُسمح للمفتاح بفعله.
المفردات
مجموعة مغلقة، بصيغة resource:action. صغيرة بما يكفي لعرضها على إنسان في قائمة مربعات اختيار، وثابتة بما يكفي ليظل المنح المخزَّن يعني الشيء نفسه بعد عام. والمفردات نفسها هي ما يُكتب به دور مساحة العمل وما يحكم أدوات MCP، فالعميل المقصور على القراءة لا يستطيع حتى رؤية أداة إرسال. أبجدية واحدة، وثلاثة أسطح.
| الصلاحية | تمنح |
|---|---|
| emails:send | إرسال البريد الإلكتروني |
| emails:read | قراءة الرسائل المرسَلة وحالة تسليمها |
| drafts:read | قراءة المسودات |
| drafts:write | إنشاء المسودات وتحريرها |
| threads:read | قراءة السلاسل والرسائل |
| threads:write | تسمية السلاسل وقراءتها وأرشفتها |
| labels:read | قراءة التسميات |
| labels:write | إنشاء التسميات وتحريرها |
| contacts:read | قراءة جهات الاتصال |
| contacts:write | إضافة جهات الاتصال وتحريرها وإزالتها |
| audiences:read | قراءة الجماهير ومن فيها |
| audiences:write | إنشاء الجماهير وتحريرها، وتغيير من فيها |
| calendar:read | قراءة أحداث التقويم والدعوات |
| calendar:write | إنشاء أحداث التقويم وتغييرها والرد عليها |
| templates:read | قراءة قوالب البريد ومعاينتها |
| templates:write | إنشاء قوالب البريد وتحريرها والإرسال بها |
| domains:read | قراءة النطاقات وحالة DNS الخاصة بها |
| domains:write | توثيق النطاقات وتهيئتها |
| webhooks:read | قراءة نقاط نهاية الويب هوك وعمليات التسليم |
| webhooks:write | إنشاء الويب هوك وتحريره واختباره |
| rules:read | قراءة قواعد البريد واختبارها |
| rules:write | إنشاء قواعد البريد وتحريرها وإعادة ترتيبها |
| connections:read | قراءة أي صناديق بريد متصلة |
| members:read | رؤية من في مساحة العمل وما يحمله |
| members:write | إضافة الأشخاص وإزالتهم، وتغيير ما يمكنهم الوصول إليه |
| roles:read | قراءة الأدوار التي تعرّفها مساحة العمل هذه |
| roles:write | إنشاء الأدوار وتحريرها وحذفها |
| settings:read | قراءة إعدادات صندوق البريد، بما فيها التوقيع |
| settings:write | تغيير إعدادات صندوق البريد والتوقيع |
| keys:write | استبدال سرّه الخاص دون أن يفتح أحد لوحة التحكم |
المفتاح المنشأ بلا قائمة صلاحيات مدروسة يحصل على emails:send ولا شيء غيرها. فالافتراض الآمن لبيانات اعتماد هو أضيق ما يجعلها مفيدة.
المفتاح مُقيَّد بالدور الذي خلفه
يمكن إصدار المفتاح مقابل دور، والدور سقف لا منح ثانٍ. فما يُسمح للمفتاح بفعله فعلًا هو صلاحياته هو متقاطعةً مع أذونات ذلك الدور (key.scopes ∩ role.permissions)، محسوبةً مرة واحدة عند الحدود، في كل طلب، قبل بلوغ أي نقطة نهاية. ولا شيء في المصب يعلم بوجود الأدوار: فالصلاحية التي لا يحملها الدور ببساطة ليست في القائمة التي تقرؤها فحوص الصلاحيات.
فالقائمتان تُقرآن معًا ولا تفوز إحداهما وحدها. فالمفتاح الحامل emails:send تحت دور لا يحملها لا يُسمح له بالإرسال؛ والدور الحامل emails:send لا يعطي شيئًا لمفتاح لم يطلبها قط. فتحديد صلاحية هو طلب سلطة، والدور يقرر كم ستنال مما طلبت.
المفتاح بلا دور بلا سقف، وهو لذلك بسعة مساحة العمل التي صدر مقابلها. وهذا ما يحمله كل مفتاح أُنشئ قبل وجود الأدوار وما يناله المالك بترك الحقل على حاله، فالدور الفارغ هو أوسع حالة يمكن أن يكون عليها مفتاح لا أضيقها. وهو أيضًا سبب أن حذف دور يُلزمك بقول أين تذهب مفاتيحه: فإيتامها كان سيرقّي كلًّا منها بصمت.
يُحسَم التقاطع لكل طلب بدل ختمه على المفتاح وقت الإصدار. وهذا يجعل تضييق دور سحبًا حيًّا، ساريًا على النداء التالي للمُنادي دون الحاجة إلى تدوير المفتاح، ويجعل توسيعه حيًّا بالطريقة نفسها تمامًا، وهو النصف الجدير بالتذكّر.
GET /ping وGET /keys/self يُبلغان بـ scopes إلى جانب grantedScopes وroleId لأجل إخفاق واحد بعينه. فـ scopes هي القائمة الفعّالة والوحيدة التي تُفوّض أي شيء؛ وgrantedScopes هي ما صدر به المفتاح. وأي شيء في الثانية وغائب عن الأولى أخذه الدور، وذلك الفرق هو الجواب الكامل عن «مفتاحي يحمل emails:send وأنا أحصل على insufficient_scope». والحل تغيير الدور لا مفتاح آخر.
curl "$OE/ping" -H "$AUTH" { "ok": true, "keyId": "4c1b257a66287fd113bd89d0", "mode": "live", "scopes": ["emails:read", "threads:read"], "roleId": "role_c40a95f21cc65d31c2a89e07", "grantedScopes": ["emails:send", "emails:read", "threads:read"], "workspaceId": "10417196-e324-4283-af98-66ec62167c47"}خمسة أذونات لا يمكن أن تصل إلى مفتاح إطلاقًا: api-keys:read وapi-keys:write وbilling:read وbilling:write وworkspace:manage. فهي أذونات لا صلاحيات، فلا يستطيع أي دور مهما كان سخيًّا وضعها على رمز: فصكّ مفتاح آخر، أو تغيير ما يُسمح لمفتاح آخر بفعله، أو نقل الخطة، شيء لا يفعله إلا شخص مسجَّل الدخول. والشيء الوحيد الذي يُسمح للمفتاح بفعله بنفسه هو استبدال سرّه الخاص، خلف الصلاحية keys:write. وGET /roles/permissions يضع على الخمسة scope: false، وهو ما يتيح لمكوّن واحد أن يرسم مصفوفة الأدوار وقائمة مربعات إنشاء المفتاح معًا.
roles:write هي عمليًا المفردات كلها، والتظاهر بغير ذلك كان سيصير التوثيق الأخطر. فالمفتاح الذي يحملها يستطيع تعديل الدور نفسه الذي يقيّده بـ PATCH ويمنح نفسه كل شيء آخر، ولأن السقف يُحسَم لكل طلب فإن الأوسع يسري على النداء التالي مباشرة. وهذه ليست ثغرة تُسَد، لأن محرر أدوار لا يستطيع تحرير الأدوار ليس محرر أدوار. بل هي سبب ألّا تضع roles:write على مفتاح لم يكن يحتاج إلا إلى قراءة قائمة الأعضاء.
نطاق الإرسال
بمعزل عن الصلاحيات، يمكن تضييق ما يُسمح للمفتاح بالإرسال باسمه. فهو يحمل قائمتين. domainAllowlist تحمل نطاقات كاملة، والمفتاح الحامل نطاقًا يستطيع الإرسال باسم أي عنوان عليه، بما في ذلك عناوين أُنشئت بعد المفتاح. وaddressAllowlist تحمل عناوين مفردة. اترك الاثنتين فارغتين ويكون المفتاح بسعة مساحة العمل، لا أوسع أبدًا. وGET /keys/self يعرض القائمتين، وGET /addresses يُبلغ بما يُسمح لمفتاح معين باستخدامه فعلًا، وهو الجواب عن from_address_forbidden غير المفسَّر.
والمجموعة نفسها تضيّق ما يقرؤه المفتاح. فالبريد المرسَل والتتبع والتقويم لا تجيب إلا عن العناوين التي يُسمح للمفتاح بالإرسال باسمها، فمفتاح محصور بنطاق واحد لا يرسل ولا يقرأ نيابةً عن نطاق آخر. والنطاق الكامل يتيح للمفتاح أيضًا ضبط مضيف التتبع لذلك النطاق، وهو ما لا يستطيعه مفتاح محصور بعناوين مفردة.
ثلاثة تضييقات إذن، وهي تتركّب لا يلغي بعضها بعضًا: صلاحيات المفتاح، وأذونات الدور فوقه، والنطاقات والعناوين التي يُسمح له بوضعها في ترويسة From. والإرسال يحتاج الثلاثة، والرفض يسمّي أول ما صادفه منها فقط.