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

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

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

يمكنك الاستفادة من نموذج بيانات 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 هو معرّف المستخدم.

الخطوة 1: ابدأ بملف قواعد أساسي يتضمّن قواعد فارغة للقصص والتعليقات:

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;
        }
     }
   }
}

القيود

يوضّح الحلّ الموضّح أعلاه كيفية تأمين بيانات المستخدمين باستخدام "قواعد الأمان"، ولكن عليك العِلم بالقيود التالية:

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