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

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

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

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

الخطوة 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;
        }
     }
   }
}

القيود

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

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