Proteggi i tuoi dati Firestore con le regole di sicurezza di Firebase

1. Prima di iniziare

Cloud Firestore, Cloud Storage for Firebase e Realtime Database si basano sui file di configurazione che scrivi per concedere l'accesso in lettura e scrittura. La configurazione, chiamata Regole di sicurezza, può anche fungere da tipo di schema per l'app. È una delle parti più importanti dello sviluppo della tua applicazione. Questo codelab ti guiderà nella procedura.

Prerequisiti

  • Un editor semplice come Visual Studio Code, Atom o Sublime Text
  • Node.js 8.6.0 o versioni successive (per installare Node.js, utilizza nvm; per controllare la versione, esegui node --version)
  • Java 7 o versioni successive (per installare Java utilizza queste istruzioni; per controllare la versione, esegui java -version)

Attività previste

In questo codelab proteggerai una semplice piattaforma blog basata su Firestore. Utilizzerai l'emulatore Firestore per eseguire i test delle unità in base alle regole di sicurezza e assicurarti che le regole consentano o meno l'accesso previsto.

Al termine del corso sarai in grado di:

  • Concedi autorizzazioni granulari
  • Applica convalide dei dati e dei tipi
  • Implementare il controllo degli accessi basato su attributi
  • Concedi l'accesso in base al metodo di autenticazione
  • Creare funzioni personalizzate
  • Creare regole di sicurezza basate sul tempo
  • Implementare un elenco valori non consentiti ed eliminazioni temporanee
  • Scopri quando denormalizzare i dati per soddisfare più pattern di accesso

2. Configura

Si tratta di un'applicazione di blogging. Ecco un riepilogo generale delle funzionalità dell'applicazione:

Bozza di post del blog:

  • Gli utenti possono creare bozze di post del blog, che sono disponibili nella raccolta drafts.
  • L'autore può continuare ad aggiornare una bozza finché non è pronta per essere pubblicata.
  • Quando è pronto per la pubblicazione, viene attivata una funzione Firebase che crea un nuovo documento nella raccolta published.
  • Le bozze possono essere eliminate dall'autore o dai moderatori del sito

Post del blog pubblicati:

  • I post pubblicati non possono essere creati dagli utenti, ma solo tramite una funzione.
  • Possono essere solo eliminati temporaneamente, il che aggiorna un attributo visible in false.

Commenti

  • I post pubblicati consentono i commenti, che sono una sottoraccolta di ogni post pubblicato.
  • Per ridurre gli abusi, gli utenti devono avere un indirizzo email verificato e non devono far parte di una lista bloccata per poter lasciare un commento.
  • I commenti possono essere aggiornati solo entro un'ora dalla loro pubblicazione.
  • I commenti possono essere eliminati dall'autore del commento, dall'autore del post originale o dai moderatori.

Oltre alle regole di accesso, creerai regole di sicurezza che applicano convalide dei dati e dei campi obbligatori.

Tutto avverrà localmente, utilizzando Firebase Emulator Suite.

Ottieni il codice sorgente

In questo codelab, inizierai con i test per le regole di sicurezza, ma minimizzando le regole di sicurezza stesse, quindi la prima cosa da fare è clonare il codice sorgente per eseguire i test:

$ git clone https://github.com/FirebaseExtended/codelab-rules.git

Quindi vai alla directory stato-iniziale, in cui lavorerai per il resto di questo codelab:

$ cd codelab-rules/initial-state

Ora installa le dipendenze per eseguire i test. Se la connessione a internet è lenta, l'operazione potrebbe richiedere un paio di minuti:

# Move into the functions directory, install dependencies, jump out.
$ cd functions && npm install && cd -

Ottieni l'interfaccia a riga di comando di Firebase

La suite di emulatori che utilizzerai per eseguire i test fa parte dell'interfaccia a riga di comando (CLI) di Firebase, che può essere installata sulla tua macchina con il seguente comando:

$ npm install -g firebase-tools

Quindi, verifica di avere la versione più recente dell'interfaccia a riga di comando. Questo codelab dovrebbe funzionare con la versione 8.4.0 o successive, ma le versioni successive includono più correzioni di bug.

$ firebase --version
9.10.2

3. Esegui i test

In questa sezione eseguirai i test in locale. Questo significa che è il momento di avviare Emulator Suite.

Avvia gli emulatori

L'applicazione con cui lavorerai ha tre collezioni Firestore principali: drafts contiene i post del blog in corso, la collezione published contiene i post del blog che sono stati pubblicati e comments è una sottoraccolta dei post pubblicati. Il repo include test di unità per le regole di sicurezza che definiscono gli attributi utente e altre condizioni necessarie per consentire a un utente di creare, leggere, aggiornare ed eliminare documenti nelle raccolte drafts, published e comments. Dovrai scrivere le regole di sicurezza per far superare questi test.

Per iniziare, il database è bloccato: le operazioni di lettura e scrittura sul database sono rifiutate universalmente e tutti i test hanno esito negativo. Man mano che scrivi le regole di sicurezza, i test vengono superati. Per visualizzare i test, apri functions/test.js nell'editor.

Nella riga di comando, avvia gli emulatori utilizzando emulators:exec ed esegui i test:

$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test"

Scorri fino all'inizio dell'output:

$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test"
i  emulators: Starting emulators: functions, firestore, hosting
⚠  functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: auth, database, pubsub
⚠  functions: Unable to fetch project Admin SDK configuration, Admin SDK behavior in Cloud Functions emulator may be incorrect.
i  firestore: Importing data from /Users/user/src/firebase/rules-codelab/initial-state/.seed/firestore_export/firestore_export.overall_export_metadata
i  firestore: Firestore Emulator logging to firestore-debug.log
⚠  hosting: Authentication error when trying to fetch your current web app configuration, have you run firebase login?
⚠  hosting: Could not fetch web app configuration and there is no cached configuration on this machine. Check your internet connection and make sure you are authenticated. To continue, you must call firebase.initializeApp({...}) in your code before using Firebase.
i  hosting: Serving hosting files from: public
✔  hosting: Local server: http://localhost:5000
i  functions: Watching "/Users/user/src/firebase/rules-codelab/initial-state/functions" for Cloud Functions...
✔  functions[publishPost]: http function initialized (http://localhost:5001/codelab/us-central1/publishPost).
✔  functions[softDelete]: http function initialized (http://localhost:5001/codelab/us-central1/softDelete).
i  Running script: pushd functions; npm test
~/src/firebase/rules-codelab/initial-state/functions ~/src/firebase/rules-codelab/initial-state

> functions@ test /Users/user/src/firebase/rules-codelab/initial-state/functions
> mocha

(node:76619) ExperimentalWarning: Conditional exports is an experimental feature. This feature could change at any time


  Draft blog posts
    1) can be created with required fields by the author
    2) can be updated by author if immutable fields are unchanged
    3) can be read by the author and moderator

  Published blog posts
    4) can be read by everyone; created or deleted by no one
    5) can be updated by author or moderator

  Comments on published blog posts
    6) can be read by anyone with a permanent account
    7) can be created if email is verfied and not blocked
    8) can be updated by author for 1 hour after creation
    9) can be deleted by an author or moderator


  0 passing (848ms)
  9 failing

...

Al momento ci sono 9 errori. Man mano che crei il file delle regole, puoi misurare i progressi osservando il superamento di un numero maggiore di test.

4. Creare bozze dei post del blog.

Poiché l'accesso alle bozze dei post del blog è molto diverso dall'accesso ai post del blog pubblicati, questa app di blogging archivia le bozze dei post del blog in una raccolta separata, /drafts. Le bozze sono accessibili solo all'autore o a un moderatore e prevedono convalide per i campi obbligatori e immutabili.

Se apri il file firestore.rules, troverai un file di regole predefinite:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if false;
    }
  }
}

L'istruzione di corrispondenza match /{document=**} utilizza la sintassi ** per applicarsi in modo ricorsivo a tutti i documenti delle sottoraccolte. Poiché si trova al livello più alto, al momento la stessa regola generale si applica a tutte le richieste, indipendentemente da chi le effettua o dai dati che stanno cercando di leggere o scrivere.

Inizia rimuovendo l'istruzione di corrispondenza più interna e sostituendola con match /drafts/{draftID}. I commenti sulla struttura dei documenti possono essere utili nelle regole e saranno inclusi in questo codelab; sono sempre facoltativi.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional
    }
  }
}

La prima regola che scriverai per le bozze controllerà chi può creare i documenti. In questa applicazione, le bozze possono essere create solo dalla persona indicata come autore. Verifica che l'UID della persona che effettua la richiesta sia lo stesso UID indicato nel documento.

La prima condizione per la creazione sarà:

request.resource.data.authorUID == request.auth.uid

Inoltre, i documenti possono essere creati solo se includono i tre campi obbligatori authorUID, createdAt e title. L'utente non fornisce il campo createdAt; questo impone all'app di aggiungerlo prima di provare a creare un documento. Poiché devi solo verificare che gli attributi vengano creati, puoi controllare che request.resource abbia tutte le chiavi:

request.resource.data.keys().hasAll([
  "authorUID",
  "createdAt",
  "title"
])

L'ultimo requisito per creare un post del blog è che il titolo non può contenere più di 50 caratteri:

request.resource.data.title.size() < 50

Poiché tutte queste condizioni devono essere vere, concatenale con l'operatore logico AND, &&. La prima regola diventa:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;
    }
  }
}

Nel terminale, esegui di nuovo i test e verifica che il primo test sia superato.

5. Aggiornare le bozze dei post del blog.

Successivamente, man mano che gli autori perfezionano le bozze dei loro post del blog, potranno modificare le bozze dei documenti. Crea una regola per le condizioni in cui un post può essere aggiornato. Innanzitutto, solo l'autore può aggiornare le bozze. Tieni presente che qui controlli l'UID già scritto,resource.data.authorUID:

resource.data.authorUID == request.auth.uid

Il secondo requisito per un aggiornamento è che due attributi, authorUID e createdAt, non devono cambiare:

request.resource.data.diff(resource.data).unchangedKeys().hasAll([
    "authorUID",
    "createdAt"
]);

Infine, il titolo deve contenere al massimo 50 caratteri:

request.resource.data.title.size() < 50;

Poiché tutte queste condizioni devono essere soddisfatte, concatenale con &&:

allow update: if
  // User is the author, and
  resource.data.authorUID == request.auth.uid &&
  // `authorUID` and `createdAt` are unchanged
  request.resource.data.diff(resource.data).unchangedKeys().hasAll([
    "authorUID",
    "createdAt"
  ]) &&
  // Title must be < 50 characters long
  request.resource.data.title.size() < 50;

Le regole complete diventano:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow update: if
        // User is the author, and
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;
    }
  }
}

Esegui di nuovo i test e verifica che un altro test sia superato.

6. Eliminare e leggere le bozze: controllo dell'accesso basato su attributi

Così come gli autori possono creare e aggiornare le bozze, possono anche eliminarle.

resource.data.authorUID == request.auth.uid

Inoltre, gli autori con un attributo isModerator nel token di autenticazione sono autorizzati a eliminare le bozze:

request.auth.token.isModerator == true

Poiché una di queste condizioni è sufficiente per un'eliminazione, concatenale con un operatore logico OR ||:

allow delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true

Le stesse condizioni si applicano alle letture, per cui è possibile aggiungere l'autorizzazione alla regola:

allow read, delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true

Ora le regole complete sono:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow update: if
        // User is the author, and
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow read, delete: if
        // User is draft author
        resource.data.authorUID == request.auth.uid ||
        // User is a moderator
        request.auth.token.isModerator == true;
    }
  }
}

Esegui di nuovo i test e verifica che un altro test ora sia superato.

7. Legge, crea ed elimina i post pubblicati: denormalizzazione per diversi pattern di accesso

Poiché i pattern di accesso per i post pubblicati e le bozze di post sono molto diversi, questa app denormalizza i post in raccolte draft e published separate. Ad esempio, i post pubblicati possono essere letti da chiunque, ma non possono essere eliminati definitivamente, mentre le bozze possono essere eliminate, ma possono essere lette solo dall'autore e dai moderatori. In questa app, quando un utente vuole pubblicare una bozza di post del blog, viene attivata una funzione che crea il nuovo post pubblicato.

Successivamente, scriverai le regole per i post pubblicati. Le regole più semplici da scrivere sono che i post pubblicati possono essere letti da chiunque e non possono essere creati o eliminati da nessuno. Aggiungi queste regole:

match /published/{postID} {
  // `authorUID`: string, required
  // `content`: string, required
  // `publishedAt`: timestamp, required
  // `title`: string, < 50 characters, required
  // `url`: string, required
  // `visible`: boolean, required

  // Can be read by everyone
  allow read: if true;

  // Published posts are created only via functions, never by users
  // No hard deletes; soft deletes update `visible` field.
  allow create, delete: if false;
}

Se le aggiungi alle regole esistenti, l'intero file delle regole diventa:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow update: if
        // User is the author, and
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow read, delete: if
        // User is draft author
        resource.data.authorUID == request.auth.uid ||
        // User is a moderator
        request.auth.token.isModerator == true;
    }

    match /published/{postID} {
      // `authorUID`: string, required
      // `content`: string, required
      // `publishedAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, required
      // `visible`: boolean, required

      // Can be read by everyone
      allow read: if true;

      // Published posts are created only via functions, never by users
      // No hard deletes; soft deletes update `visible` field.
      allow create, delete: if false;
    }
  }
}

Esegui di nuovo i test e conferma che un altro test sia stato superato.

8. Aggiornamento dei post pubblicati: funzioni personalizzate e variabili locali

Le condizioni per aggiornare un post pubblicato sono:

  • può essere eseguita solo dall'autore o dal moderatore e
  • deve contenere tutti i campi obbligatori.

Dal momento che hai già scritto delle condizioni per diventare autore o moderatore, puoi copiarle e incollarle, ma nel tempo potrebbe diventare difficile da leggere e gestire. Creerai invece una funzione personalizzata che incapsula la logica per essere un autore o un moderatore. In seguito, la chiamerai da più condizioni.

crea una funzione personalizzata

Sopra l'istruzione di corrispondenza per le bozze, crea una nuova funzione denominata isAuthorOrModerator che utilizza come argomenti un documento post (funziona per bozze o post pubblicati) e l'oggetto auth dell'utente:

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

    // Returns true if user is post author or a moderator
    function isAuthorOrModerator(post, auth) {

    }

    match /drafts/{postID} {
      allow create: ...
      allow update: ...
      ...
    }

    match /published/{postID} {
      allow read: ...
      allow create, delete: ...
    }
  }
}

Utilizzare le variabili locali

All'interno della funzione, utilizza la parola chiave let per impostare le variabili isAuthor e isModerator. Tutte le funzioni devono terminare con un'istruzione di ritorno e la nostra restituirà un valore booleano che indica se una delle due variabili è vera:

function isAuthorOrModerator(post, auth) {
  let isAuthor = auth.uid == post.authorUID;
  let isModerator = auth.token.isModerator == true;
  return isAuthor || isModerator;
}

Richiama la funzione

Ora aggiorna la regola per le bozze in modo che chiami la funzione, facendo attenzione a passare resource.data come primo argomento:

  // Draft blog posts
  match /drafts/{draftID} {
    ...
    // Can be deleted by author or moderator
    allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
  }

Ora puoi scrivere una condizione per l'aggiornamento dei post pubblicati che utilizza la nuova funzione:

allow update: if isAuthorOrModerator(resource.data, request.auth);

Aggiungi convalide

Alcuni campi di un post pubblicato non devono essere modificati, in particolare i campi url, authorUID e publishedAt sono immutabili. Gli altri due campi, title e content, e visible devono essere ancora presenti dopo un aggiornamento. Aggiungi le condizioni per applicare questi requisiti agli aggiornamenti dei post pubblicati:

// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
  "authorUID",
  "publishedAt",
  "url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
  "content",
  "title",
  "visible"
])

Creare una funzione personalizzata autonomamente

Infine, aggiungi una condizione che il titolo non superi i 50 caratteri. Poiché si tratta di una logica riutilizzata, potresti farlo creando una nuova funzione, titleIsUnder50Chars. Con la nuova funzione, la condizione per aggiornare un post pubblicato diventa:

allow update: if
  isAuthorOrModerator(resource.data, request.auth) &&
  // Immutable fields are unchanged
  request.resource.data.diff(resource.data).unchangedKeys().hasAll([
    "authorUID",
    "publishedAt",
    "url"
  ]) &&
  // Required fields are present
  request.resource.data.keys().hasAll([
    "content",
    "title",
    "visible"
  ]) &&
  titleIsUnder50Chars(request.resource.data);

Il file delle regole completo è:

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

    // Returns true if user is post author or a moderator
    function isAuthorOrModerator(post, auth) {
      let isAuthor = auth.uid == post.authorUID;
      let isModerator = auth.token.isModerator == true;
      return isAuthor || isModerator;
    }

    function titleIsUnder50Chars(post) {
      return post.title.size() < 50;
    }

    // Draft blog posts
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        titleIsUnder50Chars(request.resource.data);

      allow update: if
        // User is the author, and
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
          ]) &&
        titleIsUnder50Chars(request.resource.data);

      // Can be read or deleted by author or moderator
      allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
    }

    // Published blog posts are denormalized from drafts
    match /published/{postID} {
      // `authorUID`: string, required
      // `content`: string, required
      // `publishedAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, required
      // `visible`: boolean, required

      // Can be read by everyone
      allow read: if true;

      // Published posts are created only via functions, never by users
      // No hard deletes; soft deletes update `visible` field.
      allow create, delete: if false;

      allow update: if
        isAuthorOrModerator(resource.data, request.auth) &&
        // Immutable fields are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "publishedAt",
          "url"
        ]) &&
        // Required fields are present
        request.resource.data.keys().hasAll([
          "content",
          "title",
          "visible"
        ]) &&
        titleIsUnder50Chars(request.resource.data);
    }
  }
}

Esegui di nuovo i test. A questo punto, dovresti avere 5 test superati e 4 test non riusciti.

9. Commenti: autorizzazioni per fornitori di accesso e raccolte secondarie

I post pubblicati consentono i commenti, che vengono archiviati in una sottoraccolta del post pubblicato (/published/{postID}/comments/{commentID}). Per impostazione predefinita, le regole di una raccolta non si applicano alle sottoraccolte. Non vuoi che ai commenti vengano applicate le stesse regole applicate al documento principale del post pubblicato, ma ne creerai di diverse.

Per scrivere regole per accedere ai commenti, inizia con l'istruzione di corrispondenza:

match /published/{postID}/comments/{commentID} {
  // `authorUID`: string, required
  // `comment`: string, < 500 characters, required
  // `createdAt`: timestamp, required
  // `editedAt`: timestamp, optional

Lettura dei commenti: non è possibile essere anonimi

Per questa app, solo gli utenti che hanno creato un account permanente e non un account anonimo possono leggere i commenti. Per applicare questa regola, cerca l'attributo sign_in_provider che si trova in ogni oggetto auth.token:

allow read: if request.auth.token.firebase.sign_in_provider != "anonymous";

Esegui di nuovo i test e verifica che un altro test sia superato.

Creazione di commenti: controllo di un elenco di esclusione

Esistono tre condizioni per creare un commento:

  • un utente deve avere un indirizzo e-mail verificato
  • il commento deve contenere meno di 500 caratteri e
  • non possono essere inclusi in un elenco di utenti vietati, memorizzato in Firestore nella raccolta bannedUsers. Prendendo queste condizioni una alla volta:
request.auth.token.email_verified == true
request.resource.data.comment.size() < 500
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));

L'ultima regola per la creazione di commenti è:

allow create: if
  // User has verified email
  (request.auth.token.email_verified == true) &&
  // UID is not on bannedUsers list
  !(exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));

L'intero file delle regole è ora:

For bottom of step 9
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Returns true if user is post author or a moderator
    function isAuthorOrModerator(post, auth) {
      let isAuthor = auth.uid == post.authorUID;
      let isModerator = auth.token.isModerator == true;
      return isAuthor || isModerator;
    }

    function titleIsUnder50Chars(post) {
      return post.title.size() < 50;
    }

    // Draft blog posts
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User is author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and createdAt fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        titleIsUnder50Chars(request.resource.data);

      allow update: if
        // User is author
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
          ]) &&
        titleIsUnder50Chars(request.resource.data);

      // Can be read or deleted by author or moderator
      allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
    }

    // Published blog posts are denormalized from drafts
    match /published/{postID} {
      // `authorUID`: string, required
      // `content`: string, required
      // `publishedAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, required
      // `visible`: boolean, required

      // Can be read by everyone
      allow read: if true;

      // Published posts are created only via functions, never by users
      // No hard deletes; soft deletes update `visible` field.
      allow create, delete: if false;

      allow update: if
        isAuthorOrModerator(resource.data, request.auth) &&
        // Immutable fields are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "publishedAt",
          "url"
        ]) &&
        // Required fields are present
        request.resource.data.keys().hasAll([
          "content",
          "title",
          "visible"
        ]) &&
        titleIsUnder50Chars(request.resource.data);
    }

    match /published/{postID}/comments/{commentID} {
      // `authorUID`: string, required
      // `createdAt`: timestamp, required
      // `editedAt`: timestamp, optional
      // `comment`: string, < 500 characters, required

      // Must have permanent account to read comments
      allow read: if !(request.auth.token.firebase.sign_in_provider == "anonymous");

      allow create: if
        // User has verified email
        request.auth.token.email_verified == true &&
        // Comment is under 500 characters
        request.resource.data.comment.size() < 500 &&
        // UID is not on the block list
        !exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
    }
  }
}

Esegui di nuovo i test e assicurati che un altro test abbia esito positivo.

10. Aggiornamento dei commenti: regole basate sull'ora

La logica di business per i commenti è che possono essere modificati dall'autore per un'ora dopo la creazione. Per implementarlo, utilizza il timestamp createdAt.

Innanzitutto, per stabilire che l'utente è l'autore:

request.auth.uid == resource.data.authorUID

Ora che il commento è stato creato nell'ultima ora:

(request.time - resource.data.createdAt) < duration.value(1, 'h');

Combinando questi elementi con l'operatore logico AND, la regola per l'aggiornamento dei commenti diventa:

allow update: if
  // is author
  request.auth.uid == resource.data.authorUID &&
  // within an hour of comment creation
  (request.time - resource.data.createdAt) < duration.value(1, 'h');

Esegui di nuovo i test e assicurati che un altro test sia superato.

11. Eliminazione dei commenti: controllo della proprietà principale

I commenti possono essere eliminati dall'autore, da un moderatore o dall'autore del post del blog.

Innanzitutto, poiché la funzione di supporto che hai aggiunto in precedenza verifica la presenza di un campo authorUID che potrebbe esistere in un post o in un commento, puoi riutilizzare la funzione di supporto per verificare se l'utente è l'autore o il moderatore:

isAuthorOrModerator(resource.data, request.auth)

Per verificare se l'utente è l'autore del post del blog, utilizza un get per cercare il post in Firestore:

request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID

Poiché una di queste condizioni è sufficiente, utilizza un operatore logico OR tra le condizioni:

allow delete: if
  // is comment author or moderator
  isAuthorOrModerator(resource.data, request.auth) ||
  // is blog post author
  request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID;

Esegui di nuovo i test e assicurati che un altro test abbia esito positivo.

L'intero file delle regole è:

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

    // Returns true if user is post author or a moderator
    function isAuthorOrModerator(post, auth) {
      let isAuthor = auth.uid == post.authorUID;
      let isModerator = auth.token.isModerator == true;
      return isAuthor || isModerator;
    }

    function titleIsUnder50Chars(post) {
      return post.title.size() < 50;
    }

    // Draft blog posts
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User is author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and createdAt fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        titleIsUnder50Chars(request.resource.data);

      allow update: if
        // User is author
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
          ]) &&
        titleIsUnder50Chars(request.resource.data);

      // Can be read or deleted by author or moderator
      allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
    }

    // Published blog posts are denormalized from drafts
    match /published/{postID} {
      // `authorUID`: string, required
      // `content`: string, required
      // `publishedAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, required
      // `visible`: boolean, required

      // Can be read by everyone
      allow read: if true;

      // Published posts are created only via functions, never by users
      // No hard deletes; soft deletes update `visible` field.
      allow create, delete: if false;

      allow update: if
        isAuthorOrModerator(resource.data, request.auth) &&
        // Immutable fields are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "publishedAt",
          "url"
        ]) &&
        // Required fields are present
        request.resource.data.keys().hasAll([
          "content",
          "title",
          "visible"
        ]) &&
        titleIsUnder50Chars(request.resource.data);
    }

    match /published/{postID}/comments/{commentID} {
      // `authorUID`: string, required
      // `createdAt`: timestamp, required
      // `editedAt`: timestamp, optional
      // `comment`: string, < 500 characters, required

      // Must have permanent account to read comments
      allow read: if !(request.auth.token.firebase.sign_in_provider == "anonymous");

      allow create: if
        // User has verified email
        request.auth.token.email_verified == true &&
        // Comment is under 500 characters
        request.resource.data.comment.size() < 500 &&
        // UID is not on the block list
        !exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));

      allow update: if
        // is author
        request.auth.uid == resource.data.authorUID &&
        // within an hour of comment creation
        (request.time - resource.data.createdAt) < duration.value(1, 'h');

      allow delete: if
        // is comment author or moderator
        isAuthorOrModerator(resource.data, request.auth) ||
        // is blog post author
        request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID;
    }
  }
}

12. Passaggi successivi

Complimenti! Hai scritto le regole di sicurezza che hanno consentito il superamento di tutti i test e la protezione dell'applicazione.

Ecco alcuni argomenti correlati che approfondiremo:

  • Post del blog: Come eseguire la revisione del codice delle regole di sicurezza
  • Codelab: procedura dettagliata dello sviluppo locale con gli emulatori
  • Video: come utilizzare la configurazione di CI per i test basati su emulatore con Azioni GitHub