الوصول الآمن إلى البيانات للمستخدمين والمجموعات

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

الحل: التحكم في الوصول المستند إلى الدور

يمكنك الاستفادة من نموذج بيانات 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"
    // ...
  }
}

تحتوي التعليقات على حقلين فقط، رقم تعريف المستخدم للمؤلف وبعض المحتوى:

/storyid/{storyid}/comments/{commentid}

{
  user: "alice",
  content: "I think this is a great story!"
}

القواعد

الآن بعد أن تم تسجيل أدوار المستخدمين في قاعدة البيانات، تحتاج إلى كتابة قواعد الأمان للتحقق من صحتها. تفترض هذه القواعد أن التطبيق يستخدم مصادقة Firebase بحيث يكون المتغير 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;
        }
     }
   }
}

القيود

يوضح الحل الموضح أعلاه تأمين بيانات المستخدمين باستخدام قواعد الأمان، ولكن يجب أن تكون على دراية بالقيود التالية:

  • درجة الدقة: في المثال أعلاه، تحصل أدوار متعددة (المؤلف والمالك) على إذن وصول للكتابة إلى المستند نفسه ولكن مع قيود مختلفة. قد يصبح من الصعب إدارة هذا الأمر باستخدام مستندات أكثر تعقيدًا، وقد يكون من الأفضل تقسيم مستندات فردية إلى مستندات متعددة يملكها كل منها دور واحد.
  • المجموعات الكبيرة: إذا كنت بحاجة إلى المشاركة مع مجموعات كبيرة جدًا أو معقدة، يُرجى مراعاة نظام يتم فيه تخزين الأدوار في مجموعتها الخاصة بدلاً من تخزينها كحقل في المستند المستهدف.