دسترسی ایمن به داده ها برای کاربران و گروه ها

بسیاری از برنامه‌های مشارکتی به کاربران اجازه می‌دهند تا بر اساس مجموعه‌ای از مجوزها، بخش‌های مختلف داده‌ها را بخوانند و بنویسند. برای مثال، در یک برنامه ویرایش اسناد، کاربران ممکن است بخواهند به چند کاربر اجازه دهند اسناد خود را بخوانند و بنویسند و در عین حال دسترسی‌های ناخواسته را مسدود کنند.

راه حل: کنترل دسترسی مبتنی بر نقش

شما می‌توانید از مدل داده Cloud Firestore و همچنین قوانین امنیتی سفارشی برای پیاده‌سازی کنترل دسترسی مبتنی بر نقش در برنامه خود استفاده کنید.

فرض کنید در حال ساخت یک برنامه‌ی نوشتاری مشارکتی هستید که در آن کاربران می‌توانند «داستان» و «نظر» با الزامات امنیتی زیر ایجاد کنند:

  • هر داستان یک مالک دارد و می‌تواند با «نویسندگان»، «نظردهندگان» و «خوانندگان» به اشتراک گذاشته شود.
  • خوانندگان فقط می‌توانند داستان‌ها و نظرات را ببینند. آن‌ها نمی‌توانند چیزی را ویرایش کنند.
  • نظردهندگان تمام دسترسی‌های خوانندگان را دارند و می‌توانند به یک داستان نظر اضافه کنند.
  • نویسندگان تمام دسترسی‌های نظردهندگان را دارند و می‌توانند محتوای داستان را نیز ویرایش کنند.
  • مالکان می‌توانند هر بخشی از یک داستان را ویرایش کنند و همچنین دسترسی سایر کاربران را کنترل کنند.

ساختار داده

فرض کنید برنامه شما یک مجموعه stories دارد که در آن هر سند نشان‌دهنده یک داستان است. هر داستان همچنین یک زیرمجموعه comments دارد که در آن هر سند، نظری درباره آن داستان است.

برای پیگیری نقش‌های دسترسی، یک فیلد roles اضافه کنید که نقشه‌ای از شناسه‌های کاربری به نقش‌ها است:

/داستان‌ها/{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...
         }
     }
   }
}

مرحله ۲ : یک قانون 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} {
            // ...
         }
     }
   }
}

مرحله ۳ : قوانینی بنویسید که به کاربر با هر نقشی اجازه خواندن داستان‌ها و نظرات را بدهد. استفاده از توابع تعریف شده در مرحله قبل، قوانین را مختصر و خوانا نگه می‌دارد:

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']);
        }
     }
   }
}

مرحله ۴ : به نویسندگان داستان، نظردهندگان و صاحبان نظر اجازه دهید نظرات خود را ارسال کنند. توجه داشته باشید که این قانون همچنین تأیید می‌کند که 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;
        }
     }
   }
}

مرحله ۵ : به نویسندگان این امکان را بدهید که محتوای داستان را ویرایش کنند، اما نتوانند نقش‌های داستان را ویرایش کنند یا سایر ویژگی‌های سند را تغییر دهند. این امر مستلزم تقسیم قانون 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;
        }
     }
   }
}

محدودیت‌ها

راهکار نشان داده شده در بالا، ایمن‌سازی داده‌های کاربر با استفاده از قوانین امنیتی را نشان می‌دهد، اما باید از محدودیت‌های زیر آگاه باشید:

  • جزئیات : در مثال بالا، چندین نقش (نویسنده و مالک) دسترسی نوشتن به یک سند واحد را دارند، اما با محدودیت‌های متفاوت. مدیریت این امر با اسناد پیچیده‌تر می‌تواند دشوار شود و شاید بهتر باشد اسناد واحد را به چندین سند تقسیم کنید که هر کدام متعلق به یک نقش واحد باشند.
  • گروه‌های بزرگ : اگر نیاز به اشتراک‌گذاری با گروه‌های بسیار بزرگ یا پیچیده دارید، سیستمی را در نظر بگیرید که در آن نقش‌ها در مجموعه خودشان ذخیره می‌شوند، نه به عنوان یک فیلد در سند هدف.