تسمح العديد من التطبيقات التعاونية للمستخدمين بقراءة أجزاء مختلفة من البيانات وكتابتها استنادًا إلى مجموعة من الأذونات. في تطبيق لتعديل المستندات، على سبيل المثال، قد يريد المستخدمون السماح لعدد قليل من المستخدمين بقراءة مستنداتهم وكتابتها مع حظر الوصول غير المرغوب فيه.
الحلّ: التحكّم في الوصول المستنِد إلى الدور
يمكنك الاستفادة من نموذج بيانات Cloud Firestore بالإضافة إلى قواعد الأمان المخصّصة لتنفيذ عناصر التحكّم في الوصول على أساس الدور في تطبيقك.
لنفترض أنّك بصدد إنشاء تطبيق كتابة تعاوني يمكن للمستخدمين فيه إنشاء "قصص" و"تعليقات" مع متطلبات الأمان التالية:
- لكل قصة مالك واحد ويمكن مشاركتها مع "المؤلفين" و"المعلّقين" و "القارئين".
- بإمكان القراء الاطّلاع على الأخبار والتعليقات فقط. ولا يمكنهم تعديل أي محتوى.
- يحصل المعلِّقون على جميع الأذونات التي يحصل عليها القرّاء، ويمكنهم أيضًا إضافة تعليقات إلى القصة.
- يحصل الكاتب على جميع الأذونات التي يحصل عليها المعلِّقون، ويمكنه أيضًا تعديل محتوى القصة.
- يمكن للمالكين تعديل أي جزء من القصة، بالإضافة إلى التحكّم في إمكانية وصول المستخدمين الآخرين.
بنية البيانات
لنفترض أنّ تطبيقك يتضمّن مجموعة stories
يمثّل كل مستند فيها
قصة. تحتوي كل قصة أيضًا على مجموعة فرعية comments
يكون كل مستند فيها
تعليقًا على تلك القصة.
لتتبُّع أدوار الوصول، أضِف حقل roles
الذي يمثّل خريطة
لرموز تعريف المستخدمين إلى الأدوار:
/stories/{storyid}
{
title: "A Great Story",
content: "Once upon a time ...",
roles: {
alice: "owner",
bob: "reader",
david: "writer",
jane: "commenter"
// ...
}
}
تحتوي التعليقات على حقلين فقط، وهما رقم تعريف المستخدم للكاتب وبعض المحتوى:
/stories/{storyid}/comments/{commentid}
{
user: "alice",
content: "I think this is a great story!"
}
القواعد
بعد تسجيل أدوار المستخدمين في قاعدة البيانات، عليك كتابة
قواعد أمان للتحقّق منها. تفترض هذه القواعد أنّ التطبيق يستخدم
Firebase Auth بحيث يكون المتغيّر request.auth.uid
هو معرّف المستخدم.
الخطوة الأولى: ابدأ بملف القواعد الأساسية، والذي يتضمّن القواعد الفارغة للأخبار والتعليقات:
service cloud.firestore {
match /databases/{database}/documents {
match /stories/{story} {
// TODO: Story rules go here...
match /comments/{comment} {
// TODO: Comment rules go here...
}
}
}
}
الخطوة 2: أضِف قاعدة write
بسيطة تمنح المالكين قدرة تحكّم كاملة في القصص. تساعد الدوالّ المحدّدة في تحديد أدوار المستخدم وما إذا كانت المستندات الجديدة
صالحة:
service cloud.firestore {
match /databases/{database}/documents {
match /stories/{story} {
function isSignedIn() {
return request.auth != null;
}
function getRole(rsc) {
// Read from the "roles" map in the resource (rsc).
return rsc.data.roles[request.auth.uid];
}
function isOneOfRoles(rsc, array) {
// Determine if the user is one of an array of roles
return isSignedIn() && (getRole(rsc) in array);
}
function isValidNewStory() {
// Valid if story does not exist and the new story has the correct owner.
return resource == null && isOneOfRoles(request.resource, ['owner']);
}
// Owners can read, write, and delete stories
allow write: if isValidNewStory() || isOneOfRoles(resource, ['owner']);
match /comments/{comment} {
// ...
}
}
}
}
الخطوة 3: اكتب قواعد تسمح لأي مستخدم بقراءة القصص والتعليقات. من خلال استخدام الدوالّ المحدّدة في الخطوة السابقة، تبقى القواعد موجزة وسهلة القراءة:
service cloud.firestore {
match /databases/{database}/documents {
match /stories/{story} {
function isSignedIn() {
return request.auth != null;
}
function getRole(rsc) {
return rsc.data.roles[request.auth.uid];
}
function isOneOfRoles(rsc, array) {
return isSignedIn() && (getRole(rsc) in array);
}
function isValidNewStory() {
return resource == null
&& request.resource.data.roles[request.auth.uid] == 'owner';
}
allow write: if isValidNewStory() || isOneOfRoles(resource, ['owner']);
// Any role can read stories.
allow read: if isOneOfRoles(resource, ['owner', 'writer', 'commenter', 'reader']);
match /comments/{comment} {
// Any role can read comments.
allow read: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
['owner', 'writer', 'commenter', 'reader']);
}
}
}
}
الخطوة 4: السماح لكاتبي القصص والمعلقين وأصحاب القصص بنشر التعليقات
يُرجى العلم أنّ هذه القاعدة تتحقّق أيضًا من أنّ owner
للتعليق يتطابق مع
المستخدم الذي طلب التعليق، ما يمنع المستخدمين من الكتابة فوق تعليقات بعضهم البعض:
service cloud.firestore {
match /databases/{database}/documents {
match /stories/{story} {
function isSignedIn() {
return request.auth != null;
}
function getRole(rsc) {
return rsc.data.roles[request.auth.uid];
}
function isOneOfRoles(rsc, array) {
return isSignedIn() && (getRole(rsc) in array);
}
function isValidNewStory() {
return resource == null
&& request.resource.data.roles[request.auth.uid] == 'owner';
}
allow write: if isValidNewStory() || isOneOfRoles(resource, ['owner'])
allow read: if isOneOfRoles(resource, ['owner', 'writer', 'commenter', 'reader']);
match /comments/{comment} {
allow read: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
['owner', 'writer', 'commenter', 'reader']);
// Owners, writers, and commenters can create comments. The
// user id in the comment document must match the requesting
// user's id.
//
// Note: we have to use get() here to retrieve the story
// document so that we can check the user's role.
allow create: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
['owner', 'writer', 'commenter'])
&& request.resource.data.user == request.auth.uid;
}
}
}
}
الخطوة 5: امنح المؤلّفين إمكانية تعديل محتوى القصة، ولكن ليس تعديل
أدوار القصة أو تغيير أيّ خصائص أخرى للمستند. يتطلّب ذلك تقسيم
قاعدة القصص write
إلى قواعد منفصلة لكل من create
وupdate
و
delete
لأنّه لا يمكن للمؤلفين تعديل القصص إلا على النحو التالي:
service cloud.firestore {
match /databases/{database}/documents {
match /stories/{story} {
function isSignedIn() {
return request.auth != null;
}
function getRole(rsc) {
return rsc.data.roles[request.auth.uid];
}
function isOneOfRoles(rsc, array) {
return isSignedIn() && (getRole(rsc) in array);
}
function isValidNewStory() {
return request.resource.data.roles[request.auth.uid] == 'owner';
}
function onlyContentChanged() {
// Ensure that title and roles are unchanged and that no new
// fields are added to the document.
return request.resource.data.title == resource.data.title
&& request.resource.data.roles == resource.data.roles
&& request.resource.data.keys() == resource.data.keys();
}
// Split writing into creation, deletion, and updating. Only an
// owner can create or delete a story but a writer can update
// story content.
allow create: if isValidNewStory();
allow delete: if isOneOfRoles(resource, ['owner']);
allow update: if isOneOfRoles(resource, ['owner'])
|| (isOneOfRoles(resource, ['writer']) && onlyContentChanged());
allow read: if isOneOfRoles(resource, ['owner', 'writer', 'commenter', 'reader']);
match /comments/{comment} {
allow read: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
['owner', 'writer', 'commenter', 'reader']);
allow create: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
['owner', 'writer', 'commenter'])
&& request.resource.data.user == request.auth.uid;
}
}
}
}
القيود
يوضّح الحلّ المعروض أعلاه تأمين بيانات المستخدمين باستخدام قواعد الأمان، ولكن عليك الانتباه إلى القيود التالية:
- الدقة: في المثال أعلاه، تملك أدوار متعدّدة (كاتب ومالك) إذن الوصول للكتابة إلى المستند نفسه ولكن بحدود مختلفة. وقد يصبح من الصعب إدارة ذلك مع المستندات الأكثر تعقيدًا، وقد يكون من الأفضل تقسيم المستندات الفردية إلى مستندات متعددة يملك كل منها دورًا واحدًا.
- المجموعات الكبيرة: إذا كنت بحاجة إلى المشاركة مع مجموعات كبيرة جدًا أو معقّدة، ننصحك باستخدام نظام يتم فيه تخزين الأدوار في مجموعتها الخاصة بدلاً من استخدامها كحقل في المستند المستهدَف.