Proteggi i tuoi dati con le regole di sicurezza di Firestore

1. Prima di iniziare

Cloud Firestore, Cloud Storage for Firebase e Realtime Database si basano sui file di configurazione che scrivi per garantire l'accesso in lettura e scrittura. Tale configurazione, denominata Regole di sicurezza, può anche fungere da schema per la tua app. È una delle parti più importanti dello sviluppo della tua applicazione. E questo codelab ti guiderà attraverso di esso.

Prerequisiti

  • Un semplice editor come Visual Studio Code, Atom o Sublime Text
  • Node.js 8.6.0 o versioni successive (per installare Node.js, usa nvm ; per controllare la tua versione, esegui node --version )
  • Java 7 o versioni successive (per installare Java utilizzare queste istruzioni ; per verificare la propria versione, eseguire 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 non consentano l'accesso previsto.

Imparerai a:

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

2. Impostare

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

Bozza di post del blog:

  • Gli utenti possono creare bozze di post del blog, che risiedono nella raccolta di 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 eliminate 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 solo essere eliminati temporaneamente, il che aggiorna un attributo visible a false.

Commenti

  • I post pubblicati consentono commenti, che sono una sottoraccolta di ogni post pubblicato.
  • Per ridurre gli abusi, gli utenti devono avere un indirizzo email verificato e non essere su un negazionista 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 minimizzano le regole di sicurezza 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 hai una connessione Internet più lenta, l'operazione potrebbe richiedere uno o due minuti:

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

Ottieni la CLI di Firebase

L'Emulator Suite che utilizzerai per eseguire i test fa parte della Firebase CLI (command-line interface) che può essere installata sulla tua macchina con il seguente comando:

$ npm install -g firebase-tools

Successivamente, conferma di disporre dell'ultima versione dell'interfaccia a riga di comando. 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 in locale. Ciò significa che è ora di avviare l'Emulatore Suite.

Avvia gli emulatori

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

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

Sulla riga di comando, avvia gli emulatori usando 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

...

In questo 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 pubblicati, questa app di blog archivia le bozze dei post del blog in una raccolta separata, /drafts . Le bozze sono accessibili solo dall'autore o da un moderatore e hanno le 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 di corrispondenza, match /{document=**} , utilizza la sintassi ** per applicare ricorsivamente a tutti i documenti nelle sottoraccolte. E poiché è 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 quali dati 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. Verificare 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 ; ciò 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 tutto quelle chiavi:

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

Il requisito finale per la creazione di 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, riesegui i test e conferma che il primo test è stato 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. Nota 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 debbano cambiare:

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

E infine, il titolo dovrebbe essere di 50 caratteri o meno:

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

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

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 è stato superato.

6. Elimina e leggi le bozze: controllo degli accessi basato sugli attributi

Proprio come gli autori possono creare e aggiornare le bozze, possono anche eliminare le bozze.

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

Inoltre, gli autori con un attributo isModerator sul loro 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 OR logico, || :

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 è 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 di post sono così diversi, questa app denormalizza i post in draft e raccolte 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 del blog, viene attivata una funzione che creerà 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;
}

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

Riesegui i test e conferma che un altro test è stato 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.

Dal momento che hai già scritto le condizioni per essere un autore o un moderatore, potresti copiare e incollare le condizioni, ma nel tempo potrebbero diventare difficili da leggere e mantenere. Invece, creerai 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 la dichiarazione di corrispondenza per le bozze, crea una nuova funzione chiamata isAuthorOrModerator che prende come argomenti un documento di post (funziona sia per le bozze che per i post pubblicati) e l'oggetto di autenticazione 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: ...
    }
  }
}

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

Chiama la funzione

Ora aggiornerai la regola per le bozze per chiamare 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 utilizzi anche la nuova funzione:

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

Aggiungi convalide

Alcuni dei 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 far rispettare 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 una condizione che il titolo sia inferiore a 50 caratteri. Poiché si tratta di una logica riutilizzata, è possibile farlo creando una nuova funzione, titleIsUnder50Chars . Con la nuova funzione, la condizione per l'aggiornamento di 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 di 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);
    }
  }
}

Ripeti 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 la dichiarazione di corrispondenza:

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 che si trova su ogni oggetto auth.token :

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

Riesegui i test e conferma che un altro test è stato superato.

Creazione di commenti: controllo di un elenco di rifiuti

Esistono tre condizioni per creare un commento:

  • un utente deve avere un'e-mail verificata
  • il commento deve essere inferiore a 500 caratteri e
  • non possono trovarsi in un elenco di utenti bannati, che è 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 charachters
        request.resource.data.comment.size() < 500 &&
        // UID is not on the block list
        !exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
    }
  }
}

Riesegui i test e assicurati che un altro test sia superato.

10. Aggiornamento commenti: regole basate sul tempo

La logica aziendale per i commenti è che possono essere modificati dall'autore del commento per un'ora dopo la creazione. Per implementarlo, usa 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');

Combinandoli 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');

Riesegui i test e assicurati che un altro test sia superato.

11. Cancellazione commenti: verifica della proprietà dei genitori

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 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, utilizzare un operatore OR logico tra di esse:

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;

Riesegui i test e assicurati che un altro test sia 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 charachters
        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 superato tutti i test e messo in sicurezza l'applicazione!

Ecco alcuni argomenti correlati su cui approfondire:

  • Post sul blog : Come rivedere il codice delle regole di sicurezza
  • Codelab : passeggiata attraverso il primo sviluppo locale con gli emulatori
  • Video : come utilizzare la configurazione della configurazione per i test basati su emulatore utilizzando GitHub Actions