Consultar dados com segurança

Esta página foi desenvolvida com base nos conceitos de Como estruturar regras de segurança e Como gravar condições para regras de segurança para explicar como Cloud Firestore Security Rules interage com consultas. 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. Em seguida, também veremos 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 Cloud Firestore avalia uma consulta com relação ao potencial conjunto de resultados em vez dos valores de campo reais de todos os seus 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 no 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. Após analisar os exemplos, é possível concluir que 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.

Na regra de segurança a seguir, são usadas as variáveis request.auth e resource.data para restringir o acesso de leitura e gravação de cada story ao respectivo 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 o aplicativo inclua uma página que mostra ao usuário uma lista de documentos story de autoria própria. Você pode pensar que é possível usar a seguinte consulta para preencher esta 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 desse comportamento é que, quando o Cloud Firestore aplica suas regras de segurança, ele avalia a consulta em relação ao conjunto de resultados em potencial, não às propriedades reais dos documentos no 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.

OR consultas

Ao avaliar uma consulta OR lógica (or, in ou array-contains-any) em relação a um conjunto de regras, o Cloud Firestore avalia cada valor de comparação separadamente. Cada valor de comparação precisa obedecer às restrições da regra de segurança. Por exemplo, para a regra a seguir:

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.

// These queries will fail
query(db.collection("mydocuments"),
      or(where("x", "==", 1),
         where("x", "==", 6)
      )
    )

query(db.collection("mydocuments"),
      where("x", "in", [1, 3, 6, 42, 99])
    )

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

query(db.collection("mydocuments"),
      or(where("x", "==", 6),
         where("x", "==", 42)
      )
    )

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

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.
  • O 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, as consultas têm escopo delimitado a uma única coleção e recuperam apenas os resultados dela. 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 suas regras de segurança, você precisa permitir explicitamente as consultas do grupo de coleções. Para isso, crie uma regra para esse grupo:

  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=**} da versão 2 das regras de segurança.
  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;
    }
  }
}

Todos os usuários autenticados podem 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? Para fazer isso, use uma consulta do 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 suas regras de segurança, é preciso permitir essa consulta. Para fazer isso, escreva 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 uma única coleção, as consultas do grupo de coleções também precisam atender às restrições definidas nas suas 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, Apple e 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)
    
  • Aplicar essa restrição a todas as consultas no grupo de coleções transactions para que um usuário não consiga 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
    }
  }
}

Próximas etapas