Come funzionano le regole di sicurezza

La sicurezza può essere uno degli aspetti più complessi dello sviluppo di app. Nella maggior parte delle applicazioni, gli sviluppatori devono creare ed eseguire un server che gestisca l'autenticazione (chi è un utente) e l'autorizzazione (cosa può fare un utente).

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. Utilizza questa guida per scoprire di più su come vengono applicate le regole alle richieste in arrivo.

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

Cloud Firestore

Struttura di base

Firebase Security Rules in Cloud Firestore e Cloud Storage utilizzano 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>>
  }
}

I seguenti concetti chiave sono importanti da comprendere durante la creazione delle regole:

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

Regole di sicurezza versione 2

A partire da maggio 2019, è disponibile la versione 2 delle regole di sicurezza Firebase. La versione 2 delle regole modifica 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 inserendo 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 indirizzare a documenti, non a raccolte. Un'istruzione di corrispondenza può puntare a un documento specifico, come in match /cities/SF, o utilizzare caratteri jolly per puntare a qualsiasi documento nel percorso specificato, come in match /cities/{city}.

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

Sottoraccolte corrispondenti

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

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

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

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 qualsiasi 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.
    match /cities/{document} {
      allow read, write: if true;
    }
  }
}

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

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 con caratteri jolly ricorsivi, la variabile jolly conterrà l'intero segmento di percorso corrispondente, anche se il documento si trova in una raccolta secondaria nidificata in profondità. Ad esempio, le regole elencate corrisponderebbero a un documento che si trova in /cities/SF/landmarks/coit_tower e il valore della variabile document sarebbe SF/landmarks/coit_tower.

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

Versione 1

Le regole di sicurezza utilizzano la versione 1 per impostazione predefinita. 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 sottoraccolte, ma non nella raccolta cities, mentre match /cities/{document=**} corrisponde sia ai documenti nella raccolta cities sia nelle sottoraccolte.

I caratteri jolly ricorsivi devono essere inseriti 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 in qualsiasi sottoraccolta, nonché ai documenti nella raccolta cities.

Devi attivare la versione 2 aggiungendo rules_version = '2'; all'inizio delle 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 ovunque nell'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.

Limiti delle regole di sicurezza

Quando lavori con 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 dalla CLI utilizzando 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 utilizzano 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>>
  }
}

I seguenti concetti chiave sono importanti da comprendere durante la creazione delle regole:

  • Richiesta:il metodo o i metodi richiamati nell'istruzione allow. Questi sono i metodi che consenti di eseguire. I metodi standard sono: get, list, create, update e delete. I metodi pratici read e write consentono un ampio accesso in lettura e scrittura al database o al percorso di archiviazione specificato.
  • Percorso: il database o la posizione di archiviazione, rappresentati come un percorso URI.
  • Regola:l'istruzione allow, che include una condizione che consente una richiesta se restituisce il 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 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 restituisce il valore false, la richiesta viene negata.

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 caratteri jolly. Un carattere jolly è una variabile denominata 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 le parentesi graffe intorno 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 è l'OR del risultato di tutte le valutazioni delle regole. ovvero, se una regola a cui corrisponde il file restituisce true, il risultato è true.

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

È possibile fare riferimento a una variabile jolly dall'interno dell'autorizzazione di match per fornire il nome file o il percorso:

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

Cloud Storage Security Rules non vengono applicate a cascata e le regole vengono valutate solo quando il percorso della richiesta corrisponde a un percorso con regole specificate.

Richiedi valutazione

Caricamenti, download, modifiche ai metadati ed eliminazioni vengono valutati utilizzando request inviato a Cloud Storage. La variabile request contiene il percorso in cui viene eseguita la richiesta, l'ora in cui viene ricevuta e il nuovo valore resource se la richiesta è una scrittura. Sono incluse 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 più nel dettaglio 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 delle 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, potresti anche voler valutare i metadati del file che viene caricato, scaricato, modificato o eliminato. In questo modo puoi creare regole complesse e potenti che consentono, ad esempio, di caricare solo file con determinati tipi di contenuti o di eliminare solo file di dimensioni superiori a una determinata soglia.

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 esaminate 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 esistente nel percorso della richiesta, puoi utilizzare anche 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 dell'applicazione, ad esempio tipo o dimensione del file.

È disponibile un elenco completo delle proprietà nell'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 dell'oggetto di questo oggetto.
metageneration int La Google Cloud Storage generazione dei metadati 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 contiene tutti questi elementi, ad eccezione di generation, metageneration, etag, timeCreated e updated.

Limiti delle regole di sicurezza

Quando lavori con 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 conteggiate ai fini dei limiti.

Esempio completo

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

service firebase.storage {
 match /b/{bucket}/o {
   match /images {
     // 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) Filename (stored in imageId wildcard variable) is less than 32 characters
     match /{imageId} {
       allow read;
       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 sono costituite 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 è composta da tre elementi di base:

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

Come vengono applicate le regole ai percorsi

In Realtime Database, Rules vengono applicate in modo atomico, il che significa che le regole nei nodi principali di livello superiore sostituiscono le regole nei nodi secondari più granulari e le regole in 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 l'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/ da ogni posizione in cui /foo/ contiene un elemento secondario baz con valore true. La regola ".read": false in /foo/bar/ non ha alcun effetto qui, poiché l'accesso non può essere revocato da un percorso secondario.

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

Tuttavia, le regole .validate non vengono applicate a cascata. Tutte le regole di convalida devono essere soddisfatte a tutti i livelli della gerarchia per consentire una scrittura.

Inoltre, poiché le regole non vengono applicate a un percorso principale, l'operazione di lettura o scrittura non riesce se non esiste 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 nella destinazione 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 nella destinazione 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 conceda 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 negata:

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

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

L'operazione è stata negata perché nessuna regola di lettura consentiva l'accesso al percorso /records/, ma tieni presente che la regola per rec1 non è mai stata valutata perché non si trovava nel percorso che abbiamo richiesto. Per recuperare rec1, dovremmo 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 nella destinazione App Clip.
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
[[ref child:@"records/rec1"] observeSingleEventOfType:FEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
    // SUCCESS!
}];
Swift
Nota:questo prodotto Firebase non è disponibile nella destinazione 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 di località

Realtime Database Rules supportano una variabile $location per la corrispondenza dei segmenti di percorso. Utilizza il prefisso $ davanti al segmento del percorso per far corrispondere la regola a qualsiasi nodo secondario 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 i nomi dei percorsi 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 }
      }
    }
  }