Controle o acesso a campos específicos
Esta página baseia-se nos conceitos em Estruturar regras de segurança e Escrever condições para regras de segurança para explicar como pode usar regras de segurança do Firestore para criar regras que permitam aos clientes realizar operações em alguns campos num documento, mas não noutros.
Por vezes, pode querer controlar as alterações a um documento não ao nível do documento, mas ao nível do campo.
Por exemplo, pode querer permitir que um cliente crie ou altere um documento, mas não permitir que edite determinados campos nesse documento. Em alternativa, pode querer aplicar a todos os documentos criados por um cliente um determinado conjunto de campos. Este guia aborda a forma como pode realizar algumas destas tarefas através das regras de segurança do Firestore.
Permitir acesso de leitura apenas para campos específicos
As leituras no Firestore são realizadas ao nível do documento. Recupera o documento completo ou não recupera nada. Não é possível obter um documento parcial. É impossível usar apenas regras de segurança para impedir que os utilizadores leiam campos específicos num documento.
Se existirem determinados campos num documento que quer manter ocultos de alguns utilizadores, a melhor forma é colocá-los num documento separado. Por exemplo, pode considerar criar um documento numa subcoleção private
da seguinte 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, pode adicionar regras de segurança com diferentes níveis de acesso para as duas coleções. Neste exemplo, estamos a usar afirmações de autorização personalizadas
para indicar que apenas os utilizadores com a afirmação de autorização personalizada 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'
}
}
}
}
Restringir campos na criação de documentos
O Firestore não tem esquemas, o que significa que não existem restrições ao nível da base de dados quanto aos campos que um documento contém. Embora esta flexibilidade possa facilitar o desenvolvimento, haverá momentos em que vai querer garantir que os clientes só podem criar documentos que contenham campos específicos ou que não contenham outros campos.
Pode criar estas regras examinando o método keys
do objeto request.resource.data
. Esta é uma lista de todos os campos que o cliente está a tentar escrever neste novo documento. Ao combinar este conjunto de campos com funções como hasOnly()
ou hasAny()
, pode adicionar lógica que restringe os tipos de documentos que um utilizador pode adicionar ao Firestore.
Exigir campos específicos em novos documentos
Suponhamos que quer certificar-se de que todos os documentos criados numa coleção restaurant
contêm, pelo menos, um campo name
, location
e city
. Pode fazê-lo chamando 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']);
}
}
}
Isto permite que os restaurantes sejam criados também com outros campos, mas garante que todos os documentos criados por um cliente contêm, pelo menos, estes três campos.
Proibir campos específicos em novos documentos
Da mesma forma, pode impedir que os clientes criem documentos que contenham campos específicos usando hasAny()
numa lista de campos proibidos. Este método é avaliado como verdadeiro se um documento contiver algum destes campos, pelo que é provável que queira negar o resultado para proibir determinados campos.
Por exemplo, no exemplo seguinte, os clientes não têm autorização para criar um documento que contenha um campo average_score
ou rating_count
, uma vez que estes campos vão ser adicionados por uma chamada de servidor num ponto 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']));
}
}
}
Criar uma lista de autorizações de campos para novos documentos
Em vez de proibir determinados campos em novos documentos, pode criar uma lista apenas com os campos explicitamente permitidos em novos documentos. Em seguida, pode usar a função hasOnly()
para garantir que todos os novos documentos criados contêm apenas estes campos (ou um subconjunto destes 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']));
}
}
}
Combinar campos obrigatórios e opcionais
Pode combinar operações hasAll
e hasOnly
nas regras de segurança para exigir alguns campos e permitir outros. Por exemplo, este exemplo requer que todos os novos documentos contenham os campos name
, location
e city
, e permite opcionalmente 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']));
}
}
}
Num cenário real, pode querer mover esta lógica para uma função auxiliar para evitar duplicar o código e combinar mais facilmente os campos opcionais e obrigatórios numa ú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']);
}
}
}
Restringir campos na atualização
Uma prática de segurança comum é permitir que os clientes editem apenas alguns campos e não outros. Não pode fazê-lo apenas consultando a lista request.resource.data.keys()
descrita na secção anterior, uma vez que esta lista representa o documento completo tal como ficaria após a atualização e, por conseguinte, incluiria campos que o cliente não alterou.
No entanto, se usasse a função diff()
, podia comparar request.resource.data
com o objeto resource.data
, que representa o documento na base de dados antes da atualização. Isto cria um objeto mapDiff
, que é um objeto que contém todas as alterações entre dois mapas diferentes.
Ao chamar o método affectedKeys()
neste mapDiff, pode obter um conjunto de campos que foram alterados numa edição. Em seguida, pode usar funções como
hasOnly()
ou hasAny()
para garantir que este conjunto contém (ou não) determinados itens.
Impedir a alteração de alguns campos
Ao usar o método hasAny()
no conjunto gerado por affectedKeys()
e, em seguida, negar o resultado, pode rejeitar qualquer pedido do cliente que tente alterar campos que não quer que sejam alterados.
Por exemplo, pode querer permitir que os clientes atualizem as informações sobre um restaurante, mas não alterem a respetiva classificação média nem o número de críticas.
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']));
}
}
}
Permitir a alteração apenas de determinados campos
Em vez de especificar os campos que não quer alterar, também pode usar a função
hasOnly()
para especificar uma lista de campos que quer alterar. Geralmente, isto é considerado mais seguro porque as escritas em quaisquer novos campos de documentos são proibidas por predefinição até que as permita explicitamente nas suas regras de segurança.
Por exemplo, em vez de não permitir o campo average_score
e rating_count
, 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']));
}
}
}
Isto significa que, se numa iteração futura da sua app, os documentos de restaurantes incluírem um campo telephone
, as tentativas de editar esse campo falham até voltar atrás e adicionar esse campo à lista hasOnly()
nas regras de segurança.
Aplicar tipos de campos
Outro efeito do Firestore não ter esquema é que não existe aplicação ao nível da base de dados para os tipos de dados que podem ser armazenados em campos específicos. No entanto, pode aplicar esta restrição nas regras de segurança com o operador is
.
Por exemplo, a seguinte regra de segurança aplica que o campo score
de uma crítica tem de ser um número inteiro, os campos headline
, content
e author_name
são strings e review_date
é uma 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 suporta os tipos de dados constraint
, duration
, set
e map_diff
, mas, uma vez que estes são gerados pela própria linguagem das regras de segurança e não pelos clientes, raramente os usa na maioria das aplicações práticas.
Os tipos de dados list
e map
não são compatíveis com genéricos nem argumentos de tipo.
Por outras palavras, pode usar regras de segurança para aplicar que um determinado campo contenha uma lista ou um mapa, mas não pode aplicar que um campo contenha uma lista de todos os números inteiros ou todas as strings.
Da mesma forma, pode usar regras de segurança para aplicar valores de tipo a entradas específicas numa lista ou num mapa (usando a notação de parênteses retos ou os nomes das chaves, respetivamente), mas não existe um atalho para aplicar os tipos de dados de todos os membros num mapa ou numa lista de uma só vez.
Por exemplo, as seguintes regras garantem que um campo tags
num documento contém uma lista e que a primeira entrada é uma string. Também garante que o campo product
contém um mapa que, por sua vez, contém um nome do 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 campos têm de ser aplicados quando cria e atualiza um documento. Por conseguinte, é recomendável considerar a criação de uma função auxiliar que possa chamar nas secções de criação e atualização das 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
}
}
}
}
Imposição de tipos para campos opcionais
É importante lembrar que chamar request.resource.data.foo
num documento onde foo
não existe resulta num erro e, por isso, qualquer regra de segurança que faça essa chamada nega o pedido. Pode resolver esta situação através do método get
em request.resource.data
. O método get
permite-lhe fornecer um argumento predefinido para o campo que está a obter de um mapa se esse campo não existir.
Por exemplo, se os documentos de revisão também contiverem um campo photo_url
opcional e um campo tags
opcional que quer verificar se são strings e listas, respetivamente, pode fazê-lo reescrevendo a função reviewFieldsAreValidTypes
para algo semelhante ao 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;
}
Isto rejeita documentos onde tags
existe, mas não é uma lista, ao mesmo tempo que permite documentos que não contêm um campo tags
(ou photo_url
).
As escritas parciais nunca são permitidas
Uma nota final sobre as Regras de segurança do Firestore é que permitem que o cliente faça uma alteração a um documento ou rejeitam a edição completa. 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.