1. Prima di iniziare
Cloud Firestore, Cloud Storage for Firebase e Realtime Database si basano su file di configurazione che scrivi per concedere l'accesso in lettura e scrittura. Questa configurazione, chiamata regole di sicurezza, può fungere anche da schema per la tua app. È una delle parti più importanti dello sviluppo dell'applicazione. Questo codelab ti guiderà passo passo.
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 tua versione, esegui
node --version
) - Java 7 o versioni successive (per installare Java, segui queste istruzioni; per controllare la versione, esegui
java -version
)
Attività previste
In questo codelab, proteggerai una semplice piattaforma di blog basata su Firestore. Utilizzerai l'emulatore Firestore per eseguire test delle unità sulle regole di sicurezza e assicurarti che le regole consentano e neghino l'accesso che ti aspetti.
Al termine del corso sarai in grado di:
- Concedere autorizzazioni granulari
- Applica le convalide di dati e tipi
- Implementa il controllo dell'accesso basato su attributi
- Concedere 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
- Capire quando denormalizzare i dati per soddisfare più modelli di accesso
2. Configura
Si tratta di un'applicazione di blogging. Ecco un riepilogo generale delle funzionalità dell'applicazione:
Bozze dei post del blog:
- Gli utenti possono creare bozze di post del blog, che si trovano nella raccolta
drafts
. - L'autore può continuare ad aggiornare una bozza finché non è pronta per la pubblicazione.
- 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 eliminati temporaneamente, il che aggiorna un attributo
visible
su false.
Commenti
- I post pubblicati consentono i commenti, che sono una sottoraccolta di ogni post pubblicato.
- Per ridurre gli abusi, gli utenti devono disporre di un indirizzo email verificato e non devono essere presenti in un elenco di negazionisti 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.
Recuperare il codice sorgente
In questo codelab, inizierai con i test per le regole di sicurezza, ma con regole di sicurezza minime, quindi la prima cosa da fare è clonare l'origine per eseguire i test:
$ git clone https://github.com/FirebaseExtended/codelab-rules.git
Poi passa alla directory initial-state, in cui lavorerai per il resto di questo codelab:
$ cd codelab-rules/initial-state
Ora installa le dipendenze per poter eseguire i test. Se la connessione a internet è lenta, l'operazione potrebbe richiedere un minuto o due:
# Move into the functions directory, install dependencies, jump out. $ cd functions && npm install && cd -
Scarica la CLI di Firebase
Emulator Suite che utilizzerai per eseguire i test fa parte della CLI (interfaccia a riga di comando) di Firebase, che può essere installata sul tuo computer con il seguente comando:
$ npm install -g firebase-tools
Poi, verifica di avere l'ultima versione della CLI. 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. Ciò significa che è il momento di avviare Emulator Suite.
Avvia gli emulatori
L'applicazione con cui lavorerai ha tre raccolte Firestore principali: drafts
contiene i post del blog in corso, la raccolta published
contiene i post del blog pubblicati e comments
è una sottoraccolta dei post pubblicati. Il repository include test unitari 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
. Scriverai le regole di sicurezza per superare questi test.
Per iniziare, il database è bloccato: le operazioni di lettura e scrittura nel database vengono rifiutate universalmente e tutti i test non superano la verifica. Mentre scrivi le regole di sicurezza, i test verranno 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 di regole, puoi misurare i progressi osservando il superamento di un numero maggiore di test.
4. Creare bozze di post del blog.
Poiché l'accesso ai post del blog in bozza è molto diverso da quello ai post del blog pubblicati, questa app di blogging memorizza i post del blog in bozza in una raccolta separata, /drafts
. Le bozze sono accessibili solo all'autore o a un moderatore e hanno 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 nelle raccolte secondarie. Poiché si trova al livello superiore, al momento la stessa regola generale si applica a tutte le richieste, indipendentemente da chi le effettua o dai dati che sta tentando 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 verranno 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
Successivamente, i documenti possono essere creati solo se includono i tre campi obbligatori: authorUID
,createdAt
e title
. L'utente non fornisce il campo createdAt
; in questo modo si 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
contenga tutte queste 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 venga superato.
5. Aggiorna le bozze dei post del blog.
Successivamente, gli autori perfezioneranno le bozze dei post del blog modificando i 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 devi controllare 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 venga superato.
6. Elimina e leggi bozze: controllo dell'accesso basato su 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
nel token di autenticazione possono eliminare le bozze:
request.auth.token.isModerator == true
Poiché una qualsiasi di queste condizioni è sufficiente per l'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, 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 di nuovo i test e verifica che un altro test venga superato.
7. Operazioni di lettura, creazione ed eliminazione per i post pubblicati: denormalizzazione per diversi pattern di accesso
Poiché i pattern di accesso per i post pubblicati e le bozze 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 aggiungiamo alle regole esistenti, l'intero file di 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 verifica che un altro test venga 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.
Poiché hai già scritto le condizioni per essere un autore o un moderatore, potresti copiarle e incollarle, ma nel tempo potrebbe diventare difficile leggerle e mantenerle. Creerai invece una funzione personalizzata che incapsula la logica per essere un autore o un moderatore. Poi, lo chiamerai da più condizioni.
Creare una funzione personalizzata
Sopra l'istruzione di corrispondenza per le bozze, crea una nuova funzione chiamata isAuthorOrModerator
che accetta come argomenti un documento del post (funzionerà 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: ...
}
}
}
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 return e la nostra restituirà un valore booleano che indica se una delle due variabili è true:
function isAuthorOrModerator(post, auth) {
let isAuthor = auth.uid == post.authorUID;
let isModerator = auth.token.isModerator == true;
return isAuthor || isModerator;
}
Chiama la funzione
Ora aggiorna la regola per le bozze in modo da chiamare questa 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 aggiornare i post pubblicati che utilizza anche la nuova funzione:
allow update: if isAuthorOrModerator(resource.data, request.auth);
Aggiungere 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 comunque essere presenti dopo un aggiornamento. Aggiungi 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 preveda che il titolo contenga meno di 50 caratteri. Poiché si tratta di una logica riutilizzata, puoi 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 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);
}
}
}
Esegui di nuovo i test. A questo punto, dovresti avere 5 test superati e 4 non superati.
9. Commenti: sottoraccolte e autorizzazioni del fornitore di accesso
I post pubblicati consentono i commenti, che vengono memorizzati 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, quindi ne creerai di diverse.
Per scrivere regole per l'accesso 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 può essere anonima
Per questa app, solo gli utenti che hanno creato un account permanente, non anonimo, possono leggere i commenti. Per applicare questa regola, cerca l'attributo sign_in_provider
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 venga superato.
Creazione di commenti: controllo di una lista di rifiuto
Esistono tre condizioni per creare un commento:
- un utente deve avere un indirizzo email verificato
- il commento deve contenere meno di 500 caratteri e
- Non possono essere presenti in un elenco di utenti esclusi, memorizzato in Firestore nella raccolta
bannedUsers
. Analizziamo 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 ne venga superato un altro.
10. Aggiornamento dei commenti: regole basate sul tempo
La logica di business per i commenti prevede che possano 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
Poi, che il commento sia stato creato nell'ultima ora:
(request.time - resource.data.createdAt) < duration.value(1, 'h');
Se combiniamo 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 ne venga superato un altro.
11. Eliminazione dei commenti: verifica della proprietà del genitore
I commenti possono essere eliminati dall'autore del commento, da un moderatore o dall'autore del post del blog.
Innanzitutto, poiché la funzione helper 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 helper 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 qualsiasi di queste condizioni è sufficiente, utilizza un operatore logico OR tra 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 di nuovo i test e assicurati che ne venga superato un altro.
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 superato tutti i test e protetto l'applicazione.
Ecco alcuni argomenti correlati da approfondire:
- Post del blog: How to code review Security Rules
- Codelab: procedura dettagliata per lo sviluppo locale con gli emulatori
- Video: come configurare l'integrazione continua per i test basati su emulatore utilizzando GitHub Actions