Controlar o acesso a campos específicos

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

Pode haver momentos em que você queira controlar as alterações em um documento não no nível do documento, mas no nível do campo.

Por exemplo, você pode querer permitir que um cliente crie ou altere um documento, mas não permitir que edite determinados campos nesse documento. Ou você pode querer impor que qualquer documento criado por um cliente sempre contenha um determinado conjunto de campos. Este guia aborda como você pode realizar algumas dessas tarefas usando as regras de segurança do Cloud Firestore.

Permitindo acesso de leitura apenas para campos específicos

As leituras no Cloud Firestore são realizadas no nível do documento. Você recupera o documento completo ou não recupera nada. Não há como recuperar um documento parcial. É impossível usar apenas regras de segurança para impedir que os usuários leiam campos específicos em um documento.

Se houver determinados campos em um documento que você deseja manter ocultos para alguns usuários, a melhor maneira seria colocá-los em um documento separado. Por exemplo, você pode considerar a criação de um documento em uma subcoleção private como esta:

/funcionários/{emp_id}

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

/empregados/{emp_id}/privado/finanças

    salary: 80000,
    bonus_mult: 1.25,
    perf_review: 4.2

Em seguida, você pode adicionar regras de segurança com diferentes níveis de acesso para as duas coleções. Neste exemplo, estamos a utilizar declarações de autenticação personalizadas para dizer que apenas os utilizadores com a role de declaração de autenticação personalizada igual a Finance podem visualizar 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'
      }
    }
  }
}

Restringindo campos na criação de documentos

O Cloud Firestore não tem esquema, o que significa que não há restrições no nível do banco de dados para os campos que um documento contém. Embora essa flexibilidade possa facilitar o desenvolvimento, haverá momentos em que você desejará garantir que os clientes só possam criar documentos que contenham campos específicos ou que não contenham outros campos.

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

Exigindo campos específicos em novos documentos

Digamos que você queira ter certeza de que todos os documentos criados em uma coleção restaurant contenham pelo menos um campo name , location e city . Você poderia fazer isso chamando hasAll() na lista de chaves do 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']);
    }
  }
}

Isto permite que restaurantes sejam criados também com outros campos, mas garante que todos os documentos criados por um cliente contenham pelo menos estes três campos.

Proibindo campos específicos em novos documentos

Da mesma forma, você pode impedir que clientes criem documentos que contenham campos específicos usando hasAny() em uma lista de campos proibidos. Este método é avaliado como verdadeiro se um documento contém algum desses campos, então você provavelmente desejará negar o resultado para proibir determinados campos.

Por exemplo, no exemplo a seguir, os clientes não têm permissão para criar um documento que contenha um campo average_score ou rating_count , pois esses campos serão adicionados por uma chamada do servidor posteriormente.

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']));
    }
  }
}

Criando uma lista de permissões de campos para novos documentos

Em vez de proibir determinados campos em novos documentos, você pode querer criar uma lista apenas dos campos que são explicitamente permitidos em novos documentos. Então você pode usar a função hasOnly() para garantir que quaisquer novos documentos criados contenham apenas esses campos (ou um subconjunto desses campos) e nenhum outro.

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']));
    }
  }
}

Combinando campos obrigatórios e opcionais

Você pode combinar operações hasAll e hasOnly em suas regras de segurança para exigir alguns campos e permitir outros. Por exemplo, este exemplo exige que todos os novos documentos contenham os campos name , location e city e, opcionalmente, permite os 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 um cenário do mundo real, você pode querer mover essa lógica para uma função auxiliar para evitar a duplicação de seu código e combinar mais facilmente os campos opcionais e obrigatórios em uma única lista, assim:

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']);
    }
  }
}

Restringindo campos na atualização

Uma prática de segurança comum é permitir que os clientes editem apenas alguns campos e não outros. Você não pode fazer isso apenas olhando a lista request.resource.data.keys() descrita na seção anterior, pois esta lista representa o documento completo como ficaria após a atualização e, portanto, incluiria campos que o cliente não fez. mudar.

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

Chamando o método affectedKeys() neste mapDiff, você pode criar um conjunto de campos que foram alterados em uma edição. Então você pode usar funções como hasOnly() ou hasAny() para garantir que este conjunto contenha (ou não) determinados itens.

Evitando que alguns campos sejam alterados

Ao usar o método hasAny() no conjunto gerado por affectedKeys() e, em seguida, negar o resultado, você pode rejeitar qualquer solicitação do cliente que tente alterar campos que você não deseja que sejam alterados.

Por exemplo, você pode permitir que os clientes atualizem informações sobre um restaurante, mas não alterem sua pontuação média ou 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']));
    }
  }
}

Permitindo que apenas alguns campos sejam alterados

Em vez de especificar campos que você não deseja alterar, você também pode usar a função hasOnly() para especificar uma lista de campos que deseja alterar. Isso geralmente é considerado mais seguro porque as gravações em quaisquer novos campos do documento não são permitidas por padrão até que você as permita explicitamente em suas regras de segurança.

Por exemplo, em vez de proibir os campos average_score e rating_count , você pode criar regras de segurança que permitam aos clientes alterar apenas os campos 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']));
    }
  }
}

Isso significa que se, em alguma iteração futura do seu aplicativo, os documentos do restaurante incluírem um campo telephone , as tentativas de editar esse campo falharão até que você volte e adicione esse campo à lista hasOnly() em suas regras de segurança.

Aplicando tipos de campo

Outro efeito do Cloud Firestore ser sem esquema é que não há aplicação no nível do banco de dados para quais tipos de dados podem ser armazenados em campos específicos. No entanto, isso é algo que você pode aplicar nas regras de segurança com o operador is .

Por exemplo, a regra de segurança a seguir impõe que o campo score de uma revisão seja um número inteiro, os campos headline , content e author_name sejam strings e review_date seja 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 is operador também suporta os tipos de dados constraint , duration , set e map_diff , mas como eles são gerados pela própria linguagem de regras de segurança e não gerados pelos clientes, você raramente os usa na maioria dos aplicativos práticos.

Os tipos de dados list e map não têm suporte para genéricos ou argumentos de tipo. Em outras palavras, você pode usar regras de segurança para garantir que um determinado campo contenha uma lista ou um mapa, mas não pode impor que um campo contenha uma lista de todos os números inteiros ou de todas as strings.

Da mesma forma, você pode usar regras de segurança para impor valores de tipo para entradas específicas em uma lista ou mapa (usando notação de freios ou nomes de chaves respectivamente), mas não há atalho para impor os tipos de dados de todos os membros em um mapa ou lista em uma 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. Ele também garante que o campo product contenha um mapa que, por sua vez, contém um nome de produto que é uma string e uma quantidade que é 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 ao criar e atualizar um documento. Portanto, você pode querer considerar a criação de uma função auxiliar que possa ser chamada nas seções de criação e atualização de suas 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
      }
    }
  }
}

Aplicando tipos para campos opcionais

É importante lembrar que chamar request.resource.data.foo em um documento onde foo não existe resulta em um erro e, portanto, qualquer regra de segurança que fizer essa chamada negará a solicitação. Você pode lidar com essa situação usando o método get em request.resource.data . O método get permite 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 revisão também contiverem um campo opcional photo_url e um campo opcional tags que você deseja verificar se são strings e listas respectivamente, você pode fazer isso reescrevendo a função reviewFieldsAreValidTypes para algo como o seguinte:

  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;
  }

Isso rejeita documentos onde existem tags , mas não é uma lista, ao mesmo tempo que permite documentos que não contêm um campo tags (ou photo_url ).

Gravações parciais nunca são permitidas

Uma observação final sobre as regras de segurança do Cloud Firestore é que elas permitem que o cliente faça uma alteração em um documento ou rejeitam toda a edição. Você não pode criar regras de segurança que aceitem gravações em alguns campos do seu documento e rejeitem outros na mesma operação.