Come funzionano le regole di sicurezza

La sicurezza può essere uno dei componenti più complessi del puzzle dello sviluppo di app. Nella maggior parte delle applicazioni, gli sviluppatori devono creare ed eseguire un server che gestisca l'autenticazione (l'identità di un utente) e l'autorizzazione (ciò che un utente può fare).

Firebase Security Rules rimuove il livello intermedio (server) e ti consente di specificare autorizzazioni basate sul percorso per i client che si connettono direttamente ai tuoi dati. Consulta questa guida per scoprire di più su come vengono applicate le regole alle richieste in arrivo.

Seleziona un prodotto per scoprire di più sulle relative regole.

Cloud Firestore

Struttura di base

Firebase Security Rules in Cloud Firestore e Cloud Storage utilizza la seguente struttura e sintassi:

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

Quando crei le regole, è importante comprendere i seguenti concetti chiave:

  • Richiesta: il metodo o i metodi invocati nell'istruzione allow. Si tratta di metodi che consenti di eseguire. I metodi standard sono: get, list, create, update e delete. I metodi di utilità read e write consentono un accesso in lettura e scrittura ampio al database o al percorso di archiviazione specificato.
  • Percorso: il database o la posizione di archiviazione, rappresentata come percorso URI.
  • Regola: l'istruzione allow, che include una condizione che consente una richiesta se ha valore true.

Versione 2 delle regole di sicurezza

Da maggio 2019 è disponibile la versione 2 delle regole di sicurezza Firebase. La versione 2 delle regole cambia il comportamento dei caratteri jolly ricorsivi {name=**}. Devi utilizzare la versione 2 se prevedi di utilizzare le query sui gruppi di raccolte. Devi attivare la versione 2 impostando rules_version = '2'; come prima riga delle regole di sicurezza:

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

Percorsi corrispondenti

Tutte le istruzioni di corrispondenza devono puntare a documenti, non a raccolte. Un'istruzione di corrispondenza può fare riferimento a un documento specifico, come in match /cities/SF, oppure utilizzare caratteri jolly per fare riferimento a qualsiasi documento nel percorso specificato, come in match /cities/{city}.

Nell'esempio precedente, l'istruzione match utilizza la sintassi dei caratteri jolly {city}. Ciò significa che la regola si applica a qualsiasi documento della raccolta cities, ad esempio /cities/SF o /cities/NYC. Quando le espressioni allow nell'istruzione di corrispondenza vengono valutate, la variabile city viene risolta nel nome del documento della città, ad esempio SF o NYC.

Sottocollezioni corrispondenti

I dati in Cloud Firestore sono organizzati in raccolte di documenti e ogni documento può estendere la gerarchia tramite sottocollezioni. È importante comprendere come le regole di sicurezza interagiscono con i dati gerarchici.

Considera la situazione in cui ogni documento della raccolta cities contiene una raccolta secondaria landmarks. Le regole di sicurezza si applicano solo al percorso corrispondente, pertanto i controlli di accesso definiti nella raccolta cities non si applicano alla sottoraccolta landmarks. Scrivi invece regole esplicite per controllare l'accesso alle sottocollezioni:

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>;
      }
    }
  }
}

Quando nidifichi le istruzioni match, il percorso dell'istruzione match interna è sempre relativo al percorso dell'istruzione match esterna. Pertanto, i seguenti set di regole sono equivalenti:

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>;
    }
  }
}

Caratteri jolly ricorsivi

Se vuoi che le regole vengano applicate a una gerarchia arbitrariamente profonda, utilizza la sintassi del carattere jolly ricorsivo {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>;
    }
  }
}

Quando utilizzi la sintassi dei caratteri jolly ricorsivi, la variabile jolly conterrà l'intero segmento di percorso corrispondente, anche se il documento si trova in una sottoraccolta nidificata in modo complesso. Ad esempio, le regole elencate sopra corrispondono a un documento situato in /cities/SF/landmarks/coit_tower e il valore della variabile document è SF/landmarks/coit_tower.

Tieni presente, tuttavia, che il comportamento dei caratteri jolly ricorsivi dipende dalla versione delle regole.

Versione 1

Per impostazione predefinita, le regole di sicurezza utilizzano la versione 1. Nella versione 1, i caratteri jolly ricorsivi corrispondono a uno o più elementi del percorso. Non corrispondono a un percorso vuoto, quindi match /cities/{city}/{document=**} corrisponde ai documenti nelle raccolte secondarie, ma non nella raccolta cities, mentre match /cities/{document=**} corrisponde a entrambi i documenti nelle raccolte cities e nelle raccolte secondarie.

I caratteri jolly ricorsivi devono trovarsi alla fine di un'istruzione di corrispondenza.

Versione 2

Nella versione 2 delle regole di sicurezza, i caratteri jolly ricorsivi corrispondono a zero o più elementi del percorso. match/cities/{city}/{document=**} corrisponde ai documenti di qualsiasi sottoraccolta, nonché ai documenti della raccolta cities.

Devi attivare la versione 2 aggiungendo rules_version = '2'; nella parte superiore delle tue regole di sicurezza:

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>;
    }
  }
}

Puoi avere al massimo un carattere jolly ricorsivo per istruzione di corrispondenza, ma nella versione 2 puoi posizionarlo in qualsiasi punto dell'istruzione di corrispondenza. Ad esempio:

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>;
    }
  }
}

Se utilizzi le query sui gruppi di raccolte, devi utilizzare la versione 2. Consulta la sezione Proteggere le query sui gruppi di raccolte.

Istruzioni di corrispondenza sovrapposte

È possibile che un documento corrisponda a più di un'istruzione match. Nel caso in cui più espressioni allow corrispondano a una richiesta, l'accesso è consentito se una delle condizioni è 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;
    }
  }
}

Nell'esempio precedente, tutte le letture e le scritture nella raccolta cities saranno consentite perché la seconda regola è sempre true, anche se la prima regola è sempre false.

Limiti delle regole di sicurezza

Quando utilizzi le regole di sicurezza, tieni presente i seguenti limiti:

Limite Dettagli
Numero massimo di chiamate exists(), get() e getAfter() per richiesta
  • 10 per richieste di documenti singoli e di query.
  • 20 per transazioni, operazioni di scrittura in batch e operazioni di lettura di più documenti. A ciascuna operazione si applica anche il limite precedente di 10.

    Ad esempio, immagina di creare una richiesta di scrittura in batch con 3 operazioni di scrittura e che le tue regole di sicurezza utilizzino 2 chiamate di accesso ai documenti per convalidare ogni operazione di scrittura. In questo caso, ogni operazione di scrittura utilizza 2 delle sue 10 chiamate di accesso e la richiesta di scrittura in batch utilizza 6 delle sue 20 chiamate di accesso.

Il superamento di uno dei limiti comporta un errore di autorizzazione negata.

Alcune chiamate di accesso ai documenti possono essere memorizzate nella cache e le chiamate nella cache non vengono considerate ai fini dei limiti.

Profondità massima delle istruzioni match nidificate 10
Lunghezza massima del percorso, in segmenti di percorso, consentita all'interno di un set di istruzioni match nidificate 100
Numero massimo di variabili di acquisizione percorso consentite all'interno di un set di istruzioni match nidificate 20
Profondità massima delle chiamate funzione 20
Numero massimo di argomenti di funzione 7
Numero massimo di associazioni di variabili let per funzione 10
Numero massimo di chiamate di funzione ricorsive o cicliche 0 (non consentite)
Numero massimo di espressioni valutate per richiesta 1000
Dimensione massima di un set di regole I set di regole devono rispettare due limiti di dimensione:
  • Un limite di 256 KB per la dimensione dell'origine di testo del set di regole pubblicata dalla console Firebase o dall'interfaccia a riga di comando mediante firebase deploy.
  • Un limite di 250 kB per la dimensione del set di regole compilato che risulta quando Firebase elabora l'origine e la rende attiva sul back-end.

Cloud Storage

Struttura di base

Firebase Security Rules in Cloud Firestore e Cloud Storage utilizza la seguente struttura e sintassi:

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

Quando crei le regole, è importante comprendere i seguenti concetti chiave:

  • Richiesta: il metodo o i metodi invocati nell'istruzione allow. Si tratta di metodi che consenti di eseguire. I metodi standard sono: get, list, create, update e delete. I metodi di utilità read e write consentono un accesso in lettura e scrittura ampio al database o al percorso di archiviazione specificato.
  • Percorso: il database o la posizione di archiviazione, rappresentata come percorso URI.
  • Regola: l'istruzione allow, che include una condizione che consente una richiesta se ha valore true.

Percorsi corrispondenti

Cloud Storage Security Rules match i percorsi dei file utilizzati per accedere ai file in Cloud Storage. Le regole possono match includere percorsi esatti o con caratteri jolly e possono anche essere nidificate. Se nessuna regola di corrispondenza consente un metodo di richiesta o se la condizione è valutata come false, la richiesta viene rifiutata.

Corrispondenze esatte

// 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>;
}

Corrispondenze nidificate

// 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>;
  }
}

Corrispondenze con caratteri jolly

Le regole possono essere utilizzate anche per match un pattern utilizzando i caratteri jolly. Un carattere jolly è una variabile imendata che rappresenta una singola stringa, ad esempio profilePhoto.png, o più segmenti di percorso, ad esempio images/profilePhoto.png.

Un carattere jolly viene creato aggiungendo parentesi graffe al nome del carattere jolly, ad esempio {string}. Un carattere jolly per più segmenti può essere dichiarato aggiungendo =** al nome del carattere jolly, ad esempio {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>;
  }
}

Se più regole corrispondono a un file, il risultato è il OR del risultato di tutte le valutazioni delle regole. In altre parole, se una regola a cui corrisponde il file restituisce true, il risultato è true.

Nelle regole precedenti, il file "images/profilePhoto.png" può essere letto se condition o other_condition valutano true, mentre il file "images/users/user:12345/profilePhoto.png" è soggetto solo al risultato di other_condition.

È possibile fare riferimento a una variabile jolly all'interno dell'autorizzazione del nome o del percorso del file fornito in match:

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

Cloud Storage Security Rules non si applicano in modo ricorsivo e le regole vengono valutate solo quando il percorso della richiesta corrisponde a un percorso con regole specificate.

Richiedi valutazione

I caricamenti, i download, le modifiche e le eliminazioni dei metadati vengono valutati utilizzando il request inviato a Cloud Storage. La variabile request contiene il percorso del file in cui viene eseguita la richiesta, l'ora in cui la richiesta viene ricevuta e il nuovo valore resource se la richiesta è di scrittura. Sono inclusi anche le intestazioni HTTP e lo stato di autenticazione.

L'oggetto request contiene anche l'ID univoco dell'utente e il payload Firebase Authentication nell'oggetto request.auth, che verrà spiegato ulteriormente nella sezione Autenticazione della documentazione.

Di seguito è riportato un elenco completo delle proprietà dell'oggetto request:

Proprietà Tipo Descrizione
auth map<string, string> Quando un utente ha eseguito l'accesso, fornisce uid, l'ID univoco dell'utente, e token, una mappa di attestazioni JWT Firebase Authentication. In caso contrario, sarà null.
params map<string, string> Mappa contenente i parametri di query della richiesta.
path percorso Un path che rappresenta il percorso in cui viene eseguita la richiesta.
resource map<string, string> Il nuovo valore della risorsa, presente solo nelle richieste write.
time timestamp Un timestamp che rappresenta l'ora del server in cui viene valutata la richiesta.

Valutazione delle risorse

Quando valuti le regole, ti consigliamo di valutare anche i metadati del file caricato, scaricato, modificato o eliminato. In questo modo, puoi creare regole complesse e potenti che, ad esempio, consentono di caricare solo file con determinati tipi di contenuti o di eliminare solo file di dimensioni superiori a una determinata.

Firebase Security Rules per Cloud Storage fornisce i metadati del file nell'oggetto resource, che contiene coppie chiave/valore dei metadati visualizzati in un oggetto Cloud Storage. Queste proprietà possono essere controllate nelle richieste read o write per garantire l'integrità dei dati.

Nelle richieste write (come caricamenti, aggiornamenti dei metadati ed eliminazioni), oltre all'oggetto resource, che contiene i metadati del file attualmente esistente nel percorso della richiesta, puoi anche utilizzare l'oggetto request.resource, che contiene un sottoinsieme dei metadati del file da scrivere se la scrittura è consentita. Puoi utilizzare questi due valori per garantire l'integrità dei dati o applicare vincoli di applicazione come il tipo o le dimensioni del file.

Di seguito è riportato un elenco completo delle proprietà dell'oggetto resource:

Proprietà Tipo Descrizione
name stringa Il nome completo dell'oggetto
bucket stringa Il nome del bucket in cui si trova questo oggetto.
generation int La Google Cloud Storage generazione di oggetti di questo oggetto.
metageneration int La Google Cloud Storage metagenerazione dell'oggetto di questo oggetto.
size int Le dimensioni dell'oggetto in byte.
timeCreated timestamp Un timestamp che rappresenta l'ora di creazione di un oggetto.
updated timestamp Un timestamp che rappresenta l'ora dell'ultimo aggiornamento di un oggetto.
md5Hash stringa Un hash MD5 dell'oggetto.
crc32c stringa Un hash crc32c dell'oggetto.
etag stringa L'etag associato a questo oggetto.
contentDisposition stringa La disposizione dei contenuti associata a questo oggetto.
contentEncoding stringa La codifica dei contenuti associata a questo oggetto.
contentLanguage stringa La lingua dei contenuti associata a questo oggetto.
contentType stringa Il tipo di contenuti associato a questo oggetto.
metadata map<string, string> Coppie chiave/valore di metadati personalizzati aggiuntivi specificati dallo sviluppatore.

request.resource li contiene tutti, ad eccezione di generation, metageneration, etag, timeCreated e updated.

Limiti delle regole di sicurezza

Quando utilizzi le regole di sicurezza, tieni presente i seguenti limiti:

Limite Dettagli
Numero massimo di chiamate firestore.exists() e firestore.get() per richiesta

2 per richieste di documenti singoli e di query.

Il superamento di questo limite comporta un errore di autorizzazione negata.

Le chiamate di accesso agli stessi documenti possono essere memorizzate nella cache e le chiamate nella cache non vengono considerate ai fini dei limiti.

Esempio completo

Mettendo tutto insieme, puoi creare un esempio completo di regole per una soluzione di archiviazione delle immagini:

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

Realtime Database

Struttura di base

In Realtime Database, Firebase Security Rules è costituito da espressioni simili a JavaScript contenute in un documento JSON.

Utilizzano la seguente sintassi:

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

La regola contiene tre elementi di base:

  • Percorso: la posizione del database. Questo rispecchia la struttura JSON del database.
  • Richiesta:sono i metodi utilizzati dalla regola per concedere l'accesso. Le regole read e write concedono un accesso ampio in lettura e scrittura, mentre le regole validate agiscono come verifica secondaria per concedere l'accesso in base ai dati in entrata o esistenti.
  • Condizione:la condizione che consente una richiesta se ha valore true.

Come vengono applicate le regole ai percorsi

In Realtime Database, Rules si applicano in modo atomico, il che significa che le regole dei nodi principali di livello superiore sostituiscono le regole dei nodi secondari più granulari e le regole di un nodo più profondo non possono concedere l'accesso a un percorso principale. Non puoi perfezionare o revocare l'accesso a un percorso più profondo nella struttura del database se lo hai già concesso per uno dei percorsi principali.

Considera le seguenti regole:

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

Questa struttura di sicurezza consente di leggere /bar/ ogni volta che /foo/ contiene un elemento figlio baz con valore true. La regola ".read": false in /foo/bar/ non ha alcun effetto in questo caso, poiché l'accesso non può essere revocato da un percorso secondario.

Sebbene possa non sembrare immediatamente intuitivo, si tratta di una parte efficace del linguaggio delle regole e consente di implementare privilegi di accesso molto complessi con il minimo sforzo. Questo è particolarmente utile per la sicurezza basata sugli utenti.

Tuttavia, le regole .validate non si applicano in modo ricorsivo. Affinché una scrittura sia consentita, tutte le regole di convalida devono essere soddisfatte a tutti i livelli della gerarchia.

Inoltre, poiché le regole non vengono applicate nuovamente a un percorso principale, le operazioni di lettura o scrittura non vanno a buon fine se non è presente una regola nella posizione richiesta o in una posizione principale che concede l'accesso. Anche se ogni percorso secondario interessato è accessibile, la lettura nella posizione principale non andrà a buon fine. Considera questa struttura:

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

Se non si comprende che le regole vengono valutate in modo atomico, potrebbe sembrare che il recupero del percorso /records/ restituisca rec1, ma non rec2. Il risultato effettivo, tuttavia, è un errore:

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
Nota:questo prodotto Firebase non è disponibile come target di 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
Nota:questo prodotto Firebase non è disponibile come target di 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

Poiché l'operazione di lettura in /records/ è atomica e non esiste una regola di lettura che consenta l'accesso a tutti i dati in /records/, verrà generato un errore PERMISSION_DENIED. Se valutiamo questa regola nel simulatore di sicurezza nella nostra console Firebase, possiamo vedere che l'operazione di lettura è stata rifiutata:

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

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

L'operazione è stata rifiutata perché nessuna regola di lettura consentiva l'accesso al percorso /records/, ma tieni presente che la regola per /records/ non è mai stata valutata perché non era nel percorso che abbiamo richiesto.rec1 Per recuperare rec1, dobbiamo accedervi direttamente:

JavaScript
var db = firebase.database();
db.ref("records/rec1").once("value", function(snap) {
  // SUCCESS!
}, function(err) {
  // error callback is not called
});
Objective-C
Nota:questo prodotto Firebase non è disponibile come target di App Clip.
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
[[ref child:@"records/rec1"] observeSingleEventOfType:FEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
    // SUCCESS!
}];
Swift
Nota:questo prodotto Firebase non è disponibile come target di 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!

Variabile posizione

Realtime Database Rules supportano una variabile $location per associare i segmenti di percorso. Utilizza il prefisso $ davanti al segmento del percorso per associare la regola a eventuali nodi secondari lungo il percorso.

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

Puoi anche utilizzare $variable in parallelo con nomi di percorso costanti.

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