Apprenez la syntaxe de base du langage des règles de sécurité des bases de données en temps réel

Les règles de sécurité de la base de données en temps réel Firebase vous permettent de contrôler l'accès aux données stockées dans votre base de données. La syntaxe flexible des règles vous permet de créer des règles qui correspondent à tout, de toutes les écritures dans votre base de données aux opérations sur des nœuds individuels.

Les règles de sécurité de base de données en temps réel sont une configuration déclarative de votre base de données. Cela signifie que les règles sont définies séparément de la logique du produit. Cela présente un certain nombre d'avantages : les clients ne sont pas responsables du respect de la sécurité, les implémentations boguées ne compromettront pas vos données et, peut-être plus important encore, il n'est pas nécessaire de recourir à un arbitre intermédiaire, tel qu'un serveur, pour protéger les données du monde entier.

Cette rubrique décrit la syntaxe et la structure de base des règles de sécurité de base de données en temps réel utilisées pour créer des ensembles de règles complets.

Structurer vos règles de sécurité

Les règles de sécurité des bases de données en temps réel sont constituées d'expressions de type JavaScript contenues dans un document JSON. La structure de vos règles doit suivre la structure des données que vous avez stockées dans votre base de données.

Les règles de base identifient un ensemble de nœuds à sécuriser, les méthodes d'accès (par exemple, lecture, écriture) impliquées et les conditions dans lesquelles l'accès est autorisé ou refusé. Dans les exemples suivants, nos conditions seront de simples déclarations true et false , mais dans la rubrique suivante, nous aborderons des manières plus dynamiques d'exprimer les conditions.

Ainsi, par exemple, si nous essayons de sécuriser un child_node sous un parent_node , la syntaxe générale à suivre est :

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

Appliquons ce modèle. Par exemple, disons que vous suivez une liste de messages et que vous disposez de données qui ressemblent à ceci :

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

Vos règles doivent être structurées de la même manière. Voici un ensemble de règles de sécurité en lecture seule qui pourraient avoir du sens pour cette structure de données. Cet exemple illustre comment nous spécifions les nœuds de base de données auxquels les règles s'appliquent et les conditions d'évaluation des règles sur ces nœuds.

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

Opérations sur les règles de base

Il existe trois types de règles pour appliquer la sécurité en fonction du type d'opération effectuée sur les données : .write , .read et .validate . Voici un bref résumé de leurs objectifs :

Types de règles
.lire Décrit si et quand les données peuvent être lues par les utilisateurs.
.écrire Décrit si et quand l’écriture des données est autorisée.
.valider Définit à quoi ressemblera une valeur correctement formatée, si elle possède des attributs enfants et le type de données.

Variables de capture génériques

Toutes les instructions de règles pointent vers des nœuds. Une instruction peut pointer vers un nœud spécifique ou utiliser des variables de capture génériques $ pour pointer vers des ensembles de nœuds à un niveau de la hiérarchie. Utilisez ces variables de capture pour stocker la valeur des clés de nœud à utiliser dans les instructions de règles ultérieures. Cette technique vous permet d'écrire des conditions de règles plus complexes, ce que nous aborderons plus en détail dans la rubrique suivante.

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

Les variables $ dynamiques peuvent également être utilisées en parallèle avec des noms de chemin constants. Dans cet exemple, nous utilisons la variable $other pour déclarer une règle .validate qui garantit que widget n'a pas d'enfants autres que title et color . Toute écriture qui entraînerait la création d’enfants supplémentaires échouerait.

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

Cascade de règles de lecture et d'écriture

Les règles .read et .write fonctionnent de haut en bas, les règles moins profondes remplaçant les règles plus profondes. Si une règle accorde des autorisations de lecture ou d'écriture sur un chemin particulier, elle accorde également l'accès à tous les nœuds enfants qui s'y trouvent. Considérons la structure suivante :

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

Cette structure de sécurité permet de lire /bar/ chaque fois que /foo/ contient un enfant baz avec la valeur true . La règle ".read": false sous /foo/bar/ n'a aucun effet ici, puisque l'accès ne peut pas être révoqué par un chemin enfant.

Même si cela ne semble pas immédiatement intuitif, il s'agit d'un élément puissant du langage de règles qui permet de mettre en œuvre des privilèges d'accès très complexes avec un minimum d'effort. Cela sera illustré lorsque nous aborderons la sécurité basée sur l'utilisateur plus loin dans ce guide.

Notez que les règles .validate ne sont pas appliquées en cascade. Toutes les règles de validation doivent être satisfaites à tous les niveaux de la hiérarchie pour qu'une écriture soit autorisée.

Les règles ne sont pas des filtres

Les règles sont appliquées de manière atomique. Cela signifie qu'une opération de lecture ou d'écriture échoue immédiatement s'il n'existe pas de règle à cet emplacement ou à un emplacement parent qui accorde l'accès. Même si tous les chemins enfants concernés sont accessibles, la lecture à l'emplacement parent échouera complètement. Considérez cette structure :

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

Sans comprendre que les règles sont évaluées de manière atomique, il peut sembler que la récupération du chemin /records/ renverrait rec1 mais pas rec2 . Le résultat réel est cependant une erreur :

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
});
Objectif c
Remarque : Ce produit Firebase n'est pas disponible sur la cible App Clip.
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
}];
Rapide
Remarque : Ce produit Firebase n'est pas disponible sur la cible App Clip.
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
  });
});
REPOS
curl https://docs-examples.firebaseio.com/rest/records/
# response returns a PERMISSION_DENIED error

Étant donné que l'opération de lecture sur /records/ est atomique et qu'il n'y a pas de règle de lecture qui accorde l'accès à toutes les données sous /records/ , cela générera une erreur PERMISSION_DENIED . Si nous évaluons cette règle dans le simulateur de sécurité de notre console Firebase , nous pouvons voir que l'opération de lecture a été refusée car aucune règle de lecture n'autorisait l'accès au chemin /records/ . Cependant, notez que la règle pour rec1 n'a jamais été évaluée car elle ne se trouvait pas dans le chemin que nous avions demandé. Pour récupérer rec1 , nous aurions besoin d'y accéder directement :

Javascript
var db = firebase.database();
db.ref("records/rec1").once("value", function(snap) {
  // SUCCESS!
}, function(err) {
  // error callback is not called
});
Objectif c
Remarque : Ce produit Firebase n'est pas disponible sur la cible App Clip.
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
[[ref child:@"records/rec1"] observeSingleEventOfType:FEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
    // SUCCESS!
}];
Rapide
Remarque : Ce produit Firebase n'est pas disponible sur la cible App Clip.
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
  }
});
REPOS
curl https://docs-examples.firebaseio.com/rest/records/rec1
# SUCCESS!

Déclarations qui se chevauchent

Il est possible que plusieurs règles s'appliquent à un nœud. Dans le cas où plusieurs expressions de règles identifient un nœud, la méthode d'accès est refusée si l'une des conditions est 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"
      }
    }
  }
}

Dans l'exemple ci-dessus, les lectures sur le nœud message1 seront refusées car la deuxième règle est toujours false , même si la première règle est toujours true .

Prochaines étapes

Vous pouvez approfondir votre compréhension des règles de sécurité des bases de données en temps réel de Firebase :

  • Apprenez le prochain concept majeur du langage de règles, les conditions dynamiques, qui permettent à vos règles de vérifier l'autorisation des utilisateurs, de comparer les données existantes et entrantes, de valider les données entrantes, de vérifier la structure des requêtes provenant du client, et bien plus encore.

  • Passez en revue les cas d'utilisation typiques de la sécurité et les définitions des règles de sécurité Firebase qui les traitent .