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

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

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

Вы можете воспользоваться моделью данных 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}/комментарии/{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;
        }
     }
   }
}

Ограничения

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

  • Детализация . В приведенном выше примере несколько ролей (писатель и владелец) имеют доступ на запись к одному и тому же документу, но с разными ограничениями. С более сложными документами это может оказаться затруднительным, и, возможно, лучше разделить отдельные документы на несколько документов, каждый из которых принадлежит одной роли.
  • Большие группы . Если вам необходимо предоставить общий доступ к очень большим или сложным группам, рассмотрите систему, в которой роли хранятся в отдельной коллекции, а не в виде поля в целевом документе.