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

Mantenha tudo organizado com as coleções Salve e categorize o conteúdo com base nas suas preferências.

1. Antes de começar

O Cloud Firestore, o Cloud Storage for Firebase e o Realtime Database dependem dos arquivos de configuração que você grava para conceder acesso de leitura e gravação. Essa configuração, chamada Regras de Segurança, também pode atuar como uma espécie de esquema para seu aplicativo. É uma das partes mais importantes do desenvolvimento de seu aplicativo. E este codelab irá orientá-lo.

Pré-requisitos

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

O que você vai fazer

Neste codelab, você protegerá uma plataforma de blog simples criada no Firestore. Você usará o emulador do Firestore para executar testes de unidade em relação às regras de segurança e garantir que as regras permitam ou não o acesso esperado.

Você aprenderá a:

  • Conceder permissões granulares
  • Aplicar validações de dados e tipos
  • Implementar Controle de Acesso Baseado em Atributos
  • Conceder acesso com base no método de autenticação
  • Crie funções personalizadas
  • Criar regras de segurança baseadas em tempo
  • Implementar uma lista de negações 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 blog. Aqui está um resumo de alto nível da funcionalidade do aplicativo:

Rascunho de postagens do blog:

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

Postagens de blog publicadas:

  • Posts publicados não podem ser criados por usuários, apenas por meio de uma função.
  • Eles só podem ser excluídos 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 o abuso, os usuários devem ter um endereço de e-mail verificado e não estar em uma negação para deixar um comentário.
  • Os comentários só podem ser atualizados dentro de uma hora após a publicação.
  • 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ê criará regras de segurança que impõem campos obrigatórios e validações de dados.

Tudo acontecerá localmente, usando o Firebase Emulator Suite.

Obtenha o código-fonte

Neste codelab, você começará com testes para as regras de segurança, mas com regras de segurança mínimas, então a primeira coisa que você precisa fazer é clonar a fonte para executar os testes:

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

Em seguida, vá para o diretório de estado inicial, onde você trabalhará pelo restante deste codelab:

$ cd codelab-rules/initial-state

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

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

Obter a CLI do Firebase

O Emulator Suite que você usará para executar os testes faz parte da Firebase CLI (interface de linha de comando) que pode ser instalada em sua máquina com o seguinte comando:

$ npm install -g firebase-tools

Em seguida, confirme se você tem a versão mais recente da CLI. Este codelab deve funcionar com a versão 8.4.0 ou superior, mas as versões posteriores incluem mais correções de bugs.

$ firebase --version
9.10.2

3. Execute os testes

Nesta seção, você executará os testes localmente. Isso significa que é hora de inicializar o Emulator Suite.

Inicie os emuladores

O aplicativo com o qual você trabalhará tem três coleções principais do Firestore: os drafts contêm postagens de blog em andamento, a coleção published contém as postagens de blog que foram publicadas e os comments são 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 em drafts , published e coleções de comments . Você escreverá as Regras de Segurança para fazer esses testes passarem.

Para começar, seu banco de dados está bloqueado: leituras e gravações no banco de dados são negadas universalmente e todos os testes falham. Conforme você escreve as Regras de Segurança, os testes serão aprovados. Para ver os testes, abra functions/test.js em seu 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é o topo 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

...

Neste momento existem 9 falhas. À medida que você cria o arquivo de regras, pode medir o progresso observando mais testes passarem.

4. Crie rascunhos de postagem no blog.

Como o acesso para rascunhos de postagens de blog é muito diferente do acesso para postagens de blog publicadas, esse aplicativo de blog armazena rascunhos de postagens de blog em uma coleção separada, /drafts . Os rascunhos só podem ser acessados ​​pelo autor ou moderador, e possuem 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 match, match /{document=**} , está usando a sintaxe ** para aplicar recursivamente a todos os documentos em subcoleções. E porque está no nível superior, agora a mesma regra geral se aplica a todas as solicitações, não importa quem está fazendo a solicitação ou quais dados eles estão tentando ler ou gravar.

Comece removendo a instrução match mais interna e substituindo-a por match /drafts/{draftID} . (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ê escreverá para rascunhos controlará quem pode criar os documentos. Neste aplicativo, os rascunhos só podem ser criados pela pessoa listada como autor. Verifique se o UID da pessoa que faz a solicitação é o mesmo UID listado no documento.

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

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

Em seguida, os documentos só podem 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 está impondo que o aplicativo deve adicioná-lo antes de tentar criar um documento.) Como você só precisa verificar se os atributos estão sendo criados, você pode verificar se request.resource tem todos essas chaves:

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

O requisito final 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 devem ser verdadeiras, concatene-as com o operador lógico AND, && . A primeira regra passa a ser:

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 novamente os testes e confirme se o primeiro teste foi aprovado.

5. Atualize os rascunhos das postagens do blog.

Em seguida, à medida que os autores refinarem seus rascunhos de postagens de blog, eles editarão os documentos de rascunho. Crie uma regra para as condições em que uma postagem pode ser atualizada. Primeiro, apenas o autor pode atualizar seus rascunhos. Observe que aqui você verifica o UID que já está escrito, resource.data.authorUID :

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

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

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

E, finalmente, o título deve ter 50 caracteres ou menos:

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

Como todas essas condições precisam ser atendidas, concatene-as 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 se tornam:

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 novamente seus testes e confirme se outro teste foi 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 excluir rascunhos.

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

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

request.auth.token.isModerator == true

Como qualquer uma dessas condições é suficiente para uma exclusão, concatene-as com um operador OR lógico, || :

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

As regras completas agora 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 novamente seus testes e confirme se outro teste passou.

7. Lê, cria e exclui postagens publicadas: desnormalizando para diferentes padrões de acesso

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

Em seguida, você escreverá as regras para postagens publicadas. As regras mais simples de escrever são que as postagens publicadas podem ser lidas por qualquer pessoa e não podem ser criadas ou 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;
}

Adicionando-as às regras existentes, todo o arquivo de regras 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;

      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 novamente os testes e confirme se outro teste foi aprovado.

8. Atualizando postagens publicadas: funções personalizadas e variáveis ​​locais

As condições para atualizar um post publicado são:

  • só pode ser feito pelo autor ou moderador, e
  • deve conter todos os campos obrigatórios.

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

Criar uma função personalizada

Acima da declaração de correspondência para rascunhos, crie uma nova função chamada isAuthorOrModerator que recebe como argumentos um documento de postagem (isso funcionará para rascunhos ou postagens publicadas) e o objeto de autenticação 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 devem terminar com uma declaração de retorno, e a nossa retornará um booleano indicando se alguma das variáveis ​​é verdadeira:

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

Chame a função

Agora você atualizará a regra para que os rascunhos chamem essa função, tendo o cuidado de passar 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 as 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 devem ser alterados, especificamente os campos url , authorUID e publishedAt são imutáveis. Os outros dois campos, title e content , e visible ainda devem estar presentes após uma atualização. Adicione condições para aplicar esses requisitos para atualizações de 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"
])

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

E, finalmente, adicione uma condição de que o título tenha menos de 50 caracteres. Como essa é uma lógica reutilizada, você pode fazer isso criando uma nova função, titleIsUnder50Chars . Com a nova função, a condição de atualização de um post publicado passa a 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 regras 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 novamente os testes. Neste ponto, você deve ter 5 testes de aprovação e 4 de reprovação.

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

As postagens publicadas permitem comentários e os comentários 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 a subcoleções. Você não quer que as mesmas regras que se aplicam ao documento pai da postagem publicada sejam aplicadas aos comentários; você criará diferentes.

Para escrever regras para acessar os 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

Lendo comentários: Não pode ser anônimo

Para este aplicativo, apenas usuários que criaram uma conta permanente, 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 novamente seus testes e confirme se mais um teste foi aprovado.

Criando comentários: verificando uma lista de negações

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

  • um usuário deve ter um e-mail verificado
  • o comentário deve ter menos de 500 caracteres e
  • eles não podem estar em uma lista de usuários banidos, que é armazenada no firestore na coleção bannedUsers . Tomando 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));

Todo o arquivo de regras 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 charachters
        request.resource.data.comment.size() < 500 &&
        // UID is not on the block list
        !exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
    }
  }
}

Execute novamente os testes e certifique-se de que mais um teste seja aprovado.

10. Atualizando comentários: regras baseadas em tempo

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

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

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

Em seguida, que o comentário foi criado na última hora:

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

Combinando-os 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 novamente os testes e certifique-se de que mais um teste seja aprovado.

11. Excluindo comentários: verificando a propriedade dos pais

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

Primeiro, como a função auxiliar que você adicionou anteriormente verifica um campo authorUID que pode existir em uma postagem ou em um 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 pesquisar 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 OR lógico 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 novamente os testes e certifique-se de que mais um teste seja aprovado.

E todo o arquivo de regras é:

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 charachters
        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óximos passos

Parabéns! Você escreveu as Regras de Segurança que fizeram todos os testes passarem e protegeram o aplicativo!

Aqui estão alguns tópicos relacionados para mergulhar a seguir:

  • Postagem do blog : Como revisar o código das regras de segurança
  • Codelab : caminhando pelo primeiro desenvolvimento local com os emuladores
  • Vídeo : Como usar a configuração de CI para testes baseados em emulador usando o GitHub Actions
,

1. Antes de começar

O Cloud Firestore, o Cloud Storage for Firebase e o Realtime Database dependem dos arquivos de configuração que você grava para conceder acesso de leitura e gravação. Essa configuração, chamada Regras de Segurança, também pode atuar como uma espécie de esquema para seu aplicativo. É uma das partes mais importantes do desenvolvimento de seu aplicativo. E este codelab irá orientá-lo.

Pré-requisitos

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

O que você vai fazer

Neste codelab, você protegerá uma plataforma de blog simples criada no Firestore. Você usará o emulador do Firestore para executar testes de unidade em relação às regras de segurança e garantir que as regras permitam ou não o acesso esperado.

Você aprenderá a:

  • Conceder permissões granulares
  • Aplicar validações de dados e tipos
  • Implementar Controle de Acesso Baseado em Atributos
  • Conceder acesso com base no método de autenticação
  • Crie funções personalizadas
  • Criar regras de segurança baseadas em tempo
  • Implementar uma lista de negações 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 blog. Aqui está um resumo de alto nível da funcionalidade do aplicativo:

Rascunho de postagens do blog:

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

Postagens de blog publicadas:

  • Posts publicados não podem ser criados por usuários, apenas por meio de uma função.
  • Eles só podem ser excluídos 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 o abuso, os usuários devem ter um endereço de e-mail verificado e não estar em uma negação para deixar um comentário.
  • Os comentários só podem ser atualizados dentro de uma hora após a publicação.
  • 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ê criará regras de segurança que impõem campos obrigatórios e validações de dados.

Tudo acontecerá localmente, usando o Firebase Emulator Suite.

Obtenha o código-fonte

Neste codelab, você começará com testes para as regras de segurança, mas com regras de segurança mínimas, então a primeira coisa que você precisa fazer é clonar a fonte para executar os testes:

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

Em seguida, vá para o diretório de estado inicial, onde você trabalhará pelo restante deste codelab:

$ cd codelab-rules/initial-state

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

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

Obter a CLI do Firebase

O Emulator Suite que você usará para executar os testes faz parte da Firebase CLI (interface de linha de comando) que pode ser instalada em sua máquina com o seguinte comando:

$ npm install -g firebase-tools

Em seguida, confirme se você tem a versão mais recente da CLI. Este codelab deve funcionar com a versão 8.4.0 ou superior, mas as versões posteriores incluem mais correções de bugs.

$ firebase --version
9.10.2

3. Execute os testes

Nesta seção, você executará os testes localmente. Isso significa que é hora de inicializar o Emulator Suite.

Inicie os emuladores

O aplicativo com o qual você trabalhará tem três coleções principais do Firestore: os drafts contêm postagens de blog em andamento, a coleção published contém as postagens de blog que foram publicadas e os comments são 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 em drafts , published e coleções de comments . Você escreverá as Regras de Segurança para fazer esses testes passarem.

Para começar, seu banco de dados está bloqueado: leituras e gravações no banco de dados são negadas universalmente e todos os testes falham. Conforme você escreve as Regras de Segurança, os testes serão aprovados. Para ver os testes, abra functions/test.js em seu 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é o topo 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

...

Neste momento existem 9 falhas. À medida que você cria o arquivo de regras, pode medir o progresso observando mais testes passarem.

4. Crie rascunhos de postagem no blog.

Como o acesso para rascunhos de postagens de blog é muito diferente do acesso para postagens de blog publicadas, esse aplicativo de blog armazena rascunhos de postagens de blog em uma coleção separada, /drafts . Os rascunhos só podem ser acessados ​​pelo autor ou moderador, e possuem 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 match, match /{document=**} , está usando a sintaxe ** para aplicar recursivamente a todos os documentos em subcoleções. E porque está no nível superior, agora a mesma regra geral se aplica a todas as solicitações, não importa quem está fazendo a solicitação ou quais dados eles estão tentando ler ou gravar.

Comece removendo a instrução match mais interna e substituindo-a por match /drafts/{draftID} . (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ê escreverá para rascunhos controlará quem pode criar os documentos. Neste aplicativo, os rascunhos só podem ser criados pela pessoa listada como autor. Verifique se o UID da pessoa que faz a solicitação é o mesmo UID listado no documento.

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

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

Em seguida, os documentos só podem 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 está impondo que o aplicativo deve adicioná-lo antes de tentar criar um documento.) Como você só precisa verificar se os atributos estão sendo criados, você pode verificar se request.resource tem todos essas chaves:

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

O requisito final 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 devem ser verdadeiras, concatene-as com o operador lógico AND, && . A primeira regra passa a ser:

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 novamente os testes e confirme se o primeiro teste foi aprovado.

5. Atualize os rascunhos das postagens do blog.

Em seguida, à medida que os autores refinarem seus rascunhos de postagens de blog, eles editarão os documentos de rascunho. Crie uma regra para as condições em que uma postagem pode ser atualizada. Primeiro, apenas o autor pode atualizar seus rascunhos. Observe que aqui você verifica o UID que já está escrito, resource.data.authorUID :

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

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

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

E, finalmente, o título deve ter 50 caracteres ou menos:

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

Como todas essas condições precisam ser atendidas, concatene-as 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 se tornam:

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 novamente seus testes e confirme se outro teste foi 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 excluir rascunhos.

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

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

request.auth.token.isModerator == true

Como qualquer uma dessas condições é suficiente para uma exclusão, concatene-as com um operador OR lógico, || :

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

As regras completas agora 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 novamente seus testes e confirme se outro teste passou.

7. Lê, cria e exclui postagens publicadas: desnormalizando para diferentes padrões de acesso

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

Em seguida, você escreverá as regras para postagens publicadas. As regras mais simples de escrever são que as postagens publicadas podem ser lidas por qualquer pessoa e não podem ser criadas ou 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;
}

Adicionando-as às regras existentes, todo o arquivo de regras 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;

      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 novamente os testes e confirme se outro teste foi aprovado.

8. Atualizando postagens publicadas: funções personalizadas e variáveis ​​locais

As condições para atualizar um post publicado são:

  • só pode ser feito pelo autor ou moderador, e
  • deve conter todos os campos obrigatórios.

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

Criar uma função personalizada

Acima da declaração de correspondência para rascunhos, crie uma nova função chamada isAuthorOrModerator que recebe como argumentos um documento de postagem (isso funcionará para rascunhos ou postagens publicadas) e o objeto de autenticação 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 devem terminar com uma declaração de retorno, e a nossa retornará um booleano indicando se alguma das variáveis ​​é verdadeira:

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

Chame a função

Agora você atualizará a regra para que os rascunhos chamem essa função, tendo o cuidado de passar 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 as 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 devem ser alterados, especificamente os campos url , authorUID e publishedAt são imutáveis. Os outros dois campos, title e content , e visible ainda devem estar presentes após uma atualização. Adicione condições para aplicar esses requisitos para atualizações de 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"
])

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

E, finalmente, adicione uma condição de que o título tenha menos de 50 caracteres. Como essa é uma lógica reutilizada, você pode fazer isso criando uma nova função, titleIsUnder50Chars . Com a nova função, a condição de atualização de um post publicado passa a 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 regras 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 novamente os testes. Neste ponto, você deve ter 5 testes de aprovação e 4 de reprovação.

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

As postagens publicadas permitem comentários e os comentários 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 a subcoleções. Você não quer que as mesmas regras que se aplicam ao documento pai da postagem publicada sejam aplicadas aos comentários; você criará diferentes.

Para escrever regras para acessar os 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

Lendo comentários: Não pode ser anônimo

Para este aplicativo, apenas usuários que criaram uma conta permanente, 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 novamente seus testes e confirme se mais um teste foi aprovado.

Criando comentários: verificando uma lista de negações

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

  • um usuário deve ter um e-mail verificado
  • o comentário deve ter menos de 500 caracteres e
  • eles não podem estar em uma lista de usuários banidos, que é armazenada no firestore na coleção bannedUsers . Tomando 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));

Todo o arquivo de regras 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 charachters
        request.resource.data.comment.size() < 500 &&
        // UID is not on the block list
        !exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
    }
  }
}

Execute novamente os testes e certifique-se de que mais um teste seja aprovado.

10. Atualizando comentários: regras baseadas em tempo

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

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

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

Em seguida, que o comentário foi criado na última hora:

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

Combinando-os 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 novamente os testes e certifique-se de que mais um teste seja aprovado.

11. Excluindo comentários: verificando a propriedade dos pais

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

Primeiro, como a função auxiliar que você adicionou anteriormente verifica um campo authorUID que pode existir em uma postagem ou em um 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 pesquisar 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 OR lógico 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 novamente os testes e certifique-se de que mais um teste seja aprovado.

E todo o arquivo de regras é:

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 charachters
        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óximos passos

Parabéns! Você escreveu as Regras de Segurança que fizeram todos os testes passarem e protegeram o aplicativo!

Aqui estão alguns tópicos relacionados para mergulhar a seguir:

  • Postagem do blog : Como revisar o código das regras de segurança
  • Codelab : caminhando pelo primeiro desenvolvimento local com os emuladores
  • Video : How to use set up CI for emulator-based tests using GitHub Actions