Como consultar dados com segurança

Nesta página, você verá como as regras de segurança do Firestore interagem com as consultas com base nos conceitos de Como estruturar regras de segurança e Como gravar condições para regras de segurança. Vamos examinar em detalhes como as regras de segurança afetam as consultas que você pode escrever. Além disso, vamos saber como garantir que suas consultas usem as mesmas restrições que suas regras de segurança. Esta página também descreve como escrever regras de segurança para permitir ou negar consultas com base nas propriedades dela, como limit e orderBy.

Regras não são filtros

Ao escrever consultas para recuperar documentos, lembre-se de que regras de segurança não são filtros. As consultas podem falhar ou serem bem-sucedidas. Para economizar tempo e recursos, o Firestore avalia uma consulta em relação ao possível conjunto de resultados, em vez dos valores de campo reais de todos os documentos. Se for possível que uma consulta retorne documentos para os quais o cliente não tem permissão de leitura, toda a solicitação falhará.

Consultas e regras de segurança

Como demonstrado nos exemplos abaixo, é necessário escrever consultas que atendam às restrições das suas regras de segurança.

Proteger e consultar documentos com base em auth.uid

Veja no exemplo a seguir como escrever uma consulta para recuperar documentos protegidos por uma regra de segurança. Considere um banco de dados que contém uma coleção de documentos story:

/stories/{storyid}

{
  title: "A Great Story",
  content: "Once upon a time...",
  author: "some_auth_id",
  published: false
}

Além dos campos title e content, cada documento armazena os campos author e published para usar no controle de acesso. Nestes exemplos, o aplicativo usa o Firebase Authentication para definir o campo author como o UID do usuário que criou o documento. O Firebase Authentication também preenche a variável request.auth nas regras de segurança.

A regra de segurança a seguir usa as variáveis request.auth e resource.data para restringir o acesso de leitura e gravação para cada story ao autor:

service cloud.firestore {
  match /databases/{database}/documents {
    match /stories/{storyid} {
      // Only the authenticated user who authored the document can read or write
      allow read, write: if request.auth != null && request.auth.uid == resource.data.author;
    }
  }
}

Suponha que seu aplicativo inclua uma página que mostra ao usuário uma lista de documentos story criados por ele. Use a consulta a seguir para preencher essa página. No entanto, a consulta falhará, porque ela não inclui as mesmas restrições que suas regras de segurança:

Inválido: as restrições da consulta não correspondem às das regras de segurança.

// This query will fail
db.collection("stories").get()

A consulta falhará mesmo se o usuário atual realmente for o autor de todos os documentos story. O motivo para esse comportamento é que, quando o Firestore aplica suas regras de segurança, ele avalia a consulta de acordo com o conjunto de resultados potenciais dela, e não em relação às propriedades reais dos documentos no seu banco de dados. Se uma consulta puder potencialmente incluir documentos que violam as regras de segurança, ela falhará.

Em contraste, a seguinte consulta é bem-sucedida, porque inclui a mesma restrição no campo author que as regras de segurança:

Válido: as restrições da consulta correspondem às restrições das regras de segurança.

var user = firebase.auth().currentUser;

db.collection("stories").where("author", "==", user.uid).get()

Proteger e consultar documentos com base em um campo

Para demonstrar melhor a interação entre consultas e regras, as regras de segurança abaixo expandem o acesso de leitura para a coleção de stories para permitir que qualquer usuário leia documentos de story quando o campo published estiver definido como true.

service cloud.firestore {
  match /databases/{database}/documents {
    match /stories/{storyid} {
      // Anyone can read a published story; only story authors can read unpublished stories
      allow read: if resource.data.published == true || (request.auth != null && request.auth.uid == resource.data.author);
      // Only story authors can write
      allow write: if request.auth != null && request.auth.uid == resource.data.author;
    }
  }
}

A consulta a páginas publicadas precisa incluir as mesmas restrições que as regras de segurança:

db.collection("stories").where("published", "==", true).get()

A restrição de consulta .where("published", "==", true) garante que resource.data.published é true para qualquer resultado. Portanto, essa consulta satisfaz as regras de segurança e pode ler os dados.

Consultas in e array-contains-any

Ao avaliar uma cláusula de consulta in ou array-contains-any em um conjunto de regras, o Firestore avalia cada valor de comparação separadamente. Cada valor de comparação precisa atender às restrições da regra de segurança. Por exemplo, para a seguinte regra:

match /mydocuments/{doc} {
  allow read: if resource.data.x > 5;
}

Inválido: a consulta não garante que x > 5 para todos os documentos em potencial.

// This query will fail
db.collection("mydocuments").where("x", "in", [1, 3, 6, 42, 99]).get()

Válido: a consulta garante que x > 5 para todos os documentos em potencial.

db.collection("mydocuments").where("x", "in", [6, 42, 99, 105, 200]).get()

Como avaliar restrições em consultas

Suas regras de segurança também podem aceitar ou negar consultas com base nas próprias restrições. A variável request.query contém as propriedades limit, offset e orderBy de uma consulta. Por exemplo, suas regras de segurança podem negar qualquer consulta que não limite o número máximo de documentos recuperados a um determinado intervalo:

allow list: if request.query.limit <= 10;

O conjunto de regras a seguir demonstra como escrever regras de segurança que avaliam restrições incluídas em consultas. Este exemplo expande o conjunto de regras stories anterior com as seguintes alterações:

  • O conjunto de regras separa a regra de leitura em regras get e list.
  • A regra get restringe a recuperação de documentos únicos a documentos públicos ou documentos de autoria do usuário.
  • A regra list aplica as mesmas restrições que get, mas para consultas. Ela também verifica o limite da consulta e nega qualquer consulta sem limite ou com um limite superior a 10.
  • O conjunto de regras define uma função authorOrPublished() para evitar a duplicação de código.
service cloud.firestore {

  match /databases/{database}/documents {

    match /stories/{storyid} {

      // Returns `true` if the requested story is 'published'
      // or the user authored the story
      function authorOrPublished() {
        return resource.data.published == true || request.auth.uid == resource.data.author;
      }

      // Deny any query not limited to 10 or fewer documents
      // Anyone can query published stories
      // Authors can query their unpublished stories
      allow list: if request.query.limit <= 10 &&
                     authorOrPublished();

      // Anyone can retrieve a published story
      // Only a story's author can retrieve an unpublished story
      allow get: if authorOrPublished();

      // Only a story's author can write to a story
      allow write: if request.auth.uid == resource.data.author;
    }

  }
}

Regras de segurança e consultas do grupo de coleções

Por padrão, o escopo das consultas é uma única coleção, que recupera resultados apenas dessa coleção. Com as consultas do grupo de coleções, é possível recuperar os resultados de um grupo que inclui todas as coleções com o mesmo ID. Esta seção descreve como usar regras de segurança para proteger suas consultas do grupo de coleções.

Proteger e consultar documentos com base em grupos de coleções

Nas regras de segurança, é necessário permitir explicitamente as consultas do grupo de coleções. Para isso, grave uma regra para o grupo de coleções:

  1. Verifique se rules_version = '2'; é a primeira linha do seu conjunto de regras. As consultas do grupo de coleções exigem o comportamento do novo caractere curinga recursivo {name=**} das regras de segurança versão 2.
  2. Escreva uma regra para seu grupo de coleções usando match /{path=**}/[COLLECTION_ID]/{doc}.

Por exemplo, pense em um fórum organizado em documentos forum que contenham subcoleções posts:

/forums/{forumid}/posts/{postid}

{
  author: "some_auth_id",
  authorname: "some_username",
  content: "I just read a great story.",
}

Nesse aplicativo, tornamos as postagens editáveis pelos proprietários e legíveis para os usuários autenticados:

service cloud.firestore {
  match /databases/{database}/documents {
    match /forums/{forumid}/posts/{post} {
      // Only authenticated users can read
      allow read: if request.auth != null;
      // Only the post author can write
      allow write: if request.auth != null && request.auth.uid == resource.data.author;
    }
  }
}

Qualquer usuário autenticado pode recuperar as postagens de qualquer fórum:

db.collection("forums/technology/posts").get()

E se você quiser mostrar ao usuário atual as postagens dele em todos os fóruns? É possível usar uma consulta de grupo de coleções para recuperar os resultados de todas as coleções posts:

var user = firebase.auth().currentUser;

db.collectionGroup("posts").where("author", "==", user.uid).get()

Nas regras de segurança, é preciso permitir essa consulta gravando uma regra de leitura ou de lista para o grupo de coleções posts:

rules_version = '2';
service cloud.firestore {

  match /databases/{database}/documents {
    // Authenticated users can query the posts collection group
    // Applies to collection queries, collection group queries, and
    // single document retrievals
    match /{path=**}/posts/{post} {
      allow read: if request.auth != null;
    }
    match /forums/{forumid}/posts/{postid} {
      // Only a post's author can write to a post
      allow write: if request.auth != null && request.auth.uid == resource.data.author;

    }
  }
}

Essas regras serão aplicadas a todas as coleções com o ID posts, independentemente da hierarquia. Por exemplo, elas serão aplicadas a todas as seguintes coleções posts:

  • /posts/{postid}
  • /forums/{forumid}/posts/{postid}
  • /forums/{forumid}/subforum/{subforumid}/posts/{postid}

Proteger consultas do grupo de coleções com base em um campo

Assim como as consultas de coleção única, as consultas de grupo de coleções também precisam atender às restrições definidas pelas regras de segurança. Por exemplo, podemos adicionar um campo published a cada postagem do fórum, como fizemos no exemplo stories acima:

/forums/{forumid}/posts/{postid}

{
  author: "some_auth_id",
  authorname: "some_username",
  content: "I just read a great story.",
  published: false
}

Nesse momento, podemos escrever regras para o grupo de coleções posts com base no status published e na postagem author:

rules_version = '2';
service cloud.firestore {

  match /databases/{database}/documents {

    // Returns `true` if the requested post is 'published'
    // or the user authored the post
    function authorOrPublished() {
      return resource.data.published == true || request.auth.uid == resource.data.author;
    }

    match /{path=**}/posts/{post} {

      // Anyone can query published posts
      // Authors can query their unpublished posts
      allow list: if authorOrPublished();

      // Anyone can retrieve a published post
      // Authors can retrieve an unpublished post
      allow get: if authorOrPublished();
    }

    match /forums/{forumid}/posts/{postid} {
      // Only a post's author can write to a post
      allow write: if request.auth.uid == resource.data.author;
    }
  }
}

Com essas regras, os clientes da Web, da Apple e do Android podem fazer as seguintes consultas:

  • Qualquer pessoa pode recuperar postagens publicadas em um fórum:

    db.collection("forums/technology/posts").where('published', '==', true).get()
    
  • Qualquer pessoa pode recuperar as postagens publicadas por um autor em todos os fóruns:

    db.collectionGroup("posts").where("author", "==", "some_auth_id").where('published', '==', true).get()
    
  • Os autores podem recuperar todas as postagens que publicaram e cancelaram a publicação em todos os fóruns:

    var user = firebase.auth().currentUser;
    
    db.collectionGroup("posts").where("author", "==", user.uid).get()
    

Proteger e consultar documentos com base no grupo de coleções e no caminho do documento

Em alguns casos, é recomendado restringir as consultas do grupo de coleções com base no caminho do documento. Se quiser criar essas restrições, use as mesmas técnicas adotadas para proteger e consultar documentos com base em um campo.

Pense em um aplicativo que rastreia as transações de cada usuário em várias criptomoedas e bolsas de valores:

/users/{userid}/exchange/{exchangeid}/transactions/{transaction}

{
  amount: 100,
  exchange: 'some_exchange_name',
  timestamp: April 1, 2019 at 12:00:00 PM UTC-7,
  user: "some_auth_id",
}

Observe o campo user. Sabemos a que usuário pertence um documento transaction do caminho de documento, mas duplicamos essas informações em cada documento transaction porque assim podemos realizar duas ações:

  • Gravar consultas do grupo de coleções restritas a documentos que incluam um /users/{userid} específico no caminho do documento. Por exemplo:

    var user = firebase.auth().currentUser;
    // Return current user's last five transactions across all exchanges
    db.collectionGroup("transactions").where("user", "==", user).orderBy('timestamp').limit(5)
    
  • Aplique essa restrição a todas as consultas no grupo de coleções transactions para que um usuário não possa recuperar documentos transaction de outro usuário.

Aplicamos essa restrição nas nossas regras de segurança e incluímos validação de dados para o campo user:

rules_version = '2';
service cloud.firestore {

  match /databases/{database}/documents {

    match /{path=**}/transactions/{transaction} {
      // Authenticated users can retrieve only their own transactions
      allow read: if resource.data.user == request.auth.uid;
    }

    match /users/{userid}/exchange/{exchangeid}/transactions/{transaction} {
      // Authenticated users can write to their own transactions subcollections
      // Writes must populate the user field with the correct auth id
      allow write: if userid == request.auth.uid && request.data.user == request.auth.uid
    }
  }
}

A seguir