Découvrez la syntaxe de base du langage des règles de sécurité de Realtime Database.

Les règles de sécurité de Firebase Realtime Database vous permettent de contrôler l'accès aux données stockées dans votre base de données. La syntaxe des règles flexibles vous permet de créer des règles qui correspondent à toutes sortes d'éléments : toutes les écritures de votre base de données, ou bien les opérations sur des nœuds individuels.

Les règles de sécurité Realtime Database sont une configuration déclarative pour 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 de l'application de la sécurité, les implémentations buguées ne compromettent pas vos données et, peut-être le plus important, il n'est pas nécessaire d'avoir un arbitre intermédiaire, tel qu'un serveur, pour protéger les données du monde extérieur.

Cet article décrit la syntaxe et la structure de base des règles de sécurité de Realtime Database utilisées pour créer des ensembles de règles complets.

Structurer vos règles de sécurité

Les règles de sécurité de Realtime Database sont constituées d'expressions semblables à JavaScript contenues dans un document JSON. La structure de vos règles doit suivre celle 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 sous lesquelles l'accès est autorisé ou refusé. Dans les exemples suivants, nos conditions seront de simples instructions true et false, mais dans la section suivante, nous aborderons des manières plus dynamiques d'exprimer des conditions.

Ainsi, si vous essayez de sécuriser un child_node sous un parent_node, la syntaxe générale à suivre est la suivante:

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

Appliquez ce modèle. Par exemple, supposons que vous effectuiez le suivi d'une liste de messages et que vous disposiez de données semblables à ceci:

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

Vos règles doivent être structurées de manière similaire. Voici un ensemble de règles de sécurité en lecture seule qui peuvent être adaptées à cette structure de données. Cet exemple montre 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 au niveau de 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 de base des règles

Il existe trois types de règles permettant d'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
.read Décrit si et quand les utilisateurs sont autorisés à lire les données.
.write Indique si et quand les données peuvent être écrites.
.validate Définit l'apparence d'une valeur correctement formatée, si elle comporte des attributs enfants et le type de données.

Variables de capture avec caractères 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 avec des caractères 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 suivantes. Cette technique vous permet d'écrire des conditions Rules plus complexes, que nous aborderons plus en détail dans le prochain sujet.

{
  "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 constant. 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 des règles de lecture et d'écriture

Les règles .read et .write fonctionnent de haut en bas, les règles plus superficielles remplaçant les règles plus profondes. Si une règle accorde des autorisations de lecture ou d'écriture à un chemin d'accès particulier, elle accorde également l'accès à tous les nœuds enfants sous celui-ci. Prenons 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 baz enfant avec la valeur true. La règle ".read": false sous /foo/bar/ n'a aucun effet ici, car l'accès ne peut pas être révoqué par un chemin d'accès enfant.

Bien que cela puisse ne pas sembler immédiatement intuitif, il s'agit d'une partie puissante du langage de règles et permet d'implémenter des droits d'accès très complexes avec un effort minimal. Nous verrons cela plus tard dans ce guide, lorsque nous aborderons la sécurité basée sur l'utilisateur.

Notez que les règles .validate ne sont pas en cascade. Toutes les règles de validation doivent être respectées à 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'y a pas de règle à cet emplacement ou à un emplacement parent qui accorde l'accès. Même si tous les chemins d'accès enfant concernés sont accessibles, la lecture à l'emplacement parent échoue complètement. Prenons cette structure:

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

Si vous ne comprenez pas que les règles sont évaluées de manière atomique, il peut sembler que l'extraction du chemin d'accès /records/ renvoie rec1, mais pas rec2. Toutefois, le résultat réel est 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
});
Objective-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
}];
Swift
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
  });
});
REST
curl https://docs-examples.firebaseio.com/rest/records/
# response returns a PERMISSION_DENIED error

Étant donné que l'opération de lecture à /records/ est atomique et qu'aucune règle de lecture n'accorde l'accès à toutes les données sous /records/, une erreur PERMISSION_DENIED est générée. Si nous évaluons cette règle dans le simulateur de sécurité de la 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/. Notez toutefois que la règle pour rec1 n'a jamais été évaluée, car elle ne figurait pas dans le chemin d'accès que nous avons demandé. Pour extraire rec1, nous devons 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
});
Objective-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!
}];
Swift
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
  }
});
REST
curl https://docs-examples.firebaseio.com/rest/records/rec1
# SUCCESS!

Chevauchement d'instructions

Plusieurs règles peuvent s'appliquer à 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 au moins une des conditions est définie sur 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.

Étapes suivantes

Vous pouvez approfondir vos connaissances sur les règles de sécurité Firebase Realtime Database:

  • Découvrez le prochain concept majeur du langage Rules, les conditions dynamiques, qui permettent à votre Rules de vérifier l'autorisation de l'utilisateur, 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 plus encore.

  • Consultez les cas d'utilisation de sécurité courants et les définitions des règles de sécurité Firebase qui les couvrent.