Aprenda la sintaxis principal del lenguaje de reglas de seguridad de bases de datos en tiempo real

Las reglas de seguridad de la base de datos en tiempo real de Firebase le permiten controlar el acceso a los datos almacenados en su base de datos. La sintaxis de reglas flexible le permite crear reglas que coincidan con cualquier cosa, desde todas las escrituras en su base de datos hasta operaciones en nodos individuales.

Las reglas de seguridad de bases de datos en tiempo real son configuraciones declarativas para su base de datos. Esto significa que las reglas se definen por separado de la lógica del producto. Esto tiene una serie de ventajas: los clientes no son responsables de hacer cumplir la seguridad, las implementaciones con errores no comprometerán sus datos y, quizás lo más importante, no hay necesidad de un árbitro intermedio, como un servidor, para proteger los datos del mundo.

Este tema describe la sintaxis y estructura básicas de las reglas de seguridad de bases de datos en tiempo real utilizadas para crear conjuntos de reglas completos.

Estructurar sus reglas de seguridad

Las reglas de seguridad de bases de datos en tiempo real se componen de expresiones similares a JavaScript contenidas en un documento JSON. La estructura de sus reglas debe seguir la estructura de los datos que ha almacenado en su base de datos.

Las reglas básicas identifican un conjunto de nodos que se deben proteger, los métodos de acceso (por ejemplo, lectura, escritura) involucrados y las condiciones bajo las cuales se permite o deniega el acceso. En los siguientes ejemplos, nuestras condiciones serán declaraciones simples true y false , pero en el siguiente tema cubriremos formas más dinámicas de expresar condiciones.

Entonces, por ejemplo, si estamos intentando proteger un child_node bajo un parent_node , la sintaxis general a seguir es:

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

Apliquemos este patrón. Por ejemplo, digamos que está realizando un seguimiento de una lista de mensajes y tiene datos similares a estos:

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

Sus reglas deben estructurarse de manera similar. Aquí hay un conjunto de reglas para la seguridad de solo lectura que podrían tener sentido para esta estructura de datos. Este ejemplo ilustra cómo especificamos los nodos de la base de datos a qué reglas se aplican y las condiciones para evaluar las reglas 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

Hay tres tipos de reglas para hacer cumplir la seguridad según el tipo de operación que se realiza con los datos: .write , .read y .validate . Aquí hay un breve resumen de sus propósitos:

Tipos de reglas
.leer Describe si los usuarios pueden leer los datos y cuándo.
.escribir Describe si se permite escribir datos y cuándo.
.validar Define cómo se verá un valor formateado correctamente, si tiene atributos secundarios y el tipo de datos.

Variables de captura comodín

Todas las declaraciones de reglas apuntan a nodos. Una declaración puede apuntar a un nodo específico o utilizar variables de captura con comodín $ para apuntar a conjuntos de nodos en un nivel de la jerarquía. Utilice estas variables de captura para almacenar el valor de las claves de nodo para usarlas dentro de declaraciones de reglas posteriores. Esta técnica le permite escribir condiciones de reglas más complejas, algo que cubriremos 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 utilizar en paralelo con nombres de rutas constantes. En este ejemplo, usamos la variable $other para declarar una regla .validate que garantiza que widget no tenga hijos distintos del title y color . Cualquier escritura que diera lugar a la creación de hijos 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 }
    }
  }
}

Cascada de reglas de lectura y escritura

Las reglas .read y .write funcionan de arriba hacia abajo, y las reglas menos profundas anulan las reglas más profundas. Si una regla otorga permisos de lectura o escritura en una ruta particular, también otorga acceso a todos los nodos secundarios bajo ella. Considere 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/ siempre que /foo/ contenga un baz secundario con valor true . La regla ".read": false en /foo/bar/ no tiene ningún efecto aquí, ya que el acceso no puede ser revocado por una ruta secundaria.

Si bien puede no parecer inmediatamente intuitivo, esta es una parte poderosa del lenguaje de reglas y permite implementar privilegios de acceso muy complejos con un mínimo esfuerzo. Esto se ilustrará cuando analicemos la seguridad basada en el usuario más adelante en esta guía.

Tenga en cuenta que las reglas .validate no se ejecutan en cascada. Todas las reglas de validación deben cumplirse en todos los niveles de la jerarquía para que se permita una escritura.

Las reglas no son filtros

Las reglas se aplican de manera atómica. Eso significa que una operación de lectura o escritura falla inmediatamente si no hay una regla en esa ubicación o en una ubicación principal que otorgue acceso. Incluso si se puede acceder a todas las rutas secundarias afectadas, la lectura en la ubicación principal fallará por completo. Considere esta estructura:

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

Sin entender que las reglas se evalúan atómicamente, podría parecer que recuperar la ruta /records/ devolvería rec1 pero no rec2 . El resultado real, sin embargo, 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
});
C objetivo
Nota: Este producto de Firebase no está disponible en el destino del clip de aplicación.
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
}];
Rápido
Nota: Este producto de Firebase no está disponible en el destino del clip de aplicación.
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
  });
});
DESCANSAR
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 existe una regla de lectura que otorgue acceso a todos los datos en /records/ , esto generará un error PERMISSION_DENIED . Si evaluamos esta regla en el simulador de seguridad de nuestra Firebase console , podemos ver que la operación de lectura fue denegada porque ninguna regla de lectura permitía el acceso a la ruta /records/ . Sin embargo, tenga en cuenta que la regla para rec1 nunca se evaluó porque no estaba en la ruta que solicitamos. Para recuperar rec1 , necesitaríamos 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
});
C objetivo
Nota: Este producto de Firebase no está disponible en el destino del clip de aplicación.
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
[[ref child:@"records/rec1"] observeSingleEventOfType:FEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
    // SUCCESS!
}];
Rápido
Nota: Este producto de Firebase no está disponible en el destino del clip de aplicación.
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
  }
});
DESCANSAR
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. En el caso de que varias expresiones de reglas identifiquen un nodo, el método de acceso se deniega si alguna 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 lecturas del nodo message1 porque la segunda regla siempre es false , aunque la primera regla siempre es true .

Próximos pasos

Puede profundizar su comprensión de las reglas de seguridad de bases de datos en tiempo real de Firebase:

  • Conozca el siguiente concepto importante del lenguaje de Reglas, las condiciones dinámicas, que permiten que sus Reglas verifiquen la autorización del usuario, comparen datos existentes y entrantes, validen datos entrantes, verifiquen la estructura de las consultas provenientes del cliente y más.

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