Utiliser des conditions dans les règles de sécurité de Realtime Database

Ce guide s'appuie sur le guide Découvrir le langage de base des règles de sécurité Firebase pour montrer comment ajouter des conditions à vos règles de sécurité Firebase Realtime Database.

La condition représente le composant fondamental des règles de sécurité Realtime Database. Une condition est une expression booléenne qui détermine si une opération spécifique doit être autorisée ou refusée. Pour les règles de base, l'utilisation des littéraux true et false comme conditions fonctionne parfaitement. Toutefois, le langage des règles de sécurité Realtime Database vous permet d'écrire des conditions plus complexes qui peuvent :

  • Vérifier l'authentification des utilisateurs
  • Évaluer les données existantes par rapport aux données nouvellement envoyées
  • Accéder à différentes parties de votre base de données et les comparer
  • Valider les données entrantes
  • Utiliser la structure des requêtes entrantes pour la logique de sécurité

Utiliser des variables $ pour capturer des segments de chemin d'accès

Vous pouvez capturer des parties du chemin pour une lecture ou une écriture en déclarant des variables de capture avec le préfixe $. Il sert de caractère générique et stocke la valeur de cette clé pour l'utiliser dans les conditions des règles :

{
  "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 chemins 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 entraînant la création d'enfants supplémentaires échouera.

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

Authentification

L'un des modèles de règles de sécurité les plus courants consiste à contrôler l'accès en fonction de l'état d'authentification de l'utilisateur. Par exemple, votre application peut autoriser uniquement les utilisateurs connectés à écrire des données.

Si votre application utilise Firebase Authentication, la variable request.auth contient les informations d'authentification du client demandant des données. Pour plus d'informations sur request.auth, consultez la documentation de référence.

Firebase Authentication s'intègre à Firebase Realtime Database pour vous permettre de contrôler l'accès aux données par utilisateur à l'aide de conditions. Une fois qu'un utilisateur s'est authentifié, la variable auth dans vos règles de sécurité Realtime Database est renseignée avec les informations de l'utilisateur. Ces informations incluent leur identifiant unique (uid), ainsi que les données de compte associées, telles qu'un identifiant Facebook ou une adresse e-mail, et d'autres informations. Si vous implémentez un fournisseur d'authentification personnalisé, vous pouvez ajouter vos propres champs à la charge utile d'authentification de vos utilisateurs.

Cette section explique comment combiner le langage des règles de sécurité Firebase Realtime Database avec les informations d'authentification de vos utilisateurs. En combinant ces deux concepts, vous pouvez contrôler l'accès aux données en fonction de l'identité de l'utilisateur.

Variable auth

La variable prédéfinie auth dans les règles est nulle avant l'authentification.

Une fois qu'un utilisateur est authentifié avec Firebase Authentication, il contient les attributs suivants :

fournisseur Méthode d'authentification utilisée ("password", "anonymous", "facebook", "github", "google" ou "twitter").
uid ID utilisateur unique, garanti comme tel pour tous les fournisseurs.
jeton Contenu du jeton d'identification Firebase Auth. Pour en savoir plus, consultez la documentation de référence sur auth.token.

Voici un exemple de règle qui utilise la variable auth pour s'assurer que chaque utilisateur ne peut écrire que dans un chemin d'accès spécifique à l'utilisateur :

{
  "rules": {
    "users": {
      "$user_id": {
        // grants write access to the owner of this user account
        // whose uid must exactly match the key ($user_id)
        ".write": "$user_id === auth.uid"
      }
    }
  }
}

Structurer votre base de données pour prendre en charge les conditions d'authentification

Il est généralement utile de structurer votre base de données de manière à faciliter l'écriture de Rules. Un modèle courant pour stocker les données utilisateur dans Realtime Database consiste à stocker tous vos utilisateurs dans un seul nœud users dont les enfants sont les valeurs uid pour chaque utilisateur. Si vous souhaitez restreindre l'accès à ces données afin que seul l'utilisateur connecté puisse voir ses propres données, vos règles ressembleront à ceci.

{
  "rules": {
    "users": {
      "$uid": {
        ".read": "auth !== null && auth.uid === $uid"
      }
    }
  }
}

Utiliser des revendications personnalisées pour l'authentification

Pour les applications qui nécessitent un contrôle des accès personnalisé pour différents utilisateurs, Firebase Authentication permet aux développeurs de définir des revendications pour un utilisateur Firebase. Ces revendications sont accessibles dans la variable auth.token de vos règles. Voici un exemple de règles qui utilisent la revendication personnalisée hasEmergencyTowel :

{
  "rules": {
    "frood": {
      // A towel is about the most massively useful thing an interstellar
      // hitchhiker can have
      ".read": "auth.token.hasEmergencyTowel === true"
    }
  }
}

Les développeurs qui créent leurs propres jetons d'authentification personnalisés peuvent éventuellement ajouter des revendications à ces jetons. Ces revendications sont disponibles dans la variable auth.token de vos règles.

Données existantes et nouvelles données

La variable prédéfinie data est utilisée pour faire référence aux données avant qu'une opération d'écriture n'ait lieu. À l'inverse, la variable newData contient les nouvelles données qui existeront si l'opération d'écriture réussit. newData représente le résultat fusionné des nouvelles données écrites et des données existantes.

Par exemple, cette règle nous permettrait de créer des enregistrements ou d'en supprimer, mais pas de modifier des données existantes non nulles :

// we can write as long as old data or new data does not exist
// in other words, if this is a delete or a create, but not an update
".write": "!data.exists() || !newData.exists()"

Référencer des données dans d'autres chemins d'accès

Toutes les données peuvent être utilisées comme critère pour les règles. En utilisant les variables prédéfinies root, data et newData, nous pouvons accéder à n'importe quel chemin tel qu'il existerait avant ou après un événement d'écriture.

Prenons l'exemple suivant, qui autorise les opérations d'écriture tant que la valeur du nœud /allow_writes/ est true, que le nœud parent n'a pas de flag readOnly défini et qu'il existe un enfant nommé foo dans les données nouvellement écrites :

".write": "root.child('allow_writes').val() === true &&
          !data.parent().child('readOnly').exists() &&
          newData.child('foo').exists()"

Valider les données

L'application des structures de données et la validation du format et du contenu des données doivent être effectuées à l'aide de règles .validate, qui ne sont exécutées qu'après qu'une règle .write a réussi à accorder l'accès. Vous trouverez ci-dessous un exemple de définition de règle .validate qui n'autorise que les dates au format AAAA-MM-JJ entre les années 1900 et 2099, qui sont vérifiées à l'aide d'une expression régulière.

".validate": "newData.isString() &&
              newData.val().matches(/^(19|20)[0-9][0-9][-\\/. ](0[1-9]|1[012])[-\\/. ](0[1-9]|[12][0-9]|3[01])$/)"

Les règles .validate sont le seul type de règles de sécurité qui ne sont pas en cascade. Si une règle de validation échoue sur un enregistrement enfant, l'ensemble de l'opération d'écriture est rejeté. De plus, les définitions de validation sont ignorées lorsque des données sont supprimées (c'est-à-dire lorsque la nouvelle valeur écrite est null).

Ces points peuvent sembler anodins, mais ils constituent en fait des fonctionnalités importantes pour écrire des règles de sécurité puissantes pour la base de données en temps réel Firebase. Tenez compte des règles suivantes :

{
  "rules": {
    // write is allowed for all paths
    ".write": true,
    "widget": {
      // a valid widget must have attributes "color" and "size"
      // allows deleting widgets (since .validate is not applied to delete rules)
      ".validate": "newData.hasChildren(['color', 'size'])",
      "size": {
        // the value of "size" must be a number between 0 and 99
        ".validate": "newData.isNumber() &&
                      newData.val() >= 0 &&
                      newData.val() <= 99"
      },
      "color": {
        // the value of "color" must exist as a key in our mythical
        // /valid_colors/ index
        ".validate": "root.child('valid_colors/' + newData.val()).exists()"
      }
    }
  }
}

En gardant cette variante à l'esprit, examinons les résultats des opérations d'écriture suivantes :

JavaScript
var ref = db.ref("/widget");

// PERMISSION_DENIED: does not have children color and size
ref.set('foo');

// PERMISSION DENIED: does not have child color
ref.set({size: 22});

// PERMISSION_DENIED: size is not a number
ref.set({ size: 'foo', color: 'red' });

// SUCCESS (assuming 'blue' appears in our colors list)
ref.set({ size: 21, color: 'blue'});

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
ref.child('size').set(99);
Objective-C
Remarque : Ce produit Firebase n'est pas disponible sur la cible App Clip.
FIRDatabaseReference *ref = [[[FIRDatabase database] reference] child: @"widget"];

// PERMISSION_DENIED: does not have children color and size
[ref setValue: @"foo"];

// PERMISSION DENIED: does not have child color
[ref setValue: @{ @"size": @"foo" }];

// PERMISSION_DENIED: size is not a number
[ref setValue: @{ @"size": @"foo", @"color": @"red" }];

// SUCCESS (assuming 'blue' appears in our colors list)
[ref setValue: @{ @"size": @21, @"color": @"blue" }];

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
[[ref child:@"size"] setValue: @99];
Swift
Remarque : Ce produit Firebase n'est pas disponible sur la cible App Clip.
var ref = FIRDatabase.database().reference().child("widget")

// PERMISSION_DENIED: does not have children color and size
ref.setValue("foo")

// PERMISSION DENIED: does not have child color
ref.setValue(["size": "foo"])

// PERMISSION_DENIED: size is not a number
ref.setValue(["size": "foo", "color": "red"])

// SUCCESS (assuming 'blue' appears in our colors list)
ref.setValue(["size": 21, "color": "blue"])

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
ref.child("size").setValue(99);
Java
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("widget");

// PERMISSION_DENIED: does not have children color and size
ref.setValue("foo");

// PERMISSION DENIED: does not have child color
ref.child("size").setValue(22);

// PERMISSION_DENIED: size is not a number
Map<String,Object> map = new HashMap<String, Object>();
map.put("size","foo");
map.put("color","red");
ref.setValue(map);

// SUCCESS (assuming 'blue' appears in our colors list)
map = new HashMap<String, Object>();
map.put("size", 21);
map.put("color","blue");
ref.setValue(map);

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
ref.child("size").setValue(99);
REST
# PERMISSION_DENIED: does not have children color and size
curl -X PUT -d 'foo' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# PERMISSION DENIED: does not have child color
curl -X PUT -d '{"size": 22}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# PERMISSION_DENIED: size is not a number
curl -X PUT -d '{"size": "foo", "color": "red"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# SUCCESS (assuming 'blue' appears in our colors list)
curl -X PUT -d '{"size": 21, "color": "blue"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# If the record already exists and has a color, this will
# succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
# will fail to validate
curl -X PUT -d '99' \
https://docs-examples.firebaseio.com/rest/securing-data/example/size.json

Examinons maintenant la même structure, mais en utilisant des règles .write au lieu de .validate :

{
  "rules": {
    // this variant will NOT allow deleting records (since .write would be disallowed)
    "widget": {
      // a widget must have 'color' and 'size' in order to be written to this path
      ".write": "newData.hasChildren(['color', 'size'])",
      "size": {
        // the value of "size" must be a number between 0 and 99, ONLY IF WE WRITE DIRECTLY TO SIZE
        ".write": "newData.isNumber() && newData.val() >= 0 && newData.val() <= 99"
      },
      "color": {
        // the value of "color" must exist as a key in our mythical valid_colors/ index
        // BUT ONLY IF WE WRITE DIRECTLY TO COLOR
        ".write": "root.child('valid_colors/'+newData.val()).exists()"
      }
    }
  }
}

Dans cette variante, les opérations suivantes aboutiraient :

JavaScript
var ref = new Firebase(URL + "/widget");

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
ref.set({size: 99999, color: 'red'});

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
ref.child('size').set(99);
Objective-C
Remarque : Ce produit Firebase n'est pas disponible sur la cible App Clip.
Firebase *ref = [[Firebase alloc] initWithUrl:URL];

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
[ref setValue: @{ @"size": @9999, @"color": @"red" }];

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
[[ref childByAppendingPath:@"size"] setValue: @99];
Swift
Remarque : Ce produit Firebase n'est pas disponible sur la cible App Clip.
var ref = Firebase(url:URL)

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
ref.setValue(["size": 9999, "color": "red"])

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
ref.childByAppendingPath("size").setValue(99)
Java
Firebase ref = new Firebase(URL + "/widget");

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
Map<String,Object> map = new HashMap<String, Object>();
map.put("size", 99999);
map.put("color", "red");
ref.setValue(map);

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
ref.child("size").setValue(99);
REST
# ALLOWED? Even though size is invalid, widget has children color and size,
# so write is allowed and the .write rule under color is ignored
curl -X PUT -d '{size: 99999, color: "red"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# ALLOWED? Works even if widget does not exist, allowing us to create a widget
# which is invalid and does not have a valid color.
# (allowed by the write rule under "color")
curl -X PUT -d '99' \
https://docs-examples.firebaseio.com/rest/securing-data/example/size.json

Cela illustre les différences entre les règles .write et .validate. Comme indiqué, toutes ces règles doivent être écrites à l'aide de .validate, à l'exception possible de la règle newData.hasChildren(), qui dépend de la possibilité d'autoriser les suppressions.

Règles basées sur des requêtes

Bien que vous ne puissiez pas utiliser de règles comme filtres, vous pouvez limiter l'accès à des sous-ensembles de données en utilisant des paramètres de requête dans vos règles. Utilisez des expressions query. dans vos règles pour accorder un accès en lecture ou en écriture en fonction des paramètres de requête.

Par exemple, la règle basée sur une requête suivante utilise des règles de sécurité basées sur les utilisateurs et des règles basées sur des requêtes pour limiter l'accès aux données de la collection baskets aux paniers appartenant à l'utilisateur actif :

"baskets": {
  ".read": "auth.uid !== null &&
            query.orderByChild === 'owner' &&
            query.equalTo === auth.uid" // restrict basket access to owner of basket
}

La requête suivante, qui inclut les paramètres de requête dans la règle, aboutirait :

db.ref("baskets").orderByChild("owner")
                 .equalTo(auth.currentUser.uid)
                 .on("value", cb)                 // Would succeed

Toutefois, les requêtes qui n'incluent pas les paramètres de la règle échouent et renvoient une erreur PermissionDenied :

db.ref("baskets").on("value", cb)                 // Would fail with PermissionDenied

Vous pouvez également utiliser des règles basées sur des requêtes pour limiter la quantité de données qu'un client télécharge via des opérations de lecture.

Par exemple, la règle suivante limite l'accès en lecture aux 1 000 premiers résultats d'une requête, classés par priorité :

messages: {
  ".read": "query.orderByKey &&
            query.limitToFirst <= 1000"
}

// Example queries:

db.ref("messages").on("value", cb)                // Would fail with PermissionDenied

db.ref("messages").limitToFirst(1000)
                  .on("value", cb)                // Would succeed (default order by key)

Les expressions query. suivantes sont disponibles dans les règles de sécurité Realtime Database.

Expressions de règles basées sur des requêtes
Expression Type Description
query.orderByKey
query.orderByPriority
query.orderByValue
booléen True pour les requêtes triées par clé, priorité ou valeur. Sinon, cette valeur est "false".
query.orderByChild string
null
Utilisez une chaîne pour représenter le chemin d'accès relatif à un nœud enfant. Exemple : query.orderByChild === "address/zip". Si la requête n'est pas ordonnée par un nœud enfant, cette valeur est nulle.
query.startAt
query.endAt
query.equalTo
string
number
boolean
null
Récupère les limites de la requête en cours d'exécution ou renvoie la valeur null si aucune limite n'est définie.
query.limitToFirst
query.limitToLast
number
null
Récupère la limite de la requête en cours d'exécution ou renvoie la valeur "null" si aucune limite n'est définie.

Étapes suivantes

Après cette discussion sur les conditions, vous avez une compréhension plus sophistiquée de Rules et vous êtes prêt à :

Découvrez comment gérer les principaux cas d'utilisation, ainsi que le workflow de développement, de test et de déploiement de Rules :

Découvrez les fonctionnalités Rules spécifiques à Realtime Database :