Безопасный доступ к данным для пользователей и групп

Многие приложения для совместной работы позволяют пользователям читать и записывать различные фрагменты данных на основе набора разрешений. Например, в приложении для редактирования документов пользователи могут захотеть разрешить нескольким пользователям читать и писать свои документы, блокируя при этом нежелательный доступ.

Решение: управление доступом на основе ролей

Вы можете воспользоваться преимуществами модели данных Cloud Firestore, а также настраиваемых правил безопасности для реализации управления доступом на основе ролей в своем приложении.

Предположим, вы создаете приложение для совместной работы, в котором пользователи могут создавать «рассказы» и «комментарии» со следующими требованиями безопасности:

  • У каждой истории есть один владелец, и ею можно поделиться с «писателями», «комментаторами» и «читателями».
  • Читатели могут только видеть истории и комментарии. Они не могут ничего редактировать.
  • Комментаторы имеют все права доступа читателей, и они также могут добавлять комментарии к истории.
  • Писатели имеют все права комментаторов, а также могут редактировать содержание истории.
  • Владельцы могут редактировать любую часть истории, а также контролировать доступ других пользователей.

Структура данных

Предположим, что в вашем приложении есть коллекция stories , где каждый документ представляет собой историю. Каждая история также имеет подколлекцию comments , где каждый документ является комментарием к этой истории.

Чтобы отслеживать роли доступа, добавьте поле roles , которое представляет собой сопоставление идентификаторов пользователей с ролями:

/истории/{идентификатор истории}

{
  title: "A Great Story",
  content: "Once upon a time ...",
  roles: {
    alice: "owner",
    bob: "reader",
    david: "writer",
    jane: "commenter"
    // ...
  }
}

Комментарии содержат только два поля, идентификатор пользователя автора и некоторый контент:

/stories/{идентификатор_истории}/comments/{идентификатор_комментария}

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

Ограничения

Решение, показанное выше, демонстрирует защиту пользовательских данных с помощью правил безопасности, но вы должны знать о следующих ограничениях:

  • Гранулярность : в приведенном выше примере несколько ролей (автор и владелец) имеют доступ на запись к одному и тому же документу, но с разными ограничениями. Это может стать трудным для управления более сложными документами, и может быть лучше разделить один документ на несколько документов, каждый из которых принадлежит одной роли.
  • Большие группы : если вам нужно предоставить доступ очень большим или сложным группам, рассмотрите систему, в которой роли хранятся в их собственной коллекции, а не в виде поля в целевом документе.