Acesso seguro a dados para usuários e grupos

Muitos aplicativos colaborativos permitem que os usuários leiam e gravem diferentes dados com base em um conjunto de permissões. Em um aplicativo de edição de documentos, por exemplo, os usuários podem permitir que alguns usuários leiam e escrevam seus documentos enquanto bloqueiam o acesso indesejado.

Solução: Controle de acesso baseado em função

Você pode aproveitar o modelo de dados do Cloud Firestore, bem como regras de segurança personalizadas para implementar o controle de acesso baseado em função em seu aplicativo.

Suponha que você esteja construindo um aplicativo de escrita colaborativa no qual os usuários possam criar “histórias” e “comentários” com os seguintes requisitos de segurança:

  • Cada história tem um dono e pode ser compartilhada com “escritores”, “comentaristas” e “leitores”.
  • Os leitores só podem ver histórias e comentários. Eles não podem editar nada.
  • Os comentaristas têm todo o acesso dos leitores e também podem adicionar comentários a uma história.
  • Os escritores têm todo o acesso dos comentaristas e também podem editar o conteúdo da história.
  • Os proprietários podem editar qualquer parte de uma história, bem como controlar o acesso de outros usuários.

Estrutura de dados

Suponha que seu aplicativo tenha uma coleção stories em que cada documento representa uma história. Cada história também possui uma subcoleção comments onde cada documento é um comentário sobre aquela história.

Para acompanhar as funções de acesso, adicione um campo roles que é um mapa de IDs de usuário para funções:

/histórias/{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 do 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 você tem as funções dos usuários registradas no banco de dados, é necessário escrever regras de segurança para validá-las. Essas regras pressupõem que o aplicativo usa 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 inclui 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 as funções de um usuário e se os 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} {
            // ...
         }
     }
   }
}

Etapa 3 : Escreva regras que permitam que um usuário de qualquer função leia 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 : permitir que redatores de histórias, comentaristas e proprietários postem comentários. Observe que esta regra também valida que o owner do comentário corresponde ao usuário solicitante, o que evita que os usuários escrevam os comentários uns dos outros:

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 : Dê aos redatores a capacidade de editar o conteúdo da história, mas não de editar as funções da história ou alterar quaisquer outras propriedades do documento. Isso requer a divisão da regra write de histórias em regras separadas para create , update e delete , já que 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 mostrada acima demonstra a proteção dos dados do usuário usando regras de segurança, mas você deve estar ciente das seguintes limitações:

  • Granularidade : No exemplo acima, múltiplas funções (escritor e proprietário) têm acesso de gravação ao mesmo documento, mas com limitações diferentes. Isso pode ser difícil de gerenciar com documentos mais complexos e pode ser melhor dividir documentos únicos em vários documentos, cada um pertencente a uma única função.
  • Grupos Grandes : Se você precisar compartilhar com grupos muito grandes ou complexos, considere um sistema onde as funções sejam armazenadas em sua própria coleção, em vez de como um campo no documento de destino.