1. Antes de começar
O Cloud Firestore, o Cloud Storage para 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 de regras de segurança, também pode funcionar como um tipo de esquema para o app. É uma das partes mais importantes do desenvolvimento do seu aplicativo. Este codelab contém orientações sobre isso.
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ê usará o emulador do Firestore para executar testes de unidade em relação às 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 com base em tempo
- Implementar uma lista de bloqueio e fazer exclusões reversíveis
- Entender quando desnormalizar dados para atender a vários padrões de acesso
2. Configurar
Este é um aplicativo de blog. Veja um resumo detalhado da funcionalidade do aplicativo:
Rascunhos de postagens do blog:
- Os usuários podem criar rascunhos de postagens de blog, que ficam na coleção
drafts
. - O autor pode continuar atualizando um rascunho até que ele esteja pronto para ser publicado.
- Quando estiver pronto para ser publicado, uma função do Firebase será acionada, criando 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 com 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 casos de abuso, os usuários precisam ter um endereço de e-mail verificado e não estar em uma lista de bloqueio para deixar um comentário.
- Os comentários só podem ser atualizados uma hora depois de serem postados.
- 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 vai acontecer localmente usando o Pacote de emuladores do Firebase.
Fazer o download do código-fonte
Neste codelab, você começará com testes para as regras de segurança, mas as regras de segurança menores em si. 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, vá para o diretório do estado inicial, onde você vai trabalhar no restante deste codelab:
$ cd codelab-rules/initial-state
Agora, instale as dependências para executar os testes. Se você estiver em 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 Pacote de emuladores que você vai usar para executar os testes faz parte da CLI (interface de linha de comando) do Firebase, que pode ser instalada na 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 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ê vai trabalhar tem três coleções principais do Firestore: drafts
contém postagens de blog em andamento, a coleção published
contém as postagens 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 um usuário criar, ler, atualizar e excluir documentos nas coleções drafts
, published
e comments
. Você criará as regras de segurança para que esses testes sejam aprovados.
Inicialmente, seu banco de dados é bloqueado: as leituras e gravações são negadas universalmente e todos os testes falham. Os testes são aprovados conforme você escreve as regras de segurança. Para ver os testes, abra functions/test.js
no 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é 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 a aprovação de mais testes.
4. Criar rascunhos de postagem do blog.
Como o acesso aos rascunhos de postagens do blog é muito diferente do acesso às postagens publicadas, este app de blog armazena rascunhos de postagens em uma coleção separada, /drafts
. Os rascunhos só podem ser acessados pelo autor ou moderador e têm validações para campos obrigatórios e imutáveis.
Abra o arquivo firestore.rules
para 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=**}
, está usando a sintaxe **
para ser aplicada de maneira recursiva a todos os documentos nas subcoleções. Como ele 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 de quais dados eles estão tentando ler ou gravar.
Comece removendo a instrução de correspondência mais interna e a substitua 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ê vai escrever para rascunhos controlará quem pode criar os documentos. Nesse 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
Os documentos só podem ser criados se incluírem os três campos obrigatórios: authorUID
, createdAt
e title
. O usuário não informa o campo createdAt
. Isso faz com que o app precise adicioná-lo antes de tentar criar um documento. Como você só precisa verificar se os atributos estão sendo criados, verifique se request.resource
tem todas essas chaves:
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
])
O requisito final para a criação de 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 lógico AND, &&
. A primeira regra vai ficar 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;
}
}
}
No terminal, execute os testes novamente e confirme se o primeiro foi aprovado.
5. Atualizar rascunhos de postagem do blog.
Em seguida, conforme os autores refinam os rascunhos de postagens do blog, eles editam os documentos de rascunho. Crie uma regra para as condições em que uma postagem pode ser atualizada. Primeiro, apenas o autor pode atualizar os rascunhos. Aqui você verifica o UID que já foi 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 mudem:
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]);
E, 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, 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 são as seguintes:
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 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
no 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 lógico OR, ||
:
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 os testes novamente e confirme se outro teste foi aprovado.
7. Lê, cria e exclui postagens publicadas: desnormalização para diferentes padrões de acesso
Como os padrões de acesso das postagens publicadas e de rascunho são muito diferentes, esse 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 definitivamente. Já os rascunhos podem ser excluídos, mas só podem ser lidos pelo autor e moderadores. Neste app, quando um usuário quiser publicar o rascunho de uma postagem do blog, será acionada uma função que criará a nova postagem publicada.
Em seguida, você escreverá as regras para as postagens publicadas. As regras mais simples de escrever são: 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, o arquivo de regras inteiro 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 os testes novamente e confirme se outro teste é aprovado.
8. Atualizar postagens publicadas: funções personalizadas e variáveis locais
As condições para atualizar uma postagem publicada são:
- que só pode ser feito pelo autor ou moderador, e
- ela precisa conter todos os campos obrigatórios.
Como você já escreveu condições para ser um autor ou moderador, é possível 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 de ser um autor ou moderador. Em seguida, você vai fazer a chamada usando várias condições.
Criar uma função personalizada
Acima da instrução de correspondência para rascunhos, crie uma nova função chamada isAuthorOrModerator
, que usa como argumentos um documento de postagem (isso funciona 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 instrução de retorno, e 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ê vai atualizar a regra de rascunhos para chamar essa função, tendo 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 criar uma condição para atualizar postagens publicadas que também use 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
, ainda precisam estar presentes após uma atualização. Adicione condições para aplicar os seguintes requisitos nas 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"
])
Criar uma função personalizada por conta própria
Por fim, adicione uma condição para que o título tenha 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 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 os testes novamente. A essa altura, você tem cinco testes aprovados e quatro com falha.
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 às subcoleções. Não é recomendado que as mesmas regras aplicadas ao documento pai da postagem publicada sejam aplicadas aos comentários. Você criará regras diferentes.
Para criar regras de acesso aos comentários, comece com a instrução de correspondência:
match /published/{postID}/comments/{commentID} {
// `authorUID`: string, required
// `comment`: string, < 500 characters, required
// `createdAt`: timestamp, required
// `editedAt`: timestamp, optional
Ler 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 teste foi aprovado.
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 deve ter menos de 500 caracteres;
- eles não podem estar em uma lista de usuários banidos, que é armazenada no Firestore, na coleção
bannedUsers
. Considere essas 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 a criação de 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 completo 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 confirme se mais um teste foi aprovado.
10. Atualização de comentários: regras baseadas em tempo
A lógica de negócios dos comentários é que eles podem ser editados pelo autor até uma hora após serem criados. 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
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 os testes novamente e confirme se mais um teste foi aprovado.
11. Exclusão de comentários: verificação de propriedade principal
Os comentários podem ser excluídos pelo autor, moderador ou autor da postagem no blog.
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 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 confirme se mais um teste foi 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ê criou 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 fazer a revisão de código de regras de segurança
- Codelab: como analisar o primeiro desenvolvimento local com os emuladores
- Vídeo: como usar a configuração de CI para testes baseados em emulador usando as ações do GitHub