Acesso seguro aos dados para usuários e grupos

Muitos apps colaborativos permitem que os usuários leiam e escrevam diferentes partes de dados com base em um conjunto de permissões. Em um app de edição de documentos, por exemplo, talvez os usuários queiram permitir que algumas pessoas leiam e escrevam os documentos deles ao mesmo tempo que bloqueiam os acessos indesejados.

Solução: controle de acesso com base em papéis

Aproveite o modelo de dados do Cloud Firestore e as regras de segurança personalizadas para implementar controles de acesso com base em papéis no seu app.

Imagine que você esteja criando um aplicativo colaborativo de escrita. Nele, os usuários podem criar "histórias" e "comentários" com os seguintes requisitos de segurança:

  • Cada história tem um proprietário e pode ser compartilhada com "escritores", "comentaristas" e "leitores".
  • Os leitores só podem ver as histórias e os comentários. Eles não podem editar nada.
  • Os comentaristas têm o mesmo nível de acesso que os leitores, mas eles também podem adicionar comentários a uma história.
  • Os escritores têm o mesmo nível de acesso que os comentaristas, mas eles também podem editar o conteúdo da história.
  • Os proprietários podem editar qualquer parte de uma história e também controlar o acesso de outros usuários.

Estrutura de dados

Suponha que seu aplicativo tenha uma coleção stories em que os documentos representam histórias. Cada história também tem uma subcoleção comments em que cada documento é um comentário sobre a história.

Para acompanhar os papéis de acesso, adicione um campo roles que é um mapa de IDs de usuário aos papéis:

/stories/{storyid}

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

Os comentários contêm apenas dois campos, o ID de usuário do autor e algum conteúdo:

/stories/{storyid}/comments/{commentid}

{
  user: "alice",
  content: "I think this is a great story!"
}

Regras

Agora que os papéis dos usuários estão gravados no banco de dados, você precisa escrever regras de segurança para validá-los. Nessas regras, presume-se que o aplicativo use o Firebase Auth para que a variável request.auth.uid seja o ID do usuário.

Etapa 1: comece com um arquivo de regras básicas, que inclua regras vazias para histórias e comentários:

service cloud.firestore {
   match /databases/{database}/documents {
     match /stories/{story} {
         // TODO: Story rules go here...

         match /comments/{comment} {
            // TODO: Comment rules go here...
         }
     }
   }
}

Etapa 2: adicione uma regra write simples que dê aos proprietários controle total sobre as histórias. As funções definidas ajudam a determinar os papéis de um usuário e se novos documentos são válidos:

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} {
            // ...
         }
     }
   }
}

Passo 3: escreva regras que permitam a usuários de qualquer papel a leitura de histórias e comentários. O uso das funções definidas na etapa anterior mantém as regras concisas e legíveis:

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

Etapa 4: permita a postagem de comentários por parte dos escritores, comentaristas e proprietários. Observe que essa regra também valida se o owner do comentário corresponde ao usuário solicitante, o que impede que os usuários escrevam sobre os comentários um do outro:

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

Etapa 5: permita que os escritores editem o conteúdo da história, mas não os papéis dela ou outras propriedades do documento. Isso exige a divisão da regra write de histórias em regras separadas para create, update e delete, porque os escritores só podem atualizar histórias:

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

Limitações

A solução indicada acima demonstra como proteger os dados do usuário com regras de segurança. Porém, ela está sujeita às seguintes limitações:

  • Granularidade: no exemplo acima, usuários com vários papéis (escritor e proprietário) têm acesso de escrita ao mesmo documento, mas com limitações diferentes. O gerenciamento dessa situação pode se tornar difícil no caso de documentos mais complexos. Talvez seja melhor dividir documentos únicos em vários, cada um deles pertencendo a um único papel.
  • Grupos grandes: se você precisa compartilhar os dados com grupos muito grandes ou complexos, é recomendável usar um sistema em que os papéis sejam armazenados na própria coleção e não como um campo no documento de destino.