Scopri la sintassi di base del linguaggio delle regole di sicurezza di Realtime Database

Le regole di sicurezza di Firebase Realtime Database ti consentono di controllare l'accesso ai dati memorizzati nel tuo database. La sintassi flessibile delle regole ti consente di creare regole che corrispondono a qualsiasi cosa, da tutte le scritture nel database alle operazioni sui singoli nodi.

Le regole di sicurezza di Realtime Database sono una configurazione declarativa per il database. Ciò significa che le regole vengono definite separatamente dalla logica del prodotto. Ciò presenta una serie di vantaggi: i clienti non sono responsabili dell'applicazione della sicurezza, le implementazioni con bug non comprometteranno i dati e, soprattutto, non è necessario un arbitro intermedio, come un server, per proteggere i dati da tutto il mondo.

Questo argomento descrive la sintassi e la struttura di base delle regole di sicurezza del Realtime Database utilizzate per creare set di regole completi.

Strutturare le regole di sicurezza

Le regole di sicurezza di Realtime Database sono costituite da espressioni simili a JavaScript contenute in un documento JSON. La struttura delle regole deve seguire quella dei dati archiviati nel database.

Le regole di base identificano un insieme di nodi da proteggere, i metodi di accesso (ad es. lettura, scrittura) coinvolti e le condizioni in base alle quali l'accesso è consentito o negato. Nei seguenti esempi, le nostre condizioni saranno semplici istruzioni true e false, ma il prossimo argomento tratteremo modi più dinamici per esprimere le condizioni.

Ad esempio, se stiamo cercando di proteggere un child_node sotto un parent_node, la sintassi generale da seguire è:

{
  "rules": {
    "parent_node": {
      "child_node": {
        ".read": <condition>,
        ".write": <condition>,
        ".validate": <condition>,
      }
    }
  }
}

Applichiamo questo pattern. Ad esempio, supponiamo che tu stia monitorando un elenco di messaggi e che tu abbia dati come questi:

{
  "messages": {
    "message0": {
      "content": "Hello",
      "timestamp": 1405704370369
    },
    "message1": {
      "content": "Goodbye",
      "timestamp": 1405704395231
    },
    ...
  }
}

Le regole devono essere strutturate in modo simile. Di seguito è riportato un insieme di regole per la sicurezza di sola lettura che potrebbero essere utili per questa struttura di dati. Questo esempio illustra come specificare i nodi del database a cui si applicano le regole e le condizioni per la valutazione delle regole in questi nodi.

{
  "rules": {
    // For requests to access the 'messages' node...
    "messages": {
      // ...and the individual wildcarded 'message' nodes beneath
      // (we'll cover wildcarding variables more a bit later)....
      "$message": {

        // For each message, allow a read operation if <condition>. In this
        // case, we specify our condition as "true", so read access is always granted.
        ".read": "true",

        // For read-only behavior, we specify that for write operations, our
        // condition is false.
        ".write": "false"
      }
    }
  }
}

Operazioni di base delle regole

Esistono tre tipi di regole per l'applicazione della sicurezza in base al tipo di operazione eseguita sui dati: .write, .read e .validate. Ecco un breve riepilogo delle relative finalità:

Tipi di regole
.read Descrive se e quando gli utenti possono leggere i dati.
.write Descrive se e quando è consentita la scrittura dei dati.
.validate Definisce l'aspetto di un valore con formattazione corretta, se ha attributi secondari e il tipo di dati.

Variabili di acquisizione con caratteri jolly

Tutte le istruzioni delle regole rimandano ai nodi. Un'istruzione può puntare a un nodo specifico o utilizzare le variabili di acquisizione con caratteri jolly $ per puntare a insiemi di nodi a un livello gerarchico. Utilizza queste variabili di acquisizione per archiviare il valore delle chiavi nodo da utilizzare all'interno di istruzioni di regole successive. Questa tecnica ti consente di scrivere Rules condizioni più complesse, un argomento che tratteremo più dettagliatamente nel prossimo argomento.

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

Le variabili dinamiche $ possono essere utilizzate anche in parallelo con i nomi di percorso costanti. In questo esempio, utilizziamo la variabile $other per dichiarare una regola .validate che garantisce che widget non abbia elementi secondari diversi da title e color. Qualsiasi scrittura che comporti la creazione di elementi figlio aggiuntivi non andrà a buon fine.

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

Lettura e scrittura delle regole in cascata

Le regole .read e .write funzionano dall'alto verso il basso, con regole più superficiali che prevalgono su quelle più profonde. Se una regola concede autorizzazioni di lettura o scrittura in un determinato percorso, concede anche l'accesso a tutti i nodi secondari sottostanti. Considera la seguente struttura:

{
  "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 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. Lo illustreremo quando parleremo della sicurezza basata sugli utenti più avanti in questa guida.

Tieni presente che le regole .validate non si applicano in modo ricorsivo. Tutte le regole di convalida devono essere soddisfatte a tutti i livelli della gerarchia affinché sia consentita una scrittura.

Le regole non sono filtri

Le regole vengono applicate in modo atomico. Ciò significa che un'operazione di lettura o scrittura non va a buon fine immediatamente se non esiste una regola in quella posizione 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
      }
    }
  }
}

Senza aver compreso che le regole vengono valutate a livello 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 della nostra console Firebase, possiamo vedere che l'operazione di lettura è stata rifiutata perché nessuna regola di lettura consentiva l'accesso al percorso /records/. Tuttavia, tieni presente che la regola per rec1 non è mai stata valutata perché non era nel percorso richiesto. 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!

Dichiarazioni sovrapposte

È possibile che a un nodo venga applicata più di una regola. Nel caso in cui più espressioni di regole identifichino un nodo, il metodo di accesso viene negato se una delle condizioni è false:

{
  "rules": {
    "messages": {
      // A rule expression that applies to all nodes in the 'messages' node
      "$message": {
        ".read": "true",
        ".write": "true"
      },
      // A second rule expression applying specifically to the 'message1` node
      "message1": {
        ".read": "false",
        ".write": "false"
      }
    }
  }
}

Nell'esempio riportato sopra, le letture del nodo message1 verranno rifiutate perché la seconda regola è sempre false, anche se la prima regola è sempre true.

Passaggi successivi

Per approfondire le regole di sicurezza di Firebase Realtime Database:

  • Scopri il concetto principale successivo del linguaggio Rules, le condizioni dinamiche, che consentono a Rules di controllare l'autorizzazione utente, confrontare i dati esistenti e in entrata, convalidare i dati in entrata, controllare la struttura delle query provenienti dal client e altro ancora.

  • Esamina i casi d'uso di sicurezza tipici e le definizioni delle Regole di sicurezza Firebase che li risolvono.