Fonctionnement des règles de sécurité

La sécurité peut être l'une des pièces les plus complexes du puzzle du développement d'applications. Dans la plupart des applications, les développeurs doivent créer et exécuter un serveur qui gère l'authentification (qui est un utilisateur) et l'autorisation (ce qu'un utilisateur peut faire).

Les règles de sécurité Firebase suppriment la couche intermédiaire (serveur) et vous permettent de spécifier des autorisations basées sur le chemin pour les clients qui se connectent directement à vos données. Utilisez ce guide pour en savoir plus sur la façon dont les règles sont appliquées aux demandes entrantes.

Sélectionnez un produit pour en savoir plus sur ses règles.

Cloud Firestore

Structure basique

Les règles de sécurité Firebase dans Cloud Firestore et Cloud Storage utilisent la structure et la syntaxe suivantes :

service <<name>> {
  // Match the resource path.
  match <<path>> {
    // Allow the request if the following conditions are true.
    allow <<methods>> : if <<condition>>
  }
}

Il est important de comprendre les concepts clés suivants lorsque vous créez les règles :

  • Request : la ou les méthodes invoquées dans l'instruction allow . Ce sont des méthodes que vous autorisez à exécuter. Les méthodes standard sont : get , list , create , update et delete . Les méthodes pratiques read et write permettent un large accès en lecture et en écriture sur la base de données ou le chemin de stockage spécifié.
  • Chemin : la base de données ou l'emplacement de stockage, représenté sous la forme d'un chemin URI.
  • Règle : L'instruction allow , qui inclut une condition qui autorise une demande si elle est évaluée comme vraie.

Règles de sécurité version 2

Depuis mai 2019, la version 2 des règles de sécurité Firebase est désormais disponible. La version 2 des règles modifie le comportement des caractères génériques récursifs {name=**} . Vous devez utiliser la version 2 si vous envisagez d'utiliser des requêtes de groupe de collections . Vous devez vous inscrire à la version 2 en faisant rules_version = '2'; la première ligne de vos règles de sécurité :

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

Chemins correspondants

Toutes les instructions de correspondance doivent pointer vers des documents, et non vers des collections. Une instruction match peut pointer vers un document spécifique, comme dans match /cities/SF ou utiliser des caractères génériques pour pointer vers n'importe quel document dans le chemin spécifié, comme dans match /cities/{city} .

Dans l'exemple ci-dessus, l'instruction match utilise la syntaxe générique {city} . Cela signifie que la règle s'applique à tout document de la collection cities , comme /cities/SF ou /cities/NYC . Lorsque les expressions allow dans l'instruction de correspondance sont évaluées, la variable city sera résolue en nom de document de ville, tel que SF ou NYC .

Sous-collections correspondantes

Les données dans Cloud Firestore sont organisées en collections de documents, et chaque document peut étendre la hiérarchie via des sous-collections. Il est important de comprendre comment les règles de sécurité interagissent avec les données hiérarchiques.

Considérez la situation où chaque document de la collection cities contient une sous-collection landmarks . Les règles de sécurité s'appliquent uniquement au chemin correspondant, de sorte que les contrôles d'accès définis sur la collection cities ne s'appliquent pas à la sous-collection landmarks . Écrivez plutôt des règles explicites pour contrôler l'accès aux sous-collections :

service cloud.firestore {
  match /databases/{database}/documents {
    match /cities/{city} {
      allow read, write: if <condition>;

      // Explicitly define rules for the 'landmarks' subcollection
      match /landmarks/{landmark} {
        allow read, write: if <condition>;
      }
    }
  }
}

Lors de l'imbrication d'instructions match , le chemin de l'instruction match interne est toujours relatif au chemin de l'instruction match externe. Les ensembles de règles suivants sont donc équivalents :

service cloud.firestore {
  match /databases/{database}/documents {
    match /cities/{city} {
      match /landmarks/{landmark} {
        allow read, write: if <condition>;
      }
    }
  }
}
service cloud.firestore {
  match /databases/{database}/documents {
    match /cities/{city}/landmarks/{landmark} {
      allow read, write: if <condition>;
    }
  }
}

Caractères génériques récursifs

Si vous souhaitez que les règles s'appliquent à une hiérarchie arbitrairement profonde, utilisez la syntaxe générique récursive, {name=**} :

service cloud.firestore {
  match /databases/{database}/documents {
    // Matches any document in the cities collection as well as any document
    // in a subcollection.
    match /cities/{document=**} {
      allow read, write: if <condition>;
    }
  }
}

Lors de l'utilisation de la syntaxe générique récursive, la variable générique contiendra l'intégralité du segment de chemin correspondant, même si le document se trouve dans une sous-collection profondément imbriquée. Par exemple, les règles répertoriées ci-dessus correspondraient à un document situé dans /cities/SF/landmarks/coit_tower , et la valeur de la variable document serait SF/landmarks/coit_tower .

Notez cependant que le comportement des caractères génériques récursifs dépend de la version des règles.

Version 1

Les règles de sécurité utilisent la version 1 par défaut. Dans la version 1, les caractères génériques récursifs correspondent à un ou plusieurs éléments de chemin. Ils ne correspondent pas à un chemin vide, donc match /cities/{city}/{document=**} correspond aux documents dans les sous-collections mais pas dans la collection cities , alors que match /cities/{document=**} correspond aux deux documents dans le collections et sous-collections cities .

Les caractères génériques récursifs doivent figurer à la fin d'une instruction de correspondance.

Version 2

Dans la version 2 des règles de sécurité, les caractères génériques récursifs correspondent à zéro ou plusieurs éléments de chemin. match/cities/{city}/{document=**} correspond aux documents de toutes les sous-collections ainsi qu'aux documents de la collection cities .

Vous devez vous inscrire à la version 2 en ajoutant rules_version = '2'; au sommet de vos règles de sécurité :

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Matches any document in the cities collection as well as any document
    // in a subcollection.
    match /cities/{city}/{document=**} {
      allow read, write: if <condition>;
    }
  }
}

Vous pouvez avoir au plus un caractère générique récursif par instruction de correspondance, mais dans la version 2, vous pouvez placer ce caractère générique n'importe où dans l'instruction de correspondance. Par exemple:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Matches any document in the songs collection group
    match /{path=**}/songs/{song} {
      allow read, write: if <condition>;
    }
  }
}

Si vous utilisez des requêtes de groupe de collections , vous devez utiliser la version 2, voir sécurisation des requêtes de groupe de collections .

Chevauchement des déclarations de correspondance

Il est possible qu'un document corresponde à plusieurs instructions de match . Dans le cas où plusieurs expressions allow correspondent à une requête, l'accès est autorisé si l'une des conditions est true :

service cloud.firestore {
  match /databases/{database}/documents {
    // Matches any document in the 'cities' collection.
    match /cities/{city} {
      allow read, write: if false;
    }

    // Matches any document in the 'cities' collection or subcollections.
    match /cities/{document=**} {
      allow read, write: if true;
    }
  }
}

Dans l'exemple ci-dessus, toutes les lectures et écritures dans la collection cities seront autorisées car la deuxième règle est toujours true , même si la première règle est toujours false .

Limites des règles de sécurité

Lorsque vous travaillez avec des règles de sécurité, notez les limites suivantes :

Limite Détails
Nombre maximal d'appels exists() , get() et getAfter() par requête
  • 10 pour les demandes de document unique et les demandes de requête.
  • 20 pour les lectures de plusieurs documents, les transactions et les écritures par lots. La limite précédente de 10 s'applique également à chaque opération.

    Par exemple, imaginez que vous créez une demande d'écriture par lot avec 3 opérations d'écriture et que vos règles de sécurité utilisent 2 appels d'accès aux documents pour valider chaque écriture. Dans ce cas, chaque écriture utilise 2 de ses 10 appels d'accès et la demande d'écriture groupée utilise 6 de ses 20 appels d'accès.

Le dépassement de l'une ou l'autre des limites entraîne une erreur d'autorisation refusée.

Certains appels d'accès aux documents peuvent être mis en cache et les appels mis en cache ne sont pas pris en compte dans les limites.

Profondeur maximale de l'instruction match imbriquée dix
Longueur maximale du chemin, en segments de chemin, autorisée dans un ensemble d'instructions match imbriquées 100
Nombre maximal de variables de capture de chemin autorisées dans un ensemble d'instructions match correspondance imbriquées 20
Profondeur maximale d'appel de fonction 20
Nombre maximal d'arguments de fonction 7
Nombre maximal de liaisons de variables let par fonction dix
Nombre maximal d'appels de fonction récursifs ou cycliques 0 (non autorisé)
Nombre maximal d'expressions évaluées par requête 1 000
Taille maximale d'un ensemble de règles Les ensembles de règles doivent respecter deux limites de taille :
  • une limite de 256 Ko sur la taille de la source de texte de l'ensemble de règles publié à partir de la console Firebase ou de la CLI à l'aide firebase deploy .
  • une limite de 250 Ko sur la taille de l'ensemble de règles compilé qui se produit lorsque Firebase traite la source et la rend active sur le back-end.

Stockage en ligne

Structure basique

Les règles de sécurité Firebase dans Cloud Firestore et Cloud Storage utilisent la structure et la syntaxe suivantes :

service <<name>> {
  // Match the resource path.
  match <<path>> {
    // Allow the request if the following conditions are true.
    allow <<methods>> : if <<condition>>
  }
}

Il est important de comprendre les concepts clés suivants lorsque vous créez les règles :

  • Request : la ou les méthodes invoquées dans l'instruction allow . Ce sont des méthodes que vous autorisez à exécuter. Les méthodes standard sont : get , list , create , update et delete . Les méthodes pratiques read et write permettent un large accès en lecture et en écriture sur la base de données ou le chemin de stockage spécifié.
  • Chemin : la base de données ou l'emplacement de stockage, représenté sous la forme d'un chemin URI.
  • Règle : L'instruction allow , qui inclut une condition qui autorise une demande si elle est évaluée comme vraie.

Chemins correspondants

Les règles de sécurité de Cloud Storage match aux chemins d'accès aux fichiers utilisés pour accéder aux fichiers dans Cloud Storage. Les règles peuvent match à des chemins exacts ou à des chemins génériques, et les règles peuvent également être imbriquées. Si aucune règle de correspondance n'autorise une méthode de demande, ou si la condition est évaluée à false , la demande est refusée.

Correspondances exactes

// Exact match for "images/profilePhoto.png"
match /images/profilePhoto.png {
  allow write: if <condition>;
}

// Exact match for "images/croppedProfilePhoto.png"
match /images/croppedProfilePhoto.png {
  allow write: if <other_condition>;
}

Correspondances imbriquées

// Partial match for files that start with "images"
match /images {
  // Exact match for "images/profilePhoto.png"
  match /profilePhoto.png {
    allow write: if <condition>;
  }

  // Exact match for "images/croppedProfilePhoto.png"
  match /croppedProfilePhoto.png {
    allow write: if <other_condition>;
  }
}

Matchs génériques

Les règles peuvent également être utilisées pour match un modèle à l'aide de caractères génériques. Un caractère générique est une variable nommée qui représente soit une seule chaîne telle que profilePhoto.png , soit plusieurs segments de chemin, tels que images/profilePhoto.png .

Un caractère générique est créé en ajoutant des accolades autour du nom du caractère générique, comme {string} . Un caractère générique à plusieurs segments peut être déclaré en ajoutant =** au nom du caractère générique, comme {path=**} :

// Partial match for files that start with "images"
match /images {
  // Exact match for "images/*"
  // e.g. images/profilePhoto.png is matched
  match /{imageId} {
    // This rule only matches a single path segment (*)
    // imageId is a string that contains the specific segment matched
    allow read: if <condition>;
  }

  // Exact match for "images/**"
  // e.g. images/users/user:12345/profilePhoto.png is matched
  // images/profilePhoto.png is also matched!
  match /{allImages=**} {
    // This rule matches one or more path segments (**)
    // allImages is a path that contains all segments matched
    allow read: if <other_condition>;
  }
}

Si plusieurs règles correspondent à un fichier, le résultat est l' OR du résultat de toutes les évaluations de règles. Autrement dit, si une règle que le fichier correspond est évaluée à true , le résultat est true .

Dans les règles ci-dessus, le fichier "images/profilePhoto.png" peut être lu si l'une ou l'autre condition ou other_condition est évaluée à true, tandis que le fichier "images/users/user:12345/profilePhoto.png" n'est soumis qu'au résultat de other_condition .

Une variable générique peut être référencée à partir de la match fournir un nom de fichier ou une autorisation de chemin :

// Another way to restrict the name of a file
match /images/{imageId} {
  allow read: if imageId == "profilePhoto.png";
}

Les règles de sécurité Cloud Storage ne se cascadent pas et les règles ne sont évaluées que lorsque le chemin de la demande correspond à un chemin avec les règles spécifiées.

Demande d'évaluation

Les importations, les téléchargements, les modifications de métadonnées et les suppressions sont évalués à l'aide de la request envoyée à Cloud Storage. La variable request contient le chemin du fichier où la requête est exécutée, l'heure à laquelle la requête est reçue et la nouvelle valeur resource si la requête est une écriture. Les en-têtes HTTP et l'état d'authentification sont également inclus.

L'objet request contient également l'ID unique de l'utilisateur et la charge utile Firebase Authentication dans l'objet request.auth , qui sera expliqué plus en détail dans la section Authentification de la documentation.

Une liste complète des propriétés dans l'objet request est disponible ci-dessous :

Propriété Taper Description
auth carte<chaîne, chaîne> Lorsqu'un utilisateur est connecté, fournit uid , l'ID unique de l'utilisateur et token , une carte des revendications Firebase Authentication JWT. Sinon, ce sera null .
params carte<chaîne, chaîne> Carte contenant les paramètres de requête de la requête.
path chemin Un path représentant le chemin sur lequel la requête est exécutée.
resource carte<chaîne, chaîne> La nouvelle valeur de ressource, présente uniquement sur les demandes write .
time horodatage Un horodatage représentant l'heure du serveur à laquelle la demande est évaluée.

Évaluation des ressources

Lors de l'évaluation des règles, vous pouvez également souhaiter évaluer les métadonnées du fichier en cours de chargement, de téléchargement, de modification ou de suppression. Cela vous permet de créer des règles complexes et puissantes qui autorisent uniquement le téléchargement de fichiers avec certains types de contenu ou la suppression de seuls fichiers supérieurs à une certaine taille.

Les règles de sécurité Firebase pour Cloud Storage fournissent des métadonnées de fichier dans l'objet resource , qui contient des paires clé/valeur des métadonnées présentées dans un objet Cloud Storage. Ces propriétés peuvent être inspectées lors de demandes read ou write pour garantir l'intégrité des données.

Sur les demandes write (telles que les téléchargements, les mises à jour de métadonnées et les suppressions), en plus de l'objet resource , qui contient les métadonnées de fichier pour le fichier qui existe actuellement sur le chemin de la demande, vous avez également la possibilité d'utiliser l'objet request.resource , qui contient un sous-ensemble des métadonnées du fichier à écrire si l'écriture est autorisée. Vous pouvez utiliser ces deux valeurs pour garantir l'intégrité des données ou appliquer des contraintes d'application telles que le type ou la taille des fichiers.

Une liste complète des propriétés de l'objet resource est disponible ci-dessous :

Propriété Taper Description
name chaîne Le nom complet de l'objet
bucket chaîne Le nom du compartiment dans lequel cet objet réside.
generation entier La génération d'objet Google Cloud Storage de cet objet.
metageneration entier La métagénération d'objet Google Cloud Storage de cet objet.
size entier Taille de l'objet en octets.
timeCreated horodatage Un horodatage représentant l'heure à laquelle un objet a été créé.
updated horodatage Un horodatage représentant l'heure à laquelle un objet a été mis à jour pour la dernière fois.
md5Hash chaîne Un hachage MD5 de l'objet.
crc32c chaîne Un hachage crc32c de l'objet.
etag chaîne Etag associé à cet objet.
contentDisposition chaîne Disposition du contenu associée à cet objet.
contentEncoding chaîne Le codage de contenu associé à cet objet.
contentLanguage chaîne La langue du contenu associée à cet objet.
contentType chaîne Type de contenu associé à cet objet.
metadata carte<chaîne, chaîne> Paires clé/valeur de métadonnées personnalisées supplémentaires spécifiées par le développeur.

request.resource contient tous ces éléments à l'exception de generation , metageneration , etag , timeCreated et updated .

Limites des règles de sécurité

Lorsque vous travaillez avec des règles de sécurité, notez les limites suivantes :

Limite Détails
Nombre maximal d'appels firestore.exists() et firestore.get() par requête

2 pour les demandes de document unique et les demandes de requête.

Le dépassement de cette limite entraîne une erreur d'autorisation refusée.

Les appels d'accès aux mêmes documents peuvent être mis en cache et les appels mis en cache ne comptent pas dans les limites.

Exemple complet

En mettant tout cela ensemble, vous pouvez créer un exemple complet de règles pour une solution de stockage d'images :

service firebase.storage {
 match /b/{bucket}/o {
   match /images {
     // Cascade read to any image type at any path
     match /{allImages=**} {
       allow read;
     }

     // Allow write files to the path "images/*", subject to the constraints:
     // 1) File is less than 5MB
     // 2) Content type is an image
     // 3) Uploaded content type matches existing content type
     // 4) File name (stored in imageId wildcard variable) is less than 32 characters
     match /{imageId} {
       allow write: if request.resource.size < 5 * 1024 * 1024
                    && request.resource.contentType.matches('image/.*')
                    && request.resource.contentType == resource.contentType
                    && imageId.size() < 32
     }
   }
 }
}

Base de données en temps réel

Structure basique

Dans Realtime Database, les règles de sécurité Firebase consistent en des expressions de type JavaScript contenues dans un document JSON.

Ils utilisent la syntaxe suivante :

{
  "rules": {
    "<<path>>": {
    // Allow the request if the condition for each method is true.
      ".read": <<condition>>,
      ".write": <<condition>>,
      ".validate": <<condition>>
    }
  }
}

Il y a trois éléments de base dans la règle :

  • Chemin : L'emplacement de la base de données. Cela reflète la structure JSON de votre base de données.
  • Demande : il s'agit des méthodes utilisées par la règle pour accorder l'accès. Les règles read et write accordent un large accès en lecture et en écriture, tandis que les règles validate agissent comme une vérification secondaire pour accorder l'accès en fonction des données entrantes ou existantes.
  • Condition : la condition qui autorise une demande si elle est évaluée comme vraie.

Comment les règles s'appliquent aux chemins

Dans la base de données en temps réel, les règles s'appliquent de manière atomique, ce qui signifie que les règles des nœuds parents de niveau supérieur remplacent les règles des nœuds enfants plus granulaires et que les règles d'un nœud plus profond ne peuvent pas accorder l'accès à un chemin parent. Vous ne pouvez pas affiner ou révoquer l'accès à un chemin plus profond dans la structure de votre base de données si vous l'avez déjà accordé pour l'un des chemins parents.

Tenez compte des règles suivantes :

{
  "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 à /bar/ d'être lu 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.

Bien que cela ne semble pas immédiatement intuitif, il s'agit d'une partie puissante du langage des règles et permet de mettre en œuvre des privilèges d'accès très complexes avec un minimum d'effort. Ceci est particulièrement utile pour la sécurité basée sur l'utilisateur .

Cependant, les règles .validate ne fonctionnent pas 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.

De plus, étant donné que les règles ne s'appliquent pas à un chemin parent, l'opération de lecture ou d'écriture échoue s'il n'y a pas de règle à l'emplacement demandé ou à un emplacement parent qui accorde l'accès. Même si chaque chemin enfant concerné est accessible, 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, cependant, 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
});
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'aucune règle de lecture n'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 :

Attempt to read /records with auth=Success(null)
    /
    /records

No .read rule allowed the operation.
Read was denied.

L'opération a été refusée car aucune règle de lecture n'autorisait l'accès au chemin /records/ , mais notez que la règle pour rec1 n'a jamais été évaluée car elle ne se trouvait pas dans le chemin 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!

Variable de localisation

Les règles de base de données en temps réel prennent en charge une variable $location pour faire correspondre les segments de chemin. Utilisez le préfixe $ devant votre segment de chemin pour faire correspondre votre règle à tous les nœuds enfants le long du chemin.

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

Vous pouvez également utiliser la $variable en parallèle avec des noms de chemin constants.

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