Check out what’s new from Firebase at Google I/O 2022. Learn more

Come funzionano le regole di sicurezza

La sicurezza può essere uno dei pezzi più complessi del puzzle di sviluppo delle 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).

Le regole di sicurezza Firebase rimuovono il livello intermedio (server) e ti consentono di specificare autorizzazioni basate sul percorso per i client che si connettono direttamente ai tuoi dati. Usa questa guida per saperne di più su come vengono applicate le regole alle richieste in arrivo.

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

Cloud Firestore

Struttura basilare

Le regole di sicurezza Firebase in Cloud Firestore e Cloud Storage utilizzano la struttura e la sintassi seguenti:

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 invocati nell'istruzione allow . Questi sono metodi che stai permettendo di eseguire. I metodi standard sono: get , list , create , update e delete . I metodi pratici di read e write consentono un ampio accesso in lettura e scrittura al database o al percorso di archiviazione specificato.
  • Percorso: il database o il percorso di archiviazione, rappresentato come un percorso URI.
  • Regola: l'istruzione allow , che include una condizione che consente una richiesta se risulta true.

Regole di sicurezza versione 2

A partire da maggio 2019, è ora disponibile la versione 2 delle regole di sicurezza di Firebase. La versione 2 delle regole modifica il comportamento dei caratteri jolly ricorsivi {name=**} . È necessario utilizzare la versione 2 se si prevede di utilizzare le query del gruppo di raccolta . Devi acconsentire alla versione 2 rules_version = '2'; la prima riga nelle tue regole di sicurezza:

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

Percorsi di corrispondenza

Tutte le dichiarazioni di corrispondenza devono puntare a documenti, non a raccolte. Una dichiarazione 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 precedente, l'istruzione match utilizza la sintassi del carattere jolly {city} . Ciò significa che la regola si applica a qualsiasi documento nella collezione delle cities , come /cities/SF o /cities/NYC . Quando vengono valutate le espressioni di allow nella dichiarazione di corrispondenza, la variabile della city si risolverà 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 sottoraccolte. È importante capire come le regole di sicurezza interagiscono con i dati gerarchici.

Si consideri la situazione in cui ogni documento nella raccolta delle cities contiene una sottoraccolta di landmarks di riferimento. Le regole di sicurezza si applicano solo al percorso abbinato, quindi i controlli di accesso definiti nella raccolta delle cities non si applicano alla sottoraccolta dei landmarks di riferimento. Invece, scrivi regole esplicite per controllare l'accesso alle sottoraccolte:

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 si annidano le istruzioni di match , il percorso dell'istruzione di match interna è sempre relativo al percorso dell'istruzione di match esterna. I seguenti set di regole sono quindi 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 si applichino a una gerarchia arbitrariamente profonda, usa la sintassi ricorsiva dei caratteri jolly, {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 si utilizza la sintassi ricorsiva dei caratteri jolly, la variabile dei caratteri jolly conterrà l'intero segmento del percorso corrispondente, anche se il documento si trova in una sottoraccolta profondamente nidificata. Ad esempio, le regole sopra elencate corrisponderebbero a un documento che si trova in /cities/SF/landmarks/coit_tower e il valore della variabile del document sarebbe SF/landmarks/coit_tower .

Si noti, 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=**} trova i documenti nelle sottoraccolte ma non nella raccolta delle cities , mentre match /cities/{document=**} trova entrambi i documenti nella raccolta e sottoraccolte di cities .

I caratteri jolly ricorsivi devono trovarsi alla fine di una dichiarazione 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=**} abbina i documenti in tutte le sottoraccolte così come i documenti nella raccolta delle cities .

Devi accettare la versione 2 aggiungendo rules_version = '2'; in cima alle 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 dichiarazione di corrispondenza, ma nella versione 2 puoi inserire questo carattere jolly in qualsiasi punto della dichiarazione di corrispondenza. Per 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 query sui gruppi di raccolte , devi utilizzare la versione 2, vedi protezione delle query sui gruppi di raccolte .

Dichiarazioni di corrispondenza sovrapposte

È possibile che un documento corrisponda a più di una dichiarazione di match . Nel caso in cui più espressioni di 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 sopra, tutte le letture e le scritture nella raccolta delle cities saranno consentite perché la seconda regola è sempre true , anche se la prima regola è sempre false .

Limiti delle regole di sicurezza

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

Limite Particolari
Numero massimo di chiamate exist( exists() , get() e getAfter() per richiesta
  • 10 per le richieste di documenti singoli e le richieste di interrogazione.
  • 20 per letture, transazioni e scritture batch di più documenti. Ad ogni operazione vale anche il precedente limite di 10.

    Ad esempio, immagina di creare una richiesta di scrittura in batch con 3 operazioni di scrittura e che le regole di sicurezza utilizzino 2 chiamate di accesso ai documenti per convalidare ogni scrittura. In questo caso, ogni 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 provoca un errore di autorizzazione negata.

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

Profondità massima dell'istruzione di match nidificata 10
Lunghezza massima del percorso, in segmenti di percorso, consentita all'interno di una serie di istruzioni di match nidificate 100
Numero massimo di variabili di acquisizione del percorso consentite all'interno di un set di istruzioni di match nidificate 20
Profondità massima di chiamata della funzione 20
Numero massimo di argomenti di funzione 7
Numero massimo di associazioni let variabili per funzione 10
Numero massimo di richiami di funzioni ricorsivi o ciclici 0 (non consentito)
Numero massimo di espressioni valutate per richiesta 1.000
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 del testo del set di regole pubblicata dalla console Firebase o dall'interfaccia a riga di comando utilizzando firebase deploy .
  • un limite di 250 KB sulla dimensione del set di regole compilato che risulta quando Firebase elabora il sorgente e lo rende attivo sul back-end.

Archiviazione su cloud

Struttura basilare

Le regole di sicurezza Firebase in Cloud Firestore e Cloud Storage utilizzano la struttura e la sintassi seguenti:

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 invocati nell'istruzione allow . Questi sono metodi che stai permettendo di eseguire. I metodi standard sono: get , list , create , update e delete . I metodi pratici di read e write consentono un ampio accesso in lettura e scrittura al database o al percorso di archiviazione specificato.
  • Percorso: il database o il percorso di archiviazione, rappresentato come un percorso URI.
  • Regola: l'istruzione allow , che include una condizione che consente una richiesta se risulta true.

Percorsi di corrispondenza

Le regole di sicurezza di Cloud Storage match ai percorsi dei file utilizzati per accedere ai file in Cloud Storage. Le regole possono match a percorsi esatti o percorsi con caratteri jolly e le regole possono anche essere nidificate. Se nessuna regola di corrispondenza consente un metodo di richiesta o la condizione restituisce 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>;
}

Partite 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 jolly

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

Un carattere jolly viene creato aggiungendo parentesi graffe attorno al nome del carattere jolly, come {string} . È possibile dichiarare un carattere jolly a più segmenti aggiungendo =** al nome del carattere jolly, come {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. Cioè, se una qualsiasi regola il file corrisponde a true , il risultato è true .

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

È possibile fare riferimento a una variabile con caratteri jolly dall'interno della match fornire il nome del file o l'autorizzazione del percorso:

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

Le regole di sicurezza dell'archiviazione cloud non sono a cascata e le regole vengono valutate solo quando il percorso della richiesta corrisponde a un percorso con le regole specificate.

Richiedi valutazione

Caricamenti, download, modifiche ai metadati ed eliminazioni vengono valutati utilizzando la request inviata a Cloud Storage. La variabile di request contiene il percorso del file in cui viene eseguita la richiesta, l'ora in cui la richiesta viene ricevuta e il nuovo valore della resource se la richiesta è una 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 di autenticazione Firebase nell'oggetto request.auth , che verrà spiegato più avanti nella sezione Autenticazione dei documenti.

Di seguito è disponibile un elenco completo delle proprietà nell'oggetto request :

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

Valutazione delle risorse

Durante la valutazione delle regole, potresti anche voler valutare i metadati del file che viene caricato, scaricato, modificato o eliminato. Ciò ti consente di creare regole complesse e potenti che fanno cose come consentire solo il caricamento di file con determinati tipi di contenuto o l'eliminazione solo di file maggiori di una determinata dimensione.

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

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

Di seguito è disponibile un elenco completo delle proprietà nell'oggetto resource :

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

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

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

Database in tempo reale

Struttura basilare

In Realtime Database, le regole di sicurezza Firebase sono costituite da espressioni simili a JavaScript contenute in un documento JSON.

Usano la seguente sintassi:

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

Ci sono tre elementi fondamentali nella regola:

  • Percorso: la posizione del database. Questo rispecchia la struttura JSON del tuo database.
  • Richiesta: questi sono i metodi utilizzati dalla regola per concedere l'accesso. Le regole di read e write garantiscono un ampio accesso in lettura e scrittura, mentre le regole di 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 true.

Come si applicano le regole ai percorsi

In Realtime Database, le regole si applicano in modo atomico, il che significa che le regole sui nodi principali di livello superiore sovrascrivono le regole sui nodi figlio più granulari e le regole su un nodo più profondo non possono concedere l'accesso a un percorso padre. 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 baz figlio 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 figlio.

Anche se potrebbe non sembrare immediatamente intuitivo, questa è 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 sull'utente .

Tuttavia, le regole .validate non si sovrappongono. Tutte le regole di convalida devono essere soddisfatte a tutti i livelli della gerarchia affinché una scrittura sia consentita.

Inoltre, poiché le regole non si applicano a un percorso padre, l'operazione di lettura o scrittura ha esito negativo se non è presente una regola nel percorso richiesto o in un percorso padre che concede l'accesso. Anche se ogni percorso figlio interessato è accessibile, la lettura nella posizione genitore fallirà completamente. Considera questa struttura:

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

Senza comprendere che le regole vengono valutate atomicamente, 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
});
Obiettivo-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
}];
Veloce
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
})
Giava
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
  });
});
RIPOSO
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 garantisca 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 ha consentito l'accesso al percorso /records/ , ma si noti che la regola per rec1 non è mai stata valutata perché non era nel percorso 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
});
Obiettivo-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!
}];
Veloce
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!
})
Giava
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
  }
});
RIPOSO
curl https://docs-examples.firebaseio.com/rest/records/rec1
# SUCCESS!

Posizione variabile

Le regole del database in tempo reale supportano una variabile $location per abbinare i segmenti di percorso. Usa il prefisso $ davanti al tuo segmento di percorso per abbinare la tua regola a qualsiasi nodo figlio 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 usare la $variable in parallelo con nomi di 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 }
      }
    }
  }