Proteger seus dados do Firestore com as regras de segurança do Firebase

1. Antes de começar

O Cloud Firestore, o Cloud Storage para Firebase e o Realtime Database dependem de arquivos de configuração que você cria para conceder acesso de leitura e gravação. Essa configuração, chamada de regras de segurança, também pode funcionar como um tipo de esquema para o app. Ela é uma das partes mais importantes do desenvolvimento do aplicativo. E este codelab vai ajudar você.

Pré-requisitos

  • Um editor simples, como Visual Studio Code, Atom ou Sublime Text
  • Node.js 8.6.0 ou mais recente (para instalar o Node.js, use o nvm; para verificar sua versão, execute node --version)
  • Java 7 ou mais recente. Para instalar o Java, use estas instruções. Para verificar sua versão, execute java -version

Atividades deste laboratório

Neste codelab, você vai proteger uma plataforma de blog simples criada no Firestore. Você vai usar o emulador do Firestore para executar testes de unidade de acordo com as regras de segurança e garantir que as regras permitam e proíbam o acesso esperado.

Você vai aprender a:

  • Conceder permissões granulares
  • Aplicar validações de dados e tipo
  • Implementar o controle de acesso baseado em atributos
  • Conceder acesso com base no método de autenticação
  • Criar funções personalizadas
  • Criar regras de segurança baseadas em tempo
  • Implementar uma lista de bloqueio e exclusões reversíveis
  • Entenda quando desnormalizar os dados para atender a vários padrões de acesso

2. Configurar

Este é um aplicativo de blogs. Confira um resumo de alto nível da funcionalidade do aplicativo:

Rascunhos de postagens do blog:

  • Os usuários podem criar rascunhos de postagens do blog, que ficam na coleção drafts.
  • O autor pode continuar atualizando um rascunho até que ele esteja pronto para publicação.
  • Quando estiver pronto para publicação, será acionada uma função do Firebase que criará um novo documento na coleção published.
  • Os rascunhos podem ser excluídos pelo autor ou pelos moderadores do site

Postagens do blog publicadas:

  • As postagens publicadas não podem ser criadas pelos usuários, apenas por uma função.
  • Elas só podem ser excluídas de forma reversível, o que atualiza um atributo visible para falso.

Comentários

  • As postagens publicadas permitem comentários, que são uma subcoleção em cada postagem publicada.
  • Para reduzir abusos, os usuários precisam ter um endereço de e-mail verificado e não estar em uma lista de bloqueio para poder deixar comentários.
  • Os comentários só podem ser atualizados até uma hora após a postagem.
  • Os comentários podem ser excluídos pelo autor do comentário, pelo autor da postagem original ou pelos moderadores.

Além das regras de acesso, você vai criar regras de segurança que aplicam campos obrigatórios e validações de dados.

Tudo vai acontecer localmente usando o Pacote de emuladores do Firebase.

Faça o download do código-fonte

Neste codelab, você vai começar com testes para as regras de segurança, mas as regras de segurança mínimas. Portanto, a primeira coisa que você precisa fazer é clonar a origem para executar os testes:

$ git clone https://github.com/FirebaseExtended/codelab-rules.git

Em seguida, acesse o diretório de estado inicial, onde você vai trabalhar pelo restante deste codelab:

$ cd codelab-rules/initial-state

Agora, instale as dependências para executar os testes. Se você estiver com uma conexão de Internet mais lenta, isso pode levar um ou dois minutos:

# Move into the functions directory, install dependencies, jump out.
$ cd functions && npm install && cd -

Instalar a CLI do Firebase

O conjunto de emuladores que você vai usar para executar os testes faz parte da CLI do Firebase (interface de linha de comando), que pode ser instalada na sua máquina com o seguinte comando:

$ npm install -g firebase-tools

Em seguida, verifique se você tem a versão mais recente da CLI. Este codelab funciona com a versão 8.4.0 ou mais recente, mas as versões mais recentes incluem mais correções de bugs.

$ firebase --version
9.10.2

3. Executar os testes

Nesta seção, você vai executar os testes localmente. Isso significa que é hora de inicializar o Pacote de emuladores.

Iniciar os emuladores

O aplicativo com que você trabalhará tem três coleções principais do Firestore: drafts contém postagens do blog que estão em andamento, a coleção published contém as postagens do blog que foram publicadas e comments é uma subcoleção de postagens publicadas. O repositório vem com testes de unidade para as regras de segurança que definem os atributos do usuário e outras condições necessárias para que um usuário crie, leia, atualize e exclua documentos nas coleções drafts, published e comments. Você vai escrever as regras de segurança para que esses testes sejam aprovados.

Para começar, o banco de dados está bloqueado: as leituras e gravações no banco de dados são negadas universalmente, e todos os testes falham. À medida que você escreve as regras de segurança, os testes são aprovados. Para conferir os testes, abra functions/test.js no editor.

Na linha de comando, inicie os emuladores usando emulators:exec e execute os testes:

$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test"

Role até a parte de cima da saída:

$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test"
i  emulators: Starting emulators: functions, firestore, hosting
⚠  functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: auth, database, pubsub
⚠  functions: Unable to fetch project Admin SDK configuration, Admin SDK behavior in Cloud Functions emulator may be incorrect.
i  firestore: Importing data from /Users/user/src/firebase/rules-codelab/initial-state/.seed/firestore_export/firestore_export.overall_export_metadata
i  firestore: Firestore Emulator logging to firestore-debug.log
⚠  hosting: Authentication error when trying to fetch your current web app configuration, have you run firebase login?
⚠  hosting: Could not fetch web app configuration and there is no cached configuration on this machine. Check your internet connection and make sure you are authenticated. To continue, you must call firebase.initializeApp({...}) in your code before using Firebase.
i  hosting: Serving hosting files from: public
✔  hosting: Local server: http://localhost:5000
i  functions: Watching "/Users/user/src/firebase/rules-codelab/initial-state/functions" for Cloud Functions...
✔  functions[publishPost]: http function initialized (http://localhost:5001/codelab/us-central1/publishPost).
✔  functions[softDelete]: http function initialized (http://localhost:5001/codelab/us-central1/softDelete).
i  Running script: pushd functions; npm test
~/src/firebase/rules-codelab/initial-state/functions ~/src/firebase/rules-codelab/initial-state

> functions@ test /Users/user/src/firebase/rules-codelab/initial-state/functions
> mocha

(node:76619) ExperimentalWarning: Conditional exports is an experimental feature. This feature could change at any time


  Draft blog posts
    1) can be created with required fields by the author
    2) can be updated by author if immutable fields are unchanged
    3) can be read by the author and moderator

  Published blog posts
    4) can be read by everyone; created or deleted by no one
    5) can be updated by author or moderator

  Comments on published blog posts
    6) can be read by anyone with a permanent account
    7) can be created if email is verfied and not blocked
    8) can be updated by author for 1 hour after creation
    9) can be deleted by an author or moderator


  0 passing (848ms)
  9 failing

...

No momento, há nove falhas. Ao criar o arquivo de regras, você pode medir o progresso observando mais testes bem-sucedidos.

4. Criar rascunhos de postagens de blog.

Como o acesso a rascunhos de postagens é muito diferente do acesso a postagens publicadas, esse app de blog armazena rascunhos em uma coleção separada, /drafts. Os rascunhos só podem ser acessados pelo autor ou por um moderador e têm validações para campos obrigatórios e imutáveis.

Ao abrir o arquivo firestore.rules, você encontrará um arquivo de regras padrão:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if false;
    }
  }
}

A instrução de correspondência, match /{document=**}, usa a sintaxe ** para aplicar de forma recorrente a todos os documentos nas subcoleções. E como está no nível superior, no momento a mesma regra geral se aplica a todas as solicitações, independentemente de quem está fazendo a solicitação ou quais dados eles estão tentando ler ou gravar.

Comece removendo a instrução de correspondência mais interna e substituindo-a por match /drafts/{draftID}. Os comentários da estrutura dos documentos podem ser úteis nas regras e serão incluídos neste codelab. Eles são sempre opcionais.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional
    }
  }
}

A primeira regra que você vai escrever para os rascunhos controlará quem pode criar os documentos. Neste aplicativo, os rascunhos só podem ser criados pela pessoa listada como o autor. Verifique se o UID da pessoa que está fazendo a solicitação é o mesmo listado no documento.

A primeira condição para a criação será:

request.resource.data.authorUID == request.auth.uid

Além disso, os documentos só poderão ser criados se incluírem os três campos obrigatórios, authorUID, createdAt e title. O usuário não fornece o campo createdAt. Isso força o app a adicioná-lo antes de tentar criar um documento. Como você só precisa verificar se os atributos estão sendo criados, confira se request.resource tem todas essas chaves:

request.resource.data.keys().hasAll([
  "authorUID",
  "createdAt",
  "title"
])

O último requisito para criar uma postagem no blog é que o título não pode ter mais de 50 caracteres:

request.resource.data.title.size() < 50

Como todas essas condições precisam ser verdadeiras, concatene-as com o operador AND lógico, &&. A primeira regra se torna:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;
    }
  }
}

No terminal, execute os testes novamente e confirme se o primeiro teste foi aprovado.

5. Atualizar rascunhos de postagens do blog.

Em seguida, à medida que os autores refinam os rascunhos das postagens do blog, eles editam os documentos. Crie uma regra para as condições em que uma postagem pode ser atualizada. Primeiro, apenas o autor pode atualizar os rascunhos. Observe que, aqui você verifica o UID que já está gravado,resource.data.authorUID:

resource.data.authorUID == request.auth.uid

O segundo requisito para uma atualização é que dois atributos, authorUID e createdAt, não podem mudar:

request.resource.data.diff(resource.data).unchangedKeys().hasAll([
    "authorUID",
    "createdAt"
]);

Por fim, o título deve ter 50 caracteres ou menos:

request.resource.data.title.size() < 50;

Como todas essas condições precisam ser atendidas, concatenar com &&:

allow update: if
  // User is the author, and
  resource.data.authorUID == request.auth.uid &&
  // `authorUID` and `createdAt` are unchanged
  request.resource.data.diff(resource.data).unchangedKeys().hasAll([
    "authorUID",
    "createdAt"
  ]) &&
  // Title must be < 50 characters long
  request.resource.data.title.size() < 50;

As regras completas tornam-se:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow update: if
        // User is the author, and
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;
    }
  }
}

Execute os testes novamente e confirme se outro teste é aprovado.

6. Excluir e ler rascunhos: controle de acesso baseado em atributos

Assim como os autores podem criar e atualizar rascunhos, eles também podem excluí-los.

resource.data.authorUID == request.auth.uid

Além disso, os autores com um atributo isModerator no token de autenticação podem excluir rascunhos:

request.auth.token.isModerator == true

Como qualquer uma dessas condições é suficiente para uma exclusão, concatenar com um operador lógico OU, ||:

allow delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true

As mesmas condições se aplicam às leituras, para que a permissão possa ser adicionada à regra:

allow read, delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true

Agora, as regras completas são:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow update: if
        // User is the author, and
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow read, delete: if
        // User is draft author
        resource.data.authorUID == request.auth.uid ||
        // User is a moderator
        request.auth.token.isModerator == true;
    }
  }
}

Execute os testes novamente e confirme se outro teste agora é aprovado.

7. Leitura, criação e exclusão de posts publicados: desnormalização para diferentes padrões de acesso

Como os padrões de acesso às postagens publicadas e aos rascunhos são muito diferentes, este app desnormaliza as postagens em coleções draft e published separadas. Por exemplo, as postagens publicadas podem ser lidas por qualquer pessoa, mas não podem ser excluídas de modo irreversível. Já os rascunhos podem ser excluídos, mas só podem ser lidos pelo autor e pelos moderadores. Neste app, quando um usuário quer publicar o rascunho de uma postagem do blog, uma função é acionada para criar a nova postagem publicada.

Em seguida, você vai escrever as regras para postagens publicadas. As regras mais simples para escrever são que as postagens publicadas podem ser lidas por qualquer pessoa e não podem ser criadas nem excluídas por ninguém. Adicione estas regras:

match /published/{postID} {
  // `authorUID`: string, required
  // `content`: string, required
  // `publishedAt`: timestamp, required
  // `title`: string, < 50 characters, required
  // `url`: string, required
  // `visible`: boolean, required

  // Can be read by everyone
  allow read: if true;

  // Published posts are created only via functions, never by users
  // No hard deletes; soft deletes update `visible` field.
  allow create, delete: if false;
}

Ao adicionar essas regras, o arquivo de regras inteiro fica assim:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow update: if
        // User is the author, and
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow read, delete: if
        // User is draft author
        resource.data.authorUID == request.auth.uid ||
        // User is a moderator
        request.auth.token.isModerator == true;
    }

    match /published/{postID} {
      // `authorUID`: string, required
      // `content`: string, required
      // `publishedAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, required
      // `visible`: boolean, required

      // Can be read by everyone
      allow read: if true;

      // Published posts are created only via functions, never by users
      // No hard deletes; soft deletes update `visible` field.
      allow create, delete: if false;
    }
  }
}

Execute os testes novamente e confirme se outro teste é aprovado.

8. Como atualizar postagens publicadas: funções personalizadas e variáveis locais

Estas são as condições para atualizar uma postagem publicada:

  • isso só possa ser feito pelo autor ou moderador, e
  • ela precisa conter todos os campos obrigatórios.

Como você já escreveu as condições para ser um autor ou moderador, é possível copiar e colar as condições, mas, com o tempo, isso pode ser difícil de ler e manter. Em vez disso, você vai criar uma função personalizada que encapsula a lógica para ser um autor ou moderador. Em seguida, você vai chamá-la usando várias condições.

Criar uma função personalizada

Acima da declaração de correspondência para rascunhos, crie uma nova função com o nome isAuthorOrModerator que use como argumento um documento de postagem (isso funciona para rascunhos ou postagens publicadas) e o objeto Auth do usuário:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Returns true if user is post author or a moderator
    function isAuthorOrModerator(post, auth) {

    }

    match /drafts/{postID} {
      allow create: ...
      allow update: ...
      ...
    }

    match /published/{postID} {
      allow read: ...
      allow create, delete: ...
    }
  }
}

Usar variáveis locais

Dentro da função, use a palavra-chave let para definir as variáveis isAuthor e isModerator. Todas as funções precisam terminar com uma instrução de retorno. A nossa retornará um booleano indicando se uma das variáveis é verdadeira:

function isAuthorOrModerator(post, auth) {
  let isAuthor = auth.uid == post.authorUID;
  let isModerator = auth.token.isModerator == true;
  return isAuthor || isModerator;
}

Chamar a função

Agora, você atualizará a regra dos rascunhos para chamar essa função, com o cuidado de transmitir resource.data como o primeiro argumento:

  // Draft blog posts
  match /drafts/{draftID} {
    ...
    // Can be deleted by author or moderator
    allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
  }

Agora você pode escrever uma condição para atualizar postagens publicadas que também usa a nova função:

allow update: if isAuthorOrModerator(resource.data, request.auth);

Adicionar validações

Alguns dos campos de uma postagem publicada não podem ser alterados, especificamente os campos url, authorUID e publishedAt são imutáveis. Os outros dois campos, title, content e visible, precisam estar presentes após uma atualização. Adicione condições para aplicar estes requisitos nas atualizações das postagens publicadas:

// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
  "authorUID",
  "publishedAt",
  "url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
  "content",
  "title",
  "visible"
])

Criar uma função personalizada por conta própria

Por fim, adicione a condição de o título ter menos de 50 caracteres. Como essa é uma lógica reutilizada, é possível fazer isso criando uma nova função, titleIsUnder50Chars. Com a nova função, a condição para atualizar uma postagem publicada será:

allow update: if
  isAuthorOrModerator(resource.data, request.auth) &&
  // Immutable fields are unchanged
  request.resource.data.diff(resource.data).unchangedKeys().hasAll([
    "authorUID",
    "publishedAt",
    "url"
  ]) &&
  // Required fields are present
  request.resource.data.keys().hasAll([
    "content",
    "title",
    "visible"
  ]) &&
  titleIsUnder50Chars(request.resource.data);

E o arquivo de regra completo é:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Returns true if user is post author or a moderator
    function isAuthorOrModerator(post, auth) {
      let isAuthor = auth.uid == post.authorUID;
      let isModerator = auth.token.isModerator == true;
      return isAuthor || isModerator;
    }

    function titleIsUnder50Chars(post) {
      return post.title.size() < 50;
    }

    // Draft blog posts
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        titleIsUnder50Chars(request.resource.data);

      allow update: if
        // User is the author, and
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
          ]) &&
        titleIsUnder50Chars(request.resource.data);

      // Can be read or deleted by author or moderator
      allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
    }

    // Published blog posts are denormalized from drafts
    match /published/{postID} {
      // `authorUID`: string, required
      // `content`: string, required
      // `publishedAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, required
      // `visible`: boolean, required

      // Can be read by everyone
      allow read: if true;

      // Published posts are created only via functions, never by users
      // No hard deletes; soft deletes update `visible` field.
      allow create, delete: if false;

      allow update: if
        isAuthorOrModerator(resource.data, request.auth) &&
        // Immutable fields are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "publishedAt",
          "url"
        ]) &&
        // Required fields are present
        request.resource.data.keys().hasAll([
          "content",
          "title",
          "visible"
        ]) &&
        titleIsUnder50Chars(request.resource.data);
    }
  }
}

Execute os testes novamente. Neste ponto, você tem cinco testes aprovados e quatro com reprovação.

9. Comentários: subcoleções e permissões do provedor de login

As postagens publicadas permitem comentários, que são armazenados em uma subcoleção da postagem publicada (/published/{postID}/comments/{commentID}). Por padrão, as regras de uma coleção não se aplicam às subcoleções. Você não quer que as mesmas regras que se aplicam ao documento pai da postagem publicada se apliquem aos comentários. Você vai criar regras diferentes.

Para criar regras de acesso aos comentários, comece com a instrução match:

match /published/{postID}/comments/{commentID} {
  // `authorUID`: string, required
  // `comment`: string, < 500 characters, required
  // `createdAt`: timestamp, required
  // `editedAt`: timestamp, optional

Leitura de comentários: não pode ser anônimo

Neste app, somente usuários que criaram uma conta permanente, e não uma conta anônima, podem ler os comentários. Para aplicar essa regra, procure o atributo sign_in_provider que está em cada objeto auth.token:

allow read: if request.auth.token.firebase.sign_in_provider != "anonymous";

Execute os testes novamente e confirme se mais um deles é aprovado.

Como criar comentários: verificar uma lista de bloqueio

Há três condições para criar um comentário:

  • um usuário precisa ter um e-mail verificado
  • o comentário precisa ter menos de 500 caracteres e
  • não podem estar em uma lista de usuários banidos, que é armazenada no Firestore na coleção bannedUsers. Considerar estas condições uma de cada vez:
request.auth.token.email_verified == true
request.resource.data.comment.size() < 500
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));

A regra final para criar comentários é:

allow create: if
  // User has verified email
  (request.auth.token.email_verified == true) &&
  // UID is not on bannedUsers list
  !(exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));

O arquivo de regras inteiro agora é:

For bottom of step 9
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Returns true if user is post author or a moderator
    function isAuthorOrModerator(post, auth) {
      let isAuthor = auth.uid == post.authorUID;
      let isModerator = auth.token.isModerator == true;
      return isAuthor || isModerator;
    }

    function titleIsUnder50Chars(post) {
      return post.title.size() < 50;
    }

    // Draft blog posts
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User is author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and createdAt fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        titleIsUnder50Chars(request.resource.data);

      allow update: if
        // User is author
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
          ]) &&
        titleIsUnder50Chars(request.resource.data);

      // Can be read or deleted by author or moderator
      allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
    }

    // Published blog posts are denormalized from drafts
    match /published/{postID} {
      // `authorUID`: string, required
      // `content`: string, required
      // `publishedAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, required
      // `visible`: boolean, required

      // Can be read by everyone
      allow read: if true;

      // Published posts are created only via functions, never by users
      // No hard deletes; soft deletes update `visible` field.
      allow create, delete: if false;

      allow update: if
        isAuthorOrModerator(resource.data, request.auth) &&
        // Immutable fields are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "publishedAt",
          "url"
        ]) &&
        // Required fields are present
        request.resource.data.keys().hasAll([
          "content",
          "title",
          "visible"
        ]) &&
        titleIsUnder50Chars(request.resource.data);
    }

    match /published/{postID}/comments/{commentID} {
      // `authorUID`: string, required
      // `createdAt`: timestamp, required
      // `editedAt`: timestamp, optional
      // `comment`: string, < 500 characters, required

      // Must have permanent account to read comments
      allow read: if !(request.auth.token.firebase.sign_in_provider == "anonymous");

      allow create: if
        // User has verified email
        request.auth.token.email_verified == true &&
        // Comment is under 500 characters
        request.resource.data.comment.size() < 500 &&
        // UID is not on the block list
        !exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
    }
  }
}

Execute os testes novamente e confira se mais um deles é aprovado.

10. Atualização de comentários: regras com base no tempo

A lógica de negócios para comentários é que eles podem ser editados pelo autor por uma hora após a criação. Para implementar isso, use o carimbo de data/hora createdAt.

Primeiro, para estabelecer que o usuário é o autor:

request.auth.uid == resource.data.authorUID

Depois, se o comentário foi criado na última hora:

(request.time - resource.data.createdAt) < duration.value(1, 'h');

Combinando esses valores com o operador lógico AND, a regra para atualização de comentários se torna:

allow update: if
  // is author
  request.auth.uid == resource.data.authorUID &&
  // within an hour of comment creation
  (request.time - resource.data.createdAt) < duration.value(1, 'h');

Execute os testes novamente e confira se mais um deles é aprovado.

11. Como excluir comentários: verificar a propriedade do familiar responsável

Os comentários podem ser excluídos pelo autor do comentário, por um moderador ou pelo autor da postagem no blog.

Primeiro, porque a função auxiliar que você adicionou anteriormente verifica um campo authorUID que pode existir em uma postagem ou comentário, você pode reutilizar a função auxiliar para verificar se o usuário é o autor ou moderador:

isAuthorOrModerator(resource.data, request.auth)

Para verificar se o usuário é o autor da postagem do blog, use um get para procurar a postagem no Firestore:

request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID

Como qualquer uma dessas condições é suficiente, use um operador lógico OR entre elas:

allow delete: if
  // is comment author or moderator
  isAuthorOrModerator(resource.data, request.auth) ||
  // is blog post author
  request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID;

Execute os testes novamente e confira se mais um deles é aprovado.

E o arquivo de regras inteiro é:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Returns true if user is post author or a moderator
    function isAuthorOrModerator(post, auth) {
      let isAuthor = auth.uid == post.authorUID;
      let isModerator = auth.token.isModerator == true;
      return isAuthor || isModerator;
    }

    function titleIsUnder50Chars(post) {
      return post.title.size() < 50;
    }

    // Draft blog posts
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User is author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and createdAt fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        titleIsUnder50Chars(request.resource.data);

      allow update: if
        // User is author
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
          ]) &&
        titleIsUnder50Chars(request.resource.data);

      // Can be read or deleted by author or moderator
      allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
    }

    // Published blog posts are denormalized from drafts
    match /published/{postID} {
      // `authorUID`: string, required
      // `content`: string, required
      // `publishedAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, required
      // `visible`: boolean, required

      // Can be read by everyone
      allow read: if true;

      // Published posts are created only via functions, never by users
      // No hard deletes; soft deletes update `visible` field.
      allow create, delete: if false;

      allow update: if
        isAuthorOrModerator(resource.data, request.auth) &&
        // Immutable fields are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "publishedAt",
          "url"
        ]) &&
        // Required fields are present
        request.resource.data.keys().hasAll([
          "content",
          "title",
          "visible"
        ]) &&
        titleIsUnder50Chars(request.resource.data);
    }

    match /published/{postID}/comments/{commentID} {
      // `authorUID`: string, required
      // `createdAt`: timestamp, required
      // `editedAt`: timestamp, optional
      // `comment`: string, < 500 characters, required

      // Must have permanent account to read comments
      allow read: if !(request.auth.token.firebase.sign_in_provider == "anonymous");

      allow create: if
        // User has verified email
        request.auth.token.email_verified == true &&
        // Comment is under 500 characters
        request.resource.data.comment.size() < 500 &&
        // UID is not on the block list
        !exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));

      allow update: if
        // is author
        request.auth.uid == resource.data.authorUID &&
        // within an hour of comment creation
        (request.time - resource.data.createdAt) < duration.value(1, 'h');

      allow delete: if
        // is comment author or moderator
        isAuthorOrModerator(resource.data, request.auth) ||
        // is blog post author
        request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID;
    }
  }
}

12. Próximas etapas

Parabéns! Você escreveu as regras de segurança que fizeram todos os testes serem aprovados e protegeram o aplicativo.

Confira alguns tópicos relacionados para se aprofundar:

  • Postagem do blog: Como revisar códigos de regras de segurança (em inglês)
  • Codelab: como fazer o desenvolvimento local primeiro com os emuladores
  • Vídeo: como configurar a CI para testes baseados em emulador usando o GitHub Actions