Este guia foi desenvolvido com base no guia Aprender a linguagem principal das regras de segurança do Firebase para mostrar como adicionar condições às suas regras de segurança do Firebase Realtime Database.
O principal elemento básico das regras de segurança do Realtime Database é a condição. Uma condição é uma expressão booleana que determina se uma operação específica deve ser permitida ou negada. Para regras básicas, o uso de literais true
e false
como condições funciona muito bem. No entanto, a linguagem de regras de segurança do Realtime Database oferece maneiras de escrever condições mais complexas que podem:
- Verificar a autenticação do usuário
- Avalie dados existentes com dados recém-enviados
- Acessar e comparar diferentes partes do banco de dados
- Validar dados recebidos
- Usar a estrutura de consultas de entrada para lógica de segurança
Usar variáveis $ para capturar segmentos de caminhos
É possível capturar partes do caminho para uma leitura ou gravação declarando variáveis de captura com o prefixo $
.
Isso funciona como um cartão de acesso e armazena o valor dessa chave para uso dentro das condições das regras:
{ "rules": { "rooms": { // this rule applies to any child of /rooms/, the key for each room id // is stored inside $room_id variable for reference "$room_id": { "topic": { // the room's topic can be changed if the room id has "public" in it ".write": "$room_id.contains('public')" } } } } }
As variáveis $
dinâmicas também podem ser usadas paralelamente com nomes de caminhos de constantes. Neste exemplo, a variável $other
está sendo usada para declarar uma regra.validate
que garante que widget
não tenha filhos além de title
e color
.
Qualquer gravação que resulte na criação de um filho adicional falhará.
{ "rules": { "widget": { // a widget can have a title or color attribute "title": { ".validate": true }, "color": { ".validate": true }, // but no other child paths are allowed // in this case, $other means any key excluding "title" and "color" "$other": { ".validate": false } } } }
Autenticação
Um dos padrões de regras de segurança mais comuns é o controle do acesso com base no estado de autenticação do usuário. Por exemplo, seu aplicativo pode permitir que apenas usuários conectados gravem dados:
Se o aplicativo usa o Firebase Authentication, a variável request.auth
contém as informações de autenticação para o cliente que está solicitando os dados.
Para mais informações sobre request.auth
, consulte a documentação
de referência.
O Firebase Authentication se integra ao Firebase Realtime Database para permitir o controle do acesso aos
dados por usuário com condições. Quando um usuário é autenticado, a variável
auth
nas regras de segurança do Realtime
Database é preenchida com as informações dele. Essas informações incluem o identificador exclusivo (uid
),
os dados da conta vinculada, como um ID do Facebook ou um endereço de e-mail, e outras
informações. Se um provedor de autenticação personalizado for implementado, será possível adicionar seus próprios campos
ao payload de autenticação do usuário.
Nesta seção, explicamos como combinar a linguagem das regras de segurança do Firebase Realtime Database com as informações de autenticação sobre seus usuários. Ao combinar esses dois conceitos, você pode controlar o acesso aos dados com base na identidade do usuário.
Variável auth
A variável auth
predefinida nas regras é nula antes da autenticação.
Quando um usuário é autenticado com o Firebase Authentication, ele recebe os seguintes atributos:
provedor | O método de autenticação utilizado ("senha", "anônimo", "facebook", "github", "google" ou "twitter"). |
uid | Um ID de usuário único em todos os provedores. |
token |
O conteúdo do token de ID do Firebase Auth. Consulte a documentação de referência de auth.token para mais detalhes.
|
Veja um exemplo de regra que usa a variável auth
para garantir que cada usuário só possa gravar em um caminho específico:
{ "rules": { "users": { "$user_id": { // grants write access to the owner of this user account // whose uid must exactly match the key ($user_id) ".write": "$user_id === auth.uid" } } } }
Estruturação do banco de dados para aceitar condições de autenticação
Em geral, é útil estruturar o banco de dados de maneira a facilitar a gravação de Rules. Um padrão comum para armazenar dados do usuário no Realtime Database é
armazenar todos os usuários em um único nó users
com filhos que consistem nos
valores de uid
para cada usuário. Se você quiser restringir o acesso a esses dados para que apenas o usuário conectado os veja, as regras seriam semelhantes a:
{ "rules": { "users": { "$uid": { ".read": "auth !== null && auth.uid === $uid" } } } }
Trabalhar com declarações personalizadas de autenticação
Para apps que exigem controle de acesso personalizado para diferentes usuários, o Firebase Authentication
permite que os desenvolvedores definam declarações em um usuário do Firebase.
Essas declarações são acessíveis na variável auth.token
das regras.
Confira um exemplo de regras que usam a declaração
personalizada hasEmergencyTowel
:
{ "rules": { "frood": { // A towel is about the most massively useful thing an interstellar // hitchhiker can have ".read": "auth.token.hasEmergencyTowel === true" } } }
Desenvolvedores que criam os próprios tokens de autenticação personalizados podem incluir declarações neles. Essas
declarações estão disponíveis na variável auth.token
nas regras.
Dados existentes x dados novos
A variável data
predefinida é usada para consultar os dados antes de ocorrer uma operação de gravação. Por outro lado, a variável newData
contém os novos dados que existirão se a operação de gravação for bem-sucedida.
newData
representa o resultado mesclado dos novos dados que estão sendo gravados e os dados atuais.
Por exemplo, esta regra permite criar novos registros ou excluir os existentes, mas não permite fazer mudanças nos dados não nulos existentes:
// we can write as long as old data or new data does not exist // in other words, if this is a delete or a create, but not an update ".write": "!data.exists() || !newData.exists()"
Referenciar dados em outros caminhos
Todos os dados podem ser usados como critério para regras. Usando as variáveis predefinidas root
, data
e newData
, podemos acessar qualquer caminho da forma como ele existiria antes ou depois de um evento de gravação.
Considere este exemplo, que permite operações de gravação desde que o valor do nó /allow_writes/
seja true
, o nó pai não tenha um sinalizador readOnly
definido e haja um filho chamado foo
nos dados recém-gravados:
".write": "root.child('allow_writes').val() === true && !data.parent().child('readOnly').exists() && newData.child('foo').exists()"
Validação de dados
Deve-se aplicar estruturas de dados e validar o formato e o conteúdo dos dados usando as regras .validate
, que são executadas somente após uma regra .write
conceder acesso. Veja abaixo um exemplo de definição de regra .validate
que permite datas somente no formato AAAA-MM-DD entre os anos de 1900 e 2099, o que é verificado usando uma expressão regular.
".validate": "newData.isString() && newData.val().matches(/^(19|20)[0-9][0-9][-\\/. ](0[1-9]|1[012])[-\\/. ](0[1-9]|[12][0-9]|3[01])$/)"
As regras .validate
são o único tipo de regra de segurança que não são aplicadas em cascata. Se qualquer regra de validação falhar em algum registro filho, toda a operação de gravação será rejeitada.
Além disso, as definições de validação são ignoradas quando os dados são excluídos, ou seja, quando o novo valor que está sendo gravado é null
.
Esses podem parecer pontos triviais, mas, na verdade, são recursos importantes para definir regras avançadas de segurança do Firebase Realtime Database. Suponha as seguintes regras:
{ "rules": { // write is allowed for all paths ".write": true, "widget": { // a valid widget must have attributes "color" and "size" // allows deleting widgets (since .validate is not applied to delete rules) ".validate": "newData.hasChildren(['color', 'size'])", "size": { // the value of "size" must be a number between 0 and 99 ".validate": "newData.isNumber() && newData.val() >= 0 && newData.val() <= 99" }, "color": { // the value of "color" must exist as a key in our mythical // /valid_colors/ index ".validate": "root.child('valid_colors/' + newData.val()).exists()" } } } }
Com esta variante em mente, analise os resultados das operações de gravação a seguir:
JavaScript
var ref = db.ref("/widget"); // PERMISSION_DENIED: does not have children color and size ref.set('foo'); // PERMISSION DENIED: does not have child color ref.set({size: 22}); // PERMISSION_DENIED: size is not a number ref.set({ size: 'foo', color: 'red' }); // SUCCESS (assuming 'blue' appears in our colors list) ref.set({ size: 21, color: 'blue'}); // If the record already exists and has a color, this will // succeed, otherwise it will fail since newData.hasChildren(['color', 'size']) // will fail to validate ref.child('size').set(99);
Objective-C
FIRDatabaseReference *ref = [[[FIRDatabase database] reference] child: @"widget"]; // PERMISSION_DENIED: does not have children color and size [ref setValue: @"foo"]; // PERMISSION DENIED: does not have child color [ref setValue: @{ @"size": @"foo" }]; // PERMISSION_DENIED: size is not a number [ref setValue: @{ @"size": @"foo", @"color": @"red" }]; // SUCCESS (assuming 'blue' appears in our colors list) [ref setValue: @{ @"size": @21, @"color": @"blue" }]; // If the record already exists and has a color, this will // succeed, otherwise it will fail since newData.hasChildren(['color', 'size']) // will fail to validate [[ref child:@"size"] setValue: @99];
Swift
var ref = FIRDatabase.database().reference().child("widget") // PERMISSION_DENIED: does not have children color and size ref.setValue("foo") // PERMISSION DENIED: does not have child color ref.setValue(["size": "foo"]) // PERMISSION_DENIED: size is not a number ref.setValue(["size": "foo", "color": "red"]) // SUCCESS (assuming 'blue' appears in our colors list) ref.setValue(["size": 21, "color": "blue"]) // If the record already exists and has a color, this will // succeed, otherwise it will fail since newData.hasChildren(['color', 'size']) // will fail to validate ref.child("size").setValue(99);
Java
FirebaseDatabase database = FirebaseDatabase.getInstance(); DatabaseReference ref = database.getReference("widget"); // PERMISSION_DENIED: does not have children color and size ref.setValue("foo"); // PERMISSION DENIED: does not have child color ref.child("size").setValue(22); // PERMISSION_DENIED: size is not a number Map<String,Object> map = new HashMap<String, Object>(); map.put("size","foo"); map.put("color","red"); ref.setValue(map); // SUCCESS (assuming 'blue' appears in our colors list) map = new HashMap<String, Object>(); map.put("size", 21); map.put("color","blue"); ref.setValue(map); // If the record already exists and has a color, this will // succeed, otherwise it will fail since newData.hasChildren(['color', 'size']) // will fail to validate ref.child("size").setValue(99);
REST
# PERMISSION_DENIED: does not have children color and size curl -X PUT -d 'foo' \ https://docs-examples.firebaseio.com/rest/securing-data/example.json # PERMISSION DENIED: does not have child color curl -X PUT -d '{"size": 22}' \ https://docs-examples.firebaseio.com/rest/securing-data/example.json # PERMISSION_DENIED: size is not a number curl -X PUT -d '{"size": "foo", "color": "red"}' \ https://docs-examples.firebaseio.com/rest/securing-data/example.json # SUCCESS (assuming 'blue' appears in our colors list) curl -X PUT -d '{"size": 21, "color": "blue"}' \ https://docs-examples.firebaseio.com/rest/securing-data/example.json # If the record already exists and has a color, this will # succeed, otherwise it will fail since newData.hasChildren(['color', 'size']) # will fail to validate curl -X PUT -d '99' \ https://docs-examples.firebaseio.com/rest/securing-data/example/size.json
Agora vamos analisar a mesma estrutura, mas usando regras .write
em vez de .validate
:
{ "rules": { // this variant will NOT allow deleting records (since .write would be disallowed) "widget": { // a widget must have 'color' and 'size' in order to be written to this path ".write": "newData.hasChildren(['color', 'size'])", "size": { // the value of "size" must be a number between 0 and 99, ONLY IF WE WRITE DIRECTLY TO SIZE ".write": "newData.isNumber() && newData.val() >= 0 && newData.val() <= 99" }, "color": { // the value of "color" must exist as a key in our mythical valid_colors/ index // BUT ONLY IF WE WRITE DIRECTLY TO COLOR ".write": "root.child('valid_colors/'+newData.val()).exists()" } } } }
Nesta variante, todas as operações a seguir terão êxito:
JavaScript
var ref = new Firebase(URL + "/widget"); // ALLOWED? Even though size is invalid, widget has children color and size, // so write is allowed and the .write rule under color is ignored ref.set({size: 99999, color: 'red'}); // ALLOWED? Works even if widget does not exist, allowing us to create a widget // which is invalid and does not have a valid color. // (allowed by the write rule under "color") ref.child('size').set(99);
Objective-C
Firebase *ref = [[Firebase alloc] initWithUrl:URL]; // ALLOWED? Even though size is invalid, widget has children color and size, // so write is allowed and the .write rule under color is ignored [ref setValue: @{ @"size": @9999, @"color": @"red" }]; // ALLOWED? Works even if widget does not exist, allowing us to create a widget // which is invalid and does not have a valid color. // (allowed by the write rule under "color") [[ref childByAppendingPath:@"size"] setValue: @99];
Swift
var ref = Firebase(url:URL) // ALLOWED? Even though size is invalid, widget has children color and size, // so write is allowed and the .write rule under color is ignored ref.setValue(["size": 9999, "color": "red"]) // ALLOWED? Works even if widget does not exist, allowing us to create a widget // which is invalid and does not have a valid color. // (allowed by the write rule under "color") ref.childByAppendingPath("size").setValue(99)
Java
Firebase ref = new Firebase(URL + "/widget"); // ALLOWED? Even though size is invalid, widget has children color and size, // so write is allowed and the .write rule under color is ignored Map<String,Object> map = new HashMap<String, Object>(); map.put("size", 99999); map.put("color", "red"); ref.setValue(map); // ALLOWED? Works even if widget does not exist, allowing us to create a widget // which is invalid and does not have a valid color. // (allowed by the write rule under "color") ref.child("size").setValue(99);
REST
# ALLOWED? Even though size is invalid, widget has children color and size, # so write is allowed and the .write rule under color is ignored curl -X PUT -d '{size: 99999, color: "red"}' \ https://docs-examples.firebaseio.com/rest/securing-data/example.json # ALLOWED? Works even if widget does not exist, allowing us to create a widget # which is invalid and does not have a valid color. # (allowed by the write rule under "color") curl -X PUT -d '99' \ https://docs-examples.firebaseio.com/rest/securing-data/example/size.json
Isso representa as diferenças entre as regras .write
e .validate
.
Como demonstrado, todas essas regras devem ser gravadas usando .validate
, com a possível exceção da regra newData.hasChildren()
, que dependerá da possibilidade de exclusões.
Regras baseadas em consultas
Embora não seja possível usar regras como filtros, é possível limitar o acesso a subconjuntos de dados usando parâmetros de consulta nas regras.
Use expressões query.
nas suas regras para conceder acesso de leitura ou gravação com base nos parâmetros de consulta.
Por exemplo, a regra baseada em consulta a seguir usa regras de segurança baseadas em usuário e regras baseadas em consulta para restringir o acesso aos dados na coleção baskets
apenas aos cestos de compras do usuário ativo:
"baskets": {
".read": "auth.uid !== null &&
query.orderByChild === 'owner' &&
query.equalTo === auth.uid" // restrict basket access to owner of basket
}
A seguinte consulta, que inclui os parâmetros de consulta na regra, seria bem-sucedida:
db.ref("baskets").orderByChild("owner")
.equalTo(auth.currentUser.uid)
.on("value", cb) // Would succeed
No entanto, as consultas que não incluem os parâmetros na regra falhariam com um erro PermissionDenied
:
db.ref("baskets").on("value", cb) // Would fail with PermissionDenied
Você também pode usar regras baseadas em consulta para limitar a quantidade de dados que um cliente baixa por meio das operações de leitura.
Por exemplo, a seguinte regra limita o acesso de leitura somente aos primeiros 1.000 resultados de uma consulta, ordenados por prioridade:
messages: {
".read": "query.orderByKey &&
query.limitToFirst <= 1000"
}
// Example queries:
db.ref("messages").on("value", cb) // Would fail with PermissionDenied
db.ref("messages").limitToFirst(1000)
.on("value", cb) // Would succeed (default order by key)
As expressões query.
a seguir estão disponíveis nas regras de segurança do Realtime Database.
Expressões de regra baseada em consulta | ||
---|---|---|
Expressão | Tipo | Descrição |
query.orderByKey query.orderByPriority query.orderByValue |
boolean | Verdadeiro para consultas ordenadas por chave, prioridade ou valor. Falso, em outros casos. |
query.orderByChild | string null |
Usa uma string para representar o caminho relativo para um nó filho. Por exemplo:
query.orderByChild === "address/zip" Se a consulta não
for ordenada por um nó filho, esse valor é nulo.
|
query.startAt query.endAt query.equalTo |
string number boolean null |
Recupera os limites da consulta em execução ou, se não houver limite definido, retorna nulo. |
query.limitToFirst query.limitToLast |
number null |
Recupera o limite na consulta em execução ou, se não houver limite definido, retorna nulo. |
Próximas etapas
Após essa discussão sobre as condições, você entende melhor Rules e tem todas as informações necessárias para:
Saber lidar com os principais casos de uso e o fluxo de trabalho para desenvolver, testar e implantar Rules:
- Saber mais sobre o conjunto completo de variáveis predefinidas de Rules que você pode usar para criar condições.
- Escrever regras que abordam cenários comuns.
- Desenvolver seu conhecimento analisando as situações em que você precisa identificar e evitar regras não seguras.
- Saber mais sobre o Pacote de emuladores locais do Firebase e como usá-lo para testar Rules.
- Revisar os métodos disponíveis para implantar Rules.
Conhecer os recursos de Rules específicos para Realtime Database:
- Saber como indexar o Realtime Database.
- Consultar a API REST para implantar Rules.