Proteggi i tuoi dati Firestore con le regole di sicurezza 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. Tale configurazione, denominata Regole di sicurezza, può anche fungere da sorta di schema per la tua app. È una delle parti più importanti dello sviluppo della tua applicazione. E questo codelab ti guiderà attraverso tutto ciò.

Prerequisiti

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

Cosa farai

In questo codelab proteggerai una semplice piattaforma blog basata su Firestore. Utilizzerai l'emulatore Firestore per eseguire unit test rispetto alle regole di sicurezza e assicurarti che le regole consentano e impediscano l'accesso previsto.

Imparerai come:

  • Concedere autorizzazioni granulari
  • Applicare convalide di dati e tipi
  • Implementare il controllo degli accessi basato sugli attributi
  • Concedere l'accesso in base al metodo di autenticazione
  • Crea funzioni personalizzate
  • Crea regole di sicurezza basate sul tempo
  • Implementare un elenco di elementi rifiutati ed eliminazioni temporanee
  • Comprendere quando denormalizzare i dati per soddisfare più modelli di accesso

2. Configurazione

Questa è un'applicazione di blogging. Ecco un riepilogo di alto livello delle funzionalità dell'applicazione:

Bozze di post del blog:

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

Post del blog pubblicati:

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

Commenti

  • I post pubblicati consentono i commenti, che sono una sottoraccolta su ciascun post pubblicato.
  • Per ridurre gli abusi, gli utenti devono avere un indirizzo email verificato e non essere negati per poter lasciare un commento.
  • I commenti possono essere aggiornati solo entro un'ora dalla 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 i campi obbligatori e le convalide dei dati.

Tutto avverrà localmente, utilizzando Firebase Emulator Suite.

Ottieni il codice sorgente

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

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

Quindi spostati nella directory dello stato iniziale, dove lavorerai per il resto di questo codelab:

$ cd codelab-rules/initial-state

Ora installa le dipendenze in modo da poter eseguire i test. Se la tua connessione Internet è più lenta, l'operazione potrebbe richiedere un minuto o due:

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

Ottieni la CLI Firebase

L'Emulator Suite che utilizzerai per eseguire i test fa parte della CLI Firebase (interfaccia a riga di comando) che può essere installata sul tuo computer con il seguente comando:

$ npm install -g firebase-tools

Successivamente, conferma di avere la versione più recente della CLI. Questo codelab dovrebbe funzionare con la versione 8.4.0 o successiva, ma le versioni successive includono più correzioni di bug.

$ firebase --version
9.10.2

3. Eseguire i test

In questa sezione eseguirai i test localmente. Ciò significa che è ora di avviare Emulator Suite.

Avvia gli emulatori

L'applicazione con cui lavorerai dispone di tre raccolte Firestore principali: drafts contengono i post del blog in corso, la raccolta published contiene i post del blog che sono stati pubblicati e comments sono una sottoraccolta dei post pubblicati. Il repository viene fornito con test unitari per le regole di sicurezza che definiscono gli attributi utente e altre condizioni richieste affinché un utente possa creare, leggere, aggiornare ed eliminare documenti nelle drafts , nelle raccolte published e comments . Scriverai le regole di sicurezza per far passare questi test.

Per iniziare, il tuo database è bloccato: le letture e le scritture sul database sono universalmente negate e tutti i test falliscono. Man mano che scrivi le regole di sicurezza, i test verranno superati. Per vedere i test, apri functions/test.js nel tuo editor.

Sulla 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 fallimenti. Mentre crei il file delle regole, puoi misurare i progressi osservando il superamento di più test.

4. Crea bozze di post sul blog.

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

Aprendo il file firestore.rules , troverai un file di regole predefinito:

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

L'istruzione match, match /{document=**} , utilizza la sintassi ** per applicarsi in modo ricorsivo a tutti i documenti nelle sottoraccolte. E poiché si trova al livello più alto, in questo momento la stessa regola generale si applica a tutte le richieste, indipendentemente da chi sta effettuando la richiesta o da quali dati stanno tentando di leggere o scrivere.

Inizia rimuovendo l'istruzione match 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 riportato nel documento.

La prima condizione per la creazione sarà:

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

Successivamente, 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 che l'app lo aggiunga prima di provare a creare un documento.) Poiché devi solo verificare che gli attributi vengano creati, puoi verificare che request.resource abbia tutti quelle chiavi:

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

Il requisito finale per creare un post sul blog è che il titolo non possa contenere più di 50 caratteri:

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

Poiché tutte queste condizioni devono essere vere, concatenarle insieme all'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 nuovamente i test e conferma che il primo test viene superato.

5. Aggiorna le bozze dei post del blog.

Successivamente, man mano che gli autori perfezionano la bozza dei post del blog, modificheranno la bozza dei documenti. Crea una regola per le condizioni in cui un post può essere aggiornato. Innanzitutto, solo l'autore può aggiornare le proprie 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 dovrebbero cambiare:

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

Infine, il titolo dovrebbe contenere al massimo 50 caratteri:

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

Poiché tutte queste condizioni devono essere soddisfatte, concatenatele insieme 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 nuovamente i test e conferma che un altro test viene superato.

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

Proprio 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 sul proprio token di autenticazione possono eliminare le bozze:

request.auth.token.isModerator == true

Poiché una di queste condizioni è sufficiente per un'eliminazione, concatenarle 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, in modo che l'autorizzazione possa essere aggiunta alla regola:

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

Le regole complete ora 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 nuovamente i test e conferma che un altro test ora viene superato.

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

Poiché i modelli di accesso per i post pubblicati e le bozze dei 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 desidera pubblicare una bozza di post sul blog, viene attivata una funzione che creerà il nuovo post pubblicato.

Successivamente, scriverai le regole per i post pubblicati. La regola più semplice da scrivere è 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;
}

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

Eseguire nuovamente i test e confermare che un altro test viene superato.

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

Le condizioni per aggiornare un post pubblicato sono:

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

Dato che hai già scritto le condizioni per essere un autore o un moderatore, potresti copiare e incollare le condizioni, ma col tempo potrebbe diventare difficile da leggere e mantenere. Creerai invece una funzione personalizzata che incapsula la logica per essere un autore o un moderatore. Quindi, lo chiamerai da più condizioni.

Crea una funzione personalizzata

Sopra l'istruzione match per le bozze, crea una nuova funzione chiamata isAuthorOrModerator che accetta come argomenti un documento post (funzionerà sia per bozze che per 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: ...
    }
  }
}

Utilizza variabili locali

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

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

Chiama la funzione

Ora aggiornerai la regola affinché le bozze chiami quella 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 anche 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 condizioni per applicare questi requisiti per gli aggiornamenti ai 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"
])

Crea una funzione personalizzata da solo

Infine, aggiungi la condizione che il titolo contenga meno di 50 caratteri. Poiché si tratta di logica riutilizzata, è possibile farlo creando una nuova funzione, titleIsUnder50Chars . Con la nuova funzionalità 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);

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

Rieseguire i test. A questo punto, dovresti avere 5 test superati e 4 falliti.

9. Commenti: sottoraccolte e autorizzazioni del provider di accesso

I post pubblicati consentono commenti e i commenti 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 le stesse regole che si applicano al documento principale del post pubblicato si applichino ai commenti; ne creerai di diversi.

Per scrivere le regole per l'accesso ai commenti, inizia con l'istruzione match:

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

Commenti di lettura: Non può essere anonimo

Per questa app, solo gli utenti che hanno creato un account permanente, non un account anonimo, possono leggere i commenti. Per applicare questa regola, cerca l'attributo sign_in_provider presente su ciascun oggetto auth.token :

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

Esegui nuovamente i test e conferma che un altro test viene superato.

Creazione di commenti: controllo di un elenco rifiutati

Ci sono tre condizioni per creare un commento:

  • un utente deve avere un'e-mail verificata
  • il commento deve contenere meno di 500 caratteri e
  • non possono essere presenti in un elenco di utenti esclusi, archiviato 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));

La regola finale 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 nuovamente i test e assicurati che un altro test venga superato.

10. Commenti di aggiornamento: regole basate sul tempo

La logica aziendale per i commenti è che possono essere modificati dall'autore del commento entro un'ora dopo la creazione. Per implementare ciò, utilizzare il timestamp createdAt .

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

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

Successivamente, che il commento è stato creato nell'ultima ora:

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

Combinando questi con l'operatore logico AND la regola per aggiornare i 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 nuovamente i test e assicurati che un altro test venga superato.

11. Eliminazione dei commenti: verifica della proprietà principale

I commenti possono essere eliminati dall'autore del commento, 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 su un post o su 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 get per cercare il post in Firestore:

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

Poiché una qualsiasi di queste condizioni è sufficiente, utilizzare un operatore logico OR tra di loro:

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 nuovamente i test e assicurati che un altro test venga superato.

E 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. Passi successivi

Congratulazioni! Hai scritto le Regole di Sicurezza che hanno fatto superare tutti i test e messo in sicurezza l'applicazione!

Ecco alcuni argomenti correlati in cui approfondire successivamente:

  • Post del blog : Come rivedere il codice delle regole di sicurezza
  • Codelab : passeggiata nello sviluppo locale con gli emulatori
  • Video : come utilizzare la configurazione dell'elemento CI per i test basati sull'emulatore utilizzando GitHub Actions