Conoce la sintaxis básica del lenguaje de reglas de seguridad de Realtime Database

Las reglas de seguridad de Firebase Realtime Database te permiten controlar el acceso a los datos almacenados en tu base de datos. La sintaxis de reglas flexibles te permite crear reglas que coincidan con todo, desde todas las operaciones de escritura en tu base de datos hasta las operaciones en nodos individuales.

Las reglas de seguridad de Realtime Database representan una configuración declarativa para tu base de datos. Esto significa que las reglas se definen de manera independiente de la lógica del producto. Esto tiene varias ventajas: los clientes no son responsables de aplicar las condiciones de seguridad, las implementaciones erróneas no afectan tus datos y, lo que tal vez es más importante, no se necesita un intermediario, como un servidor, para proteger los datos contra entidades externas.

En este tema se describen la sintaxis básica y la estructura de las reglas de seguridad de Realtime Database que se usan para crear conjuntos de reglas completos.

Estructura tus reglas de seguridad

Las reglas de seguridad de Realtime Database se componen de expresiones similares a las de JavaScript almacenadas en un documento JSON. La estructura de tus reglas debe seguir la de los datos que almacenaste en tu base de datos.

Las reglas básicas identifican un conjunto de nodos que se protegerán, los métodos de acceso involucrados (p ej., lectura o escritura) y las condiciones que determinan si se permite el acceso. En los siguientes ejemplos nuestras condiciones serán instrucciones simples true y false, pero en el siguiente tema se analizarán formas más dinámicas para expresar condiciones.

Por lo tanto, si quieres proteger un child_node bajo un parent_node, sigue esta sintaxis general:

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

Apliquemos este patrón. Por ejemplo, supongamos que haces un seguimiento de una lista de mensajes y dispones de datos que tienen este aspecto:

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

Tus reglas deben estructurarse de manera similar. A continuación se muestra un conjunto de reglas de seguridad de solo lectura que puede tener sentido para esta estructura de datos. En este ejemplo se ilustra cómo se especifican los nodos de la base de datos a los que se aplican las reglas y las condiciones para evaluarlas en esos nodos.

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

Operaciones de reglas básicas

Existen tres tipos de reglas para aplicar la seguridad en función del tipo de operación que se realiza en los datos: .write, .read y .validate. Este es un resumen breve de sus propósitos:

Tipos de reglas
.read Describe si los usuarios pueden leer los datos y cuándo pueden hacerlo.
.write Indica si se permite la escritura de datos y en qué momento.
.validate Define el aspecto de un valor con formato correcto, si este tiene atributos secundarios y el tipo de datos.

Variables comodines de captura

Todas las declaraciones de las reglas hacen referencia a los nodos. Una declaración puede hacer referencia a un nodo específico, o bien incluir variables comodines de captura $ para referirse a conjuntos de nodos a nivel de la jerarquía. Usa estas variables de captura a fin de almacenar el valor de las claves del nodo para usarlas en las declaraciones de reglas posteriores. Esta técnica te permite escribir condiciones de Rules más complejas, que se analizarán con más detalle en el siguiente tema.

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

Las variables dinámicas $ también se pueden usar en paralelo con nombres de rutas de acceso constantes. En este ejemplo, usamos la variable $other para declarar una regla .validate que garantiza que widget no tenga más elementos secundarios aparte de title y color. Cualquier escritura que cree elementos secundarios adicionales fallaría.

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

Transmisión en cascada de reglas de lectura y escritura

Las reglas .read y .write funcionan de arriba hacia abajo, y las reglas más superficiales anulan las más profundas. Si una regla otorga permisos de lectura o de escritura a una ruta de acceso en particular, también otorga acceso a todos los nodos secundarios debajo de ella. Considera la siguiente estructura:

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

Esta estructura de seguridad permite leer /bar/ cada vez que /foo/ contiene un elemento secundario baz con el valor true. La regla ".read": false en /foo/bar/ no tiene efecto aquí, ya que una ruta secundaria no puede revocar el acceso.

Aunque no parezca intuitivo inmediatamente, esta es una parte poderosa del lenguaje de reglas y permite que se implementen privilegios de acceso complejos con un esfuerzo mínimo. Mostraremos ejemplos de esto cuando abordemos la seguridad basada en usuarios más adelante en esta guía.

Ten en cuenta que las reglas .validate no se aplican en cascada. Para que se autorice una operación de escritura, deben cumplirse todas las reglas de validación en todos los niveles de la jerarquía.

Las reglas no son filtros

Las reglas se aplican de una manera atómica. Es decir, una operación de lectura o de escritura falla inmediatamente si no hay una regla en esa ubicación ni en una superior que otorgue acceso. Incluso cuando sea posible acceder a todas las rutas de acceso secundarias afectadas, la lectura en la ubicación primaria fallará por completo. Ten en cuenta esta estructura:

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

Sin comprender que las reglas se evalúan de forma atómica, puede parecer que recuperar la ruta de acceso /records/ mostraría rec1, pero no rec2. Sin embargo, el resultado real es un error:

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
Nota: Este producto de Firebase no está disponible en el destino de App Clips.
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
Nota: Este producto de Firebase no está disponible en el destino de App Clips.
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

Dado que la operación de lectura en /records/ es atómica y no hay una regla de lectura que otorgue acceso a todos los datos en /records/, esto mostrará un error PERMISSION_DENIED. Si evaluamos esta regla en un simulador de seguridad de Firebase console, se puede ver que se rechazó la operación de lectura porque ninguna regla de lectura permitió el acceso a la ruta de acceso /records/. Sin embargo, ten en cuenta que nunca se evaluó la regla de rec1 porque no se encontraba en la ruta de acceso solicitada. Para recuperar rec1, debes acceder a él directamente:

JavaScript
var db = firebase.database();
db.ref("records/rec1").once("value", function(snap) {
  // SUCCESS!
}, function(err) {
  // error callback is not called
});
Objective-C
Nota: Este producto de Firebase no está disponible en el destino de App Clips.
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
[[ref child:@"records/rec1"] observeSingleEventOfType:FEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
    // SUCCESS!
}];
Swift
Nota: Este producto de Firebase no está disponible en el destino de App Clips.
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!

Declaraciones superpuestas

Es posible que se aplique más de una regla a un nodo. Si varias expresiones de reglas identifican un nodo, se rechaza el método de acceso si cualquiera de las condiciones es 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"
      }
    }
  }
}

En el ejemplo anterior se denegarán las operaciones de lectura al nodo message1 porque la segunda regla siempre es false, incluso si la primera regla siempre es true.

Próximos pasos

Puedes profundizar tus conocimientos sobre las reglas de seguridad de Firebase Realtime Database:

  • Conoce el siguiente concepto principal del lenguaje de Rules, las condiciones dinámicas, que permiten que tus Rules verifiquen la autorización del usuario, comparen datos existentes y entrantes, validen los datos entrantes, comprueben la estructura de las consultas del cliente y mucho más.

  • Revisa los casos de uso de seguridad típicos y las definiciones de reglas de seguridad de Firebase que los abordan.