Aprenda a sintaxe principal da linguagem das regras de segurança do Realtime Database.

Com as regras de segurança do Firebase Realtime Database, é possível controlar o acesso aos dados armazenados no seu banco de dados. A sintaxe de regras flexíveis permite criar regras que correspondam a qualquer tipo de operação, desde todas as gravações no banco de dados até operações em nós individuais.

As regras de segurança do Realtime Database são uma configuração declarativa do banco de dados. Isso significa que as regras são definidas separadamente da lógica do produto. Isso oferece diversas vantagens: os clientes não são responsáveis por aplicar a segurança, implementações inadequadas não comprometerão seus dados e, talvez o mais importante de tudo, não é preciso ter um referencial intermediário, como um servidor, para proteger os dados do mundo.

Neste tópico, descreveremos a sintaxe básica e a estrutura das regras de segurança do Realtime Database usadas para criar conjuntos de regras completos.

Estruturação de regras de segurança

As regras de segurança do Realtime Database são compostas por expressões semelhantes ao JavaScript contidas em um documento JSON. A estrutura das regras precisa seguir a estrutura de dados armazenada no banco de dados.

As regras básicas identificam um conjunto de nós a serem protegidos, os métodos de acesso envolvidos (por exemplo, leitura, gravação) e as condições sob as quais o acesso é permitido ou negado. Nos exemplos a seguir, nossas condições serão instruções simples de true e false, mas no próximo tópico falaremos sobre maneiras mais dinâmicas de expressar condições.

Por exemplo, se tentarmos proteger um child_node em um parent_node, a sintaxe geral a seguir será:

{
  "rules": {
    "parent_node": {
      "child_node": {
        ".read": <condition>,
        ".write": <condition>,
        ".validate": <condition>,
      }
    }
  }
}

Vamos aplicar esse padrão. Por exemplo, suponha que você esteja acompanhando uma lista de mensagens e tenha dados parecidos com os seguintes:

{
  "messages": {
    "message0": {
      "content": "Hello",
      "timestamp": 1405704370369
    },
    "message1": {
      "content": "Goodbye",
      "timestamp": 1405704395231
    },
    ...
  }
}

As regras devem ser estruturadas de maneira semelhante. Veja um conjunto de regras para segurança somente leitura que pode fazer sentido para essa estrutura de dados. Neste exemplo, ilustramos como especificamos nós de banco de dados a que regras se aplicam e as condições de avaliação de regras nesses nós.

{
  "rules": {
    // For requests to access the 'messages' node...
    "messages": {
      // ...and the individual wildcarded 'message' nodes beneath
      // (we'll cover wildcarding variables more a bit later)....
      "$message": {

        // For each message, allow a read operation if <condition>. In this
        // case, we specify our condition as "true", so read access is always granted.
        ".read": "true",

        // For read-only behavior, we specify that for write operations, our
        // condition is false.
        ".write": "false"
      }
    }
  }
}

Operações básicas de regras

Há três tipos de regras para aplicar a segurança com base no tipo de operação sendo executada nos dados: .write, .read e .validate. Veja um breve resumo da finalidade delas:

Tipos de regra
.read Descreve se e quando os dados podem ser lidos pelos usuários.
.write Descreve se e quando os dados podem ser gravados.
.validate Define a formatação correta do valor, o tipo de dados e se o valor tem atributos filhos.

Variáveis de captura de caractere curinga

Todas as instruções de regras apontam para nós. Uma instrução pode apontar para um nó específico ou usar variáveis de captura $ de caractere curinga para apontar para conjuntos de nós em um nível da hierarquia. Use essas variáveis de captura para armazenar o valor das chaves de nó para uso dentro das instruções de regras subsequentes. Essa técnica permite que você grave condições de regras mais complexas, que abordaremos mais detalhadamente no próximo tópico.

{
  "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 }
    }
  }
}

Regras de leitura e gravação são aplicadas em cascata

As regras .read e .write funcionam de cima para baixo, com regras em níveis menos profundos que substituem regras de níveis mais profundos. Se uma regra conceder permissões de leitura ou gravação em um caminho específico, ela também concederá acesso a todos os nós filhos nesse caminho. Considere a seguinte estrutura:

{
  "rules": {
     "foo": {
        // allows read to /foo/*
        ".read": "data.child('baz').val() === true",
        "bar": {
          /* ignored, since read was allowed already */
          ".read": false
        }
     }
  }
}

Essa estrutura de segurança permite que /bar/ seja lido sempre que /foo/ contiver um baz filho com o valor true. A regra ".read": false em /foo/bar/ não tem efeito aqui porque o acesso não pode ser revogado por um caminho filho.

Mesmo que isso não pareça intuitivo de imediato, essa é uma parte eficiente da linguagem das regras e permite implementar privilégios de acesso muito complexos com esforço mínimo. Isso será demonstrado quando abordarmos a segurança baseada no usuário posteriormente neste guia.

Vale ressaltar que regras .validate não são aplicadas em cascata. Todas as regras de validação devem ser atendidas em todos os níveis da hierarquia para que uma gravação seja permitida.

Regras não são filtros

As regras são aplicadas de maneira atômica. Isso significa que uma operação de leitura ou gravação falhará imediatamente se não houver uma regra nesse local ou em um local pai que conceda acesso. Mesmo que cada caminho filho afetado seja acessível, a leitura no local pai falhará totalmente. Suponha esta estrutura:

{
  "rules": {
    "records": {
      "rec1": {
        ".read": true
      },
      "rec2": {
        ".read": false
      }
    }
  }
}

Sem entender que as regras são avaliadas atomicamente, pode parecer que buscar o caminho /records/ retornaria rec1, mas não rec2. O resultado real, no entanto, é um erro:

JavaScript
var db = firebase.database();
db.ref("records").once("value", function(snap) {
  // success method is not called
}, function(err) {
  // error callback triggered with PERMISSION_DENIED
});
Objective-C
Observação: este produto do Firebase não está disponível no destino Clipes de apps.
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
[[_ref child:@"records"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
  // success block is not called
} withCancelBlock:^(NSError * _Nonnull error) {
  // cancel block triggered with PERMISSION_DENIED
}];
Swift
Observação: este produto do Firebase não está disponível no destino Clipes de apps.
var ref = FIRDatabase.database().reference()
ref.child("records").observeSingleEventOfType(.Value, withBlock: { snapshot in
    // success block is not called
}, withCancelBlock: { error in
    // cancel block triggered with PERMISSION_DENIED
})
Java
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("records");
ref.addListenerForSingleValueEvent(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot snapshot) {
    // success method is not called
  }

  @Override
  public void onCancelled(FirebaseError firebaseError) {
    // error callback triggered with PERMISSION_DENIED
  });
});
REST
curl https://docs-examples.firebaseio.com/rest/records/
# response returns a PERMISSION_DENIED error

Como a operação de leitura em /records/ é atômica, e não há regra de leitura que conceda acesso a todos os dados em /records/, isso gerará um erro PERMISSION_DENIED. Se avaliarmos essa regra no simulador de segurança do Console do Firebase, podemos ver que a operação de leitura foi negada porque nenhuma regra de leitura permitia acesso ao /records/ caminho. No entanto, a regra para rec1 nunca foi avaliada porque não estava no caminho solicitado. Para buscar rec1, precisamos acessá-lo diretamente:

JavaScript
var db = firebase.database();
db.ref("records/rec1").once("value", function(snap) {
  // SUCCESS!
}, function(err) {
  // error callback is not called
});
Objective-C
Observação: este produto do Firebase não está disponível no destino Clipes de apps.
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
[[ref child:@"records/rec1"] observeSingleEventOfType:FEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
    // SUCCESS!
}];
Swift
Observação: este produto do Firebase não está disponível no destino Clipes de apps.
var ref = FIRDatabase.database().reference()
ref.child("records/rec1").observeSingleEventOfType(.Value, withBlock: { snapshot in
    // SUCCESS!
})
Java
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("records/rec1");
ref.addListenerForSingleValueEvent(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot snapshot) {
    // SUCCESS!
  }

  @Override
  public void onCancelled(FirebaseError firebaseError) {
    // error callback is not called
  }
});
REST
curl https://docs-examples.firebaseio.com/rest/records/rec1
# SUCCESS!

Declarações sobrepostas

É possível aplicar mais de uma regra a um nó. No caso de várias expressões de regras identificarem um nó, o método de acesso será negado se qualquer uma das condições for false:

{
  "rules": {
    "messages": {
      // A rule expression that applies to all nodes in the 'messages' node
      "$message": {
        ".read": "true",
        ".write": "true"
      },
      // A second rule expression applying specifically to the 'message1` node
      "message1": {
        ".read": "false",
        ".write": "false"
      }
    }
  }
}

No exemplo acima, as leituras ao nó message1 serão negadas porque a segunda regra é sempre false, mesmo que a primeira regra seja sempre true.

Próximas etapas

Você pode se aprofundar nas regras de segurança do Firebase Realtime Database:

  • Aprender o próximo conceito principal da linguagem de regras, com condições dinâmicas, que permitem que as regras verifiquem a autorização do usuário, comparem dados existentes e recebidos, validem dados recebidos e verifiquem a estrutura de consultas provenientes do cliente e muito mais.

  • Analise os casos de uso de segurança típicos e as definições de regras de segurança do Firebase que abrangem esses casos.