Controlar o acesso a campos específicos

Esta página baseia-se nos conceitos de Como estruturar regras de segurança e Como gravar condições para regras de segurança para explicar como você pode usar as regras de segurança do Firestore para criar regras que permitem que os clientes realizem operações em alguns campos de um documento, mas não em outros.

Em algumas situações, talvez você queira controlar as alterações feitas no documento não no nível dele, mas no nível do campo.

Por exemplo, é possível permitir que um cliente crie ou altere um documento, mas impedir que ele edite determinados campos desse arquivo. Ou então, talvez seja interessante definir que qualquer documento criado por um cliente sempre contenha um conjunto determinado de campos. Este guia explica como realizar algumas dessas tarefas usando as regras de segurança do Firestore.

Como permitir acesso de leitura somente para campos específicos

As leituras no Firestore são feitas no nível do documento. Ou você recupera o documento completo ou nada é recuperado. Não há a opção de recuperar um documento parcial. É impossível usar regras de segurança isoladamente para impedir que os usuários leiam campos específicos de um documento.

Se você quer que determinados campos fiquem ocultos para alguns usuários, a melhor maneira de fazer isso é colocá-los em um documento separado. Por exemplo, crie um documento em uma subcoleção private desta forma:

/employees/{emp_id}

  name: "Alice Hamilton",
  department: 461,
  start_date: <timestamp>

/employees/{emp_id}/private/finances

    salary: 80000,
    bonus_mult: 1.25,
    perf_review: 4.2

Em seguida, adicione regras de segurança que tenham diferentes níveis de acesso para as duas coleções. Neste exemplo, usamos declarações de autenticação personalizadas para indicar que somente os usuários com essa declaração role igual a Finance podem ver as informações financeiras de um funcionário.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow any logged in user to view the public employee data
    match /employees/{emp_id} {
      allow read: if request.resource.auth != null
      // Allow only users with the custom auth claim of "Finance" to view
      // the employee's financial data
      match /private/finances {
        allow read: if request.resource.auth &&
          request.resource.auth.token.role == 'Finance'
      }
    }
  }
}

Como restringir campos na criação de documentos

O Firestore não tem esquemas, o que significa que não há restrições no nível do banco de dados para quais campos um documento contém. Essa flexibilidade pode facilitar o desenvolvimento, mas há momentos em que você quer garantir que os clientes só possam criar documentos que contenham campos específicos ou que não incluam outros campos.

Para criar essas regras, analise o método keys do objeto request.resource.data. Essa é uma lista com todos os campos que o cliente está tentando gravar no novo documento. Ao combinar esse conjunto de campos com funções como hasOnly() ou hasAny(), você pode adicionar uma lógica que restringe a tipos de documentos que um usuário pode adicionar ao Firestore.

Como exigir campos específicos em novos documentos

Digamos que você queira garantir que todos os documentos criados em uma coleção restaurant contenham pelo menos um campo name, location e city. Para fazer isso, chame hasAll() na lista de chaves no novo documento.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document contains a name
    // location, and city field
    match /restaurant/{restId} {
      allow create: if request.resource.data.keys().hasAll(['name', 'location', 'city']);
    }
  }
}

Assim, você permite que os restaurantes também sejam gerados com outros campos, mas garante que todos os documentos criados por um cliente incluam pelo menos esses três campos.

Como proibir campos específicos em novos documentos

Da mesma forma, é possível evitar que os clientes criem documentos contendo campos específicos ao usar hasAny() em uma lista de campos proibidos. Esse método avalia como "verdadeiro" se um documento contém qualquer um desses campos. Portanto, você precisará negar o resultado para proibir alguns campos.

No exemplo a seguir, os clientes não têm permissão para criar um documento contendo um campo average_score ou rating_count, já que esses campos serão adicionados por uma chamada de servidor em um momento posterior.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document does *not*
    // contain an average_score or rating_count field.
    match /restaurant/{restId} {
      allow create: if (!request.resource.data.keys().hasAny(
        ['average_score', 'rating_count']));
    }
  }
}

Como criar uma lista de permissões de campos para novos documentos

Em vez de proibir determinados campos em novos documentos, é recomendado criar uma lista que contenha apenas os campos explicitamente permitidos em novos documentos. Em seguida, use a função hasOnly() para garantir que os documentos criados incluam apenas esses campos (ou um subconjunto deles) e nada mais.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document doesn't contain
    // any fields besides the ones listed below.
    match /restaurant/{restId} {
      allow create: if (request.resource.data.keys().hasOnly(
        ['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

Como combinar campos obrigatórios e opcionais

É possível combinar as operações hasAll e hasOnly nas suas regras de segurança para que alguns campos sejam obrigatórios e outros, permitidos. Neste exemplo, todos os novos documentos precisam conter os campos name, location e city e permitem a existência dos campos address, hours e cuisine.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document has a name,
    // location, and city field, and optionally address, hours, or cuisine field
    match /restaurant/{restId} {
      allow create: if (request.resource.data.keys().hasAll(['name', 'location', 'city'])) &&
       (request.resource.data.keys().hasOnly(
           ['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

Em uma situação real, é possível mover essa lógica para uma função auxiliar e, assim, evitar a duplicação do código, além de facilitar a combinação entre os campos obrigatórios e opcionais em uma única lista da seguinte forma:

service cloud.firestore {
  match /databases/{database}/documents {
    function verifyFields(required, optional) {
      let allAllowedFields = required.concat(optional);
      return request.resource.data.keys().hasAll(required) &&
        request.resource.data.keys().hasOnly(allAllowedFields);
    }
    match /restaurant/{restId} {
      allow create: if verifyFields(['name', 'location', 'city'],
        ['address', 'hours', 'cuisine']);
    }
  }
}

Como restringir campos em atualizações

Uma prática comum de segurança é permitir que os clientes editem somente alguns campos. Não é possível realizar essa ação apenas analisando a lista request.resource.data.keys() descrita na seção anterior, já que ela representa como o documento completo ficaria depois da atualização e, portanto, inclui campos que o cliente não alterou.

Porém, se você usar a função diff(), poderá comparar request.resource.data com o objeto resource.data, que representa o documento no banco de dados antes da atualização. Essa ação cria um objeto mapDiff, que contém todas as alterações entre dois mapas diferentes.

Ao chamar o método affectedKeys() nesse mapDiff, você cria um conjunto dos campos que foram alterados em uma edição. Em seguida, use funções como hasOnly() ou hasAny() para garantir que esse conjunto contenha (ou não) determinados itens.

Como impedir que alguns campos sejam alterados

Quando você usa o método hasAny() no conjunto gerado por affectedKeys() e nega o resultado, são rejeitadas todas as solicitações de cliente que tenham o intuito de mudar os campos que você não quer que sejam alterados.

Por exemplo, pode ser interessante que os clientes consigam atualizar informações sobre um restaurante, mas que não sejam capazes de alterar a pontuação média ou o número de avaliações.

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
      // Allow the client to update a document only if that document doesn't
      // change the average_score or rating_count fields
      allow update: if (!request.resource.data.diff(resource.data).affectedKeys()
        .hasAny(['average_score', 'rating_count']));
    }
  }
}

Como permitir que apenas determinados campos sejam alterados

Em vez de especificar os campos que você não quer alterar, uma alternativa é usar a função hasOnly() para especificar uma lista de campos que possam ser alterados. Essa opção geralmente é considerada mais segura porque, por padrão, não é possível fazer gravações em novos campos do documento até que essa ação seja explicitamente permitida nas regras de segurança.

Por exemplo, em vez de proibir mudanças nos campos average_score e rating_count, crie regras de segurança que permitam que os clientes alterem apenas name, location, city, address, hours e cuisine.

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
    // Allow a client to update only these 6 fields in a document
      allow update: if (request.resource.data.diff(resource.data).affectedKeys()
        .hasOnly(['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

Assim, se em uma iteração futura do aplicativo os documentos do restaurante incluírem um campo telephone, ninguém conseguirá editar esse campo até que você volte e o adicione à lista hasOnly() nas regras de segurança.

Como aplicar tipos de campo

Outro efeito do Firestore ser sem esquema é que não há aplicação no nível de banco de dados sobre quais tipos de dados podem ser armazenados em campos específicos. No entanto, é possível aplicar isso às regras de segurança com o operador is.

Por exemplo, de acordo com a regra de segurança a seguir, o campo score de uma avaliação precisa ser um número inteiro, os campos headline, content e author_name são strings, e o campo review_date é um carimbo de data/hora.

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
      // Restaurant rules go here...
      match /review/{reviewId} {
        allow create: if (request.resource.data.score is int &&
          request.resource.data.headline is string &&
          request.resource.data.content is string &&
          request.resource.data.author_name is string &&
          request.resource.data.review_date is timestamp
        );
      }
    }
  }
}

Os tipos de dados válidos para o operador is são bool, bytes, float, int, list, latlng, number, path, map, string e timestamp. O operador is também é compatível com os tipos de dados constraint, duration, set e map_diff. Porém, como eles são gerados pela linguagem das regras de segurança e não pelos clientes, raramente serão usados na maioria das situações práticas.

Os tipos de dados list e map não são compatíveis com argumentos genéricos ou de tipo. Em outras palavras, é viável usar regras de segurança para determinar que um campo específico contém uma lista ou um mapa, mas não é possível definir que um campo inclui uma lista de todos os números inteiros ou todas as strings.

Da mesma forma, as regras de segurança podem ser usadas para aplicar valores de tipo a entradas específicas em uma lista ou um mapa (usando a notação em colchetes ou os nomes de chaves, respectivamente), mas não há atalho para aplicar os tipos de dados de todos os membros em um mapa ou uma lista de uma só vez.

Por exemplo, as regras a seguir garantem que um campo tags em um documento contenha uma lista e que a primeira entrada seja uma string. Elas também garantem que o campo product inclua um mapa que, por sua vez, contenha um nome de produto que seja uma string e uma quantidade representada por um número inteiro.

service cloud.firestore {
  match /databases/{database}/documents {
  match /orders/{orderId} {
    allow create: if request.resource.data.tags is list &&
      request.resource.data.tags[0] is string &&
      request.resource.data.product is map &&
      request.resource.data.product.name is string &&
      request.resource.data.product.quantity is int
      }
    }
  }
}

Os tipos de campo precisam ser aplicados na criação e atualização de um documento. Portanto, avalie criar uma função auxiliar que possa ser chamada nas seções de criação e atualização das regras de segurança.

service cloud.firestore {
  match /databases/{database}/documents {

  function reviewFieldsAreValidTypes(docData) {
     return docData.score is int &&
          docData.headline is string &&
          docData.content is string &&
          docData.author_name is string &&
          docData.review_date is timestamp;
  }

   match /restaurant/{restId} {
      // Restaurant rules go here...
      match /review/{reviewId} {
        allow create: if reviewFieldsAreValidTypes(request.resource.data) &&
          // Other rules may go here
        allow update: if reviewFieldsAreValidTypes(request.resource.data) &&
          // Other rules may go here
      }
    }
  }
}

Como aplicar tipos em campos opcionais

É importante lembrar que chamar request.resource.data.foo em um documento em que foo não existe gera um erro. Assim, qualquer regra de segurança que faça essa chamada negará a solicitação. Para lidar com essa situação, use o método get em request.resource.data. O método get permitirá fornecer um argumento padrão para o campo que você está recuperando de um mapa caso esse campo não exista.

Por exemplo, se os documentos de avaliação também contêm campos photo_url e tags opcionais e você quer verificar se eles são strings e listas, respectivamente, é possível fazer isso ao reescrever a função reviewFieldsAreValidTypes da seguinte forma:

  function reviewFieldsAreValidTypes(docData) {
     return docData.score is int &&
          docData.headline is string &&
          docData.content is string &&
          docData.author_name is string &&
          docData.review_date is timestamp &&
          docData.get('photo_url', '') is string &&
          docData.get('tags', []) is list;
  }

Essa ação rejeita documentos em que tags existe, mas não é uma lista e continua permitindo documentos que não contenham um campo tags (ou photo_url).

Gravações parciais nunca são permitidas

Um último detalhe sobre as regras de segurança do Firestore é que os clientes podem fazer uma mudança em um documento ou rejeitar toda a edição. Não é possível criar regras de segurança que aceitam gravações em alguns campos do documento e, ao mesmo tempo, rejeitam outros na mesma operação.