1. Prima di iniziare
Cloud Firestore, Cloud Storage for Firebase e Realtime Database si basano sui file di configurazione che scrivi per concedere l'accesso in lettura e scrittura. La configurazione, chiamata Regole di sicurezza, può anche fungere da tipo di schema per l'app. È una delle parti più importanti dello sviluppo della tua applicazione. Questo codelab ti guiderà nella procedura.
Prerequisiti
- Un editor semplice come Visual Studio Code, Atom o Sublime Text
- Node.js 8.6.0 o versioni successive (per installare Node.js, utilizza nvm; per controllare la versione, esegui
node --version
) - Java 7 o versioni successive (per installare Java utilizza queste istruzioni; per controllare la versione, esegui
java -version
)
Attività previste
In questo codelab proteggerai una semplice piattaforma blog basata su Firestore. Utilizzerai l'emulatore Firestore per eseguire i test delle unità in base alle regole di sicurezza e assicurarti che le regole consentano o meno l'accesso previsto.
Al termine del corso sarai in grado di:
- Concedi autorizzazioni granulari
- Applica convalide dei dati e dei tipi
- Implementare il controllo degli accessi basato su attributi
- Concedi l'accesso in base al metodo di autenticazione
- Creare funzioni personalizzate
- Creare regole di sicurezza basate sul tempo
- Implementare un elenco valori non consentiti ed eliminazioni temporanee
- Scopri quando denormalizzare i dati per soddisfare più pattern di accesso
2. Configura
Si tratta di un'applicazione di blogging. Ecco un riepilogo generale delle funzionalità dell'applicazione:
Bozza di post del blog:
- Gli utenti possono creare bozze di post del blog, che sono disponibili nella raccolta
drafts
. - L'autore può continuare ad aggiornare una bozza finché non è pronta per essere pubblicata.
- Quando è pronto per la pubblicazione, viene attivata una funzione Firebase che crea un nuovo documento nella raccolta
published
. - Le bozze possono essere eliminate dall'autore o dai moderatori del sito
Post del blog pubblicati:
- I post pubblicati non possono essere creati dagli utenti, ma solo tramite una funzione.
- Possono essere solo eliminati temporaneamente, il che aggiorna un attributo
visible
in false.
Commenti
- I post pubblicati consentono i commenti, che sono una sottoraccolta di ogni post pubblicato.
- Per ridurre gli abusi, gli utenti devono avere un indirizzo email verificato e non devono far parte di una lista bloccata per poter lasciare un commento.
- I commenti possono essere aggiornati solo entro un'ora dalla loro pubblicazione.
- I commenti possono essere eliminati dall'autore del commento, dall'autore del post originale o dai moderatori.
Oltre alle regole di accesso, creerai regole di sicurezza che applicano convalide dei dati e dei campi obbligatori.
Tutto avverrà localmente, utilizzando Firebase Emulator Suite.
Ottieni il codice sorgente
In questo codelab, inizierai con i test per le regole di sicurezza, ma minimizzando le regole di sicurezza stesse, quindi la prima cosa da fare è clonare il codice sorgente per eseguire i test:
$ git clone https://github.com/FirebaseExtended/codelab-rules.git
Quindi vai alla directory stato-iniziale, in cui lavorerai per il resto di questo codelab:
$ cd codelab-rules/initial-state
Ora installa le dipendenze per eseguire i test. Se la connessione a internet è lenta, l'operazione potrebbe richiedere un paio di minuti:
# Move into the functions directory, install dependencies, jump out. $ cd functions && npm install && cd -
Ottieni l'interfaccia a riga di comando di Firebase
La suite di emulatori che utilizzerai per eseguire i test fa parte dell'interfaccia a riga di comando (CLI) di Firebase, che può essere installata sulla tua macchina con il seguente comando:
$ npm install -g firebase-tools
Quindi, verifica di avere la versione più recente dell'interfaccia a riga di comando. Questo codelab dovrebbe funzionare con la versione 8.4.0 o successive, ma le versioni successive includono più correzioni di bug.
$ firebase --version 9.10.2
3. Esegui i test
In questa sezione eseguirai i test in locale. Questo significa che è il momento di avviare Emulator Suite.
Avvia gli emulatori
L'applicazione con cui lavorerai ha tre collezioni Firestore principali: drafts
contiene i post del blog in corso, la collezione published
contiene i post del blog che sono stati pubblicati e comments
è una sottoraccolta dei post pubblicati. Il repo include test di unità per le regole di sicurezza che definiscono gli attributi utente e altre condizioni necessarie per consentire a un utente di creare, leggere, aggiornare ed eliminare documenti nelle raccolte drafts
, published
e comments
. Dovrai scrivere le regole di sicurezza per far superare questi test.
Per iniziare, il database è bloccato: le operazioni di lettura e scrittura sul database sono rifiutate universalmente e tutti i test hanno esito negativo. Man mano che scrivi le regole di sicurezza, i test vengono superati. Per visualizzare i test, apri functions/test.js
nell'editor.
Nella riga di comando, avvia gli emulatori utilizzando emulators:exec
ed esegui i test:
$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test"
Scorri fino all'inizio dell'output:
$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test" i emulators: Starting emulators: functions, firestore, hosting ⚠ functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: auth, database, pubsub ⚠ functions: Unable to fetch project Admin SDK configuration, Admin SDK behavior in Cloud Functions emulator may be incorrect. i firestore: Importing data from /Users/user/src/firebase/rules-codelab/initial-state/.seed/firestore_export/firestore_export.overall_export_metadata i firestore: Firestore Emulator logging to firestore-debug.log ⚠ hosting: Authentication error when trying to fetch your current web app configuration, have you run firebase login? ⚠ hosting: Could not fetch web app configuration and there is no cached configuration on this machine. Check your internet connection and make sure you are authenticated. To continue, you must call firebase.initializeApp({...}) in your code before using Firebase. i hosting: Serving hosting files from: public ✔ hosting: Local server: http://localhost:5000 i functions: Watching "/Users/user/src/firebase/rules-codelab/initial-state/functions" for Cloud Functions... ✔ functions[publishPost]: http function initialized (http://localhost:5001/codelab/us-central1/publishPost). ✔ functions[softDelete]: http function initialized (http://localhost:5001/codelab/us-central1/softDelete). i Running script: pushd functions; npm test ~/src/firebase/rules-codelab/initial-state/functions ~/src/firebase/rules-codelab/initial-state > functions@ test /Users/user/src/firebase/rules-codelab/initial-state/functions > mocha (node:76619) ExperimentalWarning: Conditional exports is an experimental feature. This feature could change at any time Draft blog posts 1) can be created with required fields by the author 2) can be updated by author if immutable fields are unchanged 3) can be read by the author and moderator Published blog posts 4) can be read by everyone; created or deleted by no one 5) can be updated by author or moderator Comments on published blog posts 6) can be read by anyone with a permanent account 7) can be created if email is verfied and not blocked 8) can be updated by author for 1 hour after creation 9) can be deleted by an author or moderator 0 passing (848ms) 9 failing ...
Al momento ci sono 9 errori. Man mano che crei il file delle regole, puoi misurare i progressi osservando il superamento di un numero maggiore di test.
4. Creare bozze dei post del blog.
Poiché l'accesso alle bozze dei post del blog è molto diverso dall'accesso ai post del blog pubblicati, questa app di blogging archivia le bozze dei post del blog in una raccolta separata, /drafts
. Le bozze sono accessibili solo all'autore o a un moderatore e prevedono convalide per i campi obbligatori e immutabili.
Se apri il file firestore.rules
, troverai un file di regole predefinite:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
}
}
L'istruzione di corrispondenza match /{document=**}
utilizza la sintassi **
per applicarsi in modo ricorsivo a tutti i documenti delle sottoraccolte. Poiché si trova al livello più alto, al momento la stessa regola generale si applica a tutte le richieste, indipendentemente da chi le effettua o dai dati che stanno cercando di leggere o scrivere.
Inizia rimuovendo l'istruzione di corrispondenza più interna e sostituendola con match /drafts/{draftID}
. I commenti sulla struttura dei documenti possono essere utili nelle regole e saranno inclusi in questo codelab; sono sempre facoltativi.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
}
}
}
La prima regola che scriverai per le bozze controllerà chi può creare i documenti. In questa applicazione, le bozze possono essere create solo dalla persona indicata come autore. Verifica che l'UID della persona che effettua la richiesta sia lo stesso UID indicato nel documento.
La prima condizione per la creazione sarà:
request.resource.data.authorUID == request.auth.uid
Inoltre, i documenti possono essere creati solo se includono i tre campi obbligatori authorUID
, createdAt
e title
. L'utente non fornisce il campo createdAt
; questo impone all'app di aggiungerlo prima di provare a creare un documento. Poiché devi solo verificare che gli attributi vengano creati, puoi controllare che request.resource
abbia tutte le chiavi:
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
])
L'ultimo requisito per creare un post del blog è che il titolo non può contenere più di 50 caratteri:
request.resource.data.title.size() < 50
Poiché tutte queste condizioni devono essere vere, concatenale con l'operatore logico AND, &&
. La prima regola diventa:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
}
}
}
Nel terminale, esegui di nuovo i test e verifica che il primo test sia superato.
5. Aggiornare le bozze dei post del blog.
Successivamente, man mano che gli autori perfezionano le bozze dei loro post del blog, potranno modificare le bozze dei documenti. Crea una regola per le condizioni in cui un post può essere aggiornato. Innanzitutto, solo l'autore può aggiornare le bozze. Tieni presente che qui controlli l'UID già scritto,resource.data.authorUID
:
resource.data.authorUID == request.auth.uid
Il secondo requisito per un aggiornamento è che due attributi, authorUID
e createdAt
, non devono cambiare:
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]);
Infine, il titolo deve contenere al massimo 50 caratteri:
request.resource.data.title.size() < 50;
Poiché tutte queste condizioni devono essere soddisfatte, concatenale con &&
:
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
Le regole complete diventano:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
}
}
}
Esegui di nuovo i test e verifica che un altro test sia superato.
6. Eliminare e leggere le bozze: controllo dell'accesso basato su attributi
Così come gli autori possono creare e aggiornare le bozze, possono anche eliminarle.
resource.data.authorUID == request.auth.uid
Inoltre, gli autori con un attributo isModerator
nel token di autenticazione sono autorizzati a eliminare le bozze:
request.auth.token.isModerator == true
Poiché una di queste condizioni è sufficiente per un'eliminazione, concatenale con un operatore logico OR ||
:
allow delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true
Le stesse condizioni si applicano alle letture, per cui è possibile aggiungere l'autorizzazione alla regola:
allow read, delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true
Ora le regole complete sono:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow read, delete: if
// User is draft author
resource.data.authorUID == request.auth.uid ||
// User is a moderator
request.auth.token.isModerator == true;
}
}
}
Esegui di nuovo i test e verifica che un altro test ora sia superato.
7. Legge, crea ed elimina i post pubblicati: denormalizzazione per diversi pattern di accesso
Poiché i pattern di accesso per i post pubblicati e le bozze di post sono molto diversi, questa app denormalizza i post in raccolte draft
e published
separate. Ad esempio, i post pubblicati possono essere letti da chiunque, ma non possono essere eliminati definitivamente, mentre le bozze possono essere eliminate, ma possono essere lette solo dall'autore e dai moderatori. In questa app, quando un utente vuole pubblicare una bozza di post del blog, viene attivata una funzione che crea il nuovo post pubblicato.
Successivamente, scriverai le regole per i post pubblicati. Le regole più semplici da scrivere sono che i post pubblicati possono essere letti da chiunque e non possono essere creati o eliminati da nessuno. Aggiungi queste regole:
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
}
Se le aggiungi alle regole esistenti, l'intero file delle regole diventa:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow read, delete: if
// User is draft author
resource.data.authorUID == request.auth.uid ||
// User is a moderator
request.auth.token.isModerator == true;
}
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
}
}
}
Esegui di nuovo i test e conferma che un altro test sia stato superato.
8. Aggiornamento dei post pubblicati: funzioni personalizzate e variabili locali
Le condizioni per aggiornare un post pubblicato sono:
- può essere eseguita solo dall'autore o dal moderatore e
- deve contenere tutti i campi obbligatori.
Dal momento che hai già scritto delle condizioni per diventare autore o moderatore, puoi copiarle e incollarle, ma nel tempo potrebbe diventare difficile da leggere e gestire. Creerai invece una funzione personalizzata che incapsula la logica per essere un autore o un moderatore. In seguito, la chiamerai da più condizioni.
crea una funzione personalizzata
Sopra l'istruzione di corrispondenza per le bozze, crea una nuova funzione denominata isAuthorOrModerator
che utilizza come argomenti un documento post (funziona per bozze o post pubblicati) e l'oggetto auth dell'utente:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Returns true if user is post author or a moderator
function isAuthorOrModerator(post, auth) {
}
match /drafts/{postID} {
allow create: ...
allow update: ...
...
}
match /published/{postID} {
allow read: ...
allow create, delete: ...
}
}
}
Utilizzare le variabili locali
All'interno della funzione, utilizza la parola chiave let
per impostare le variabili isAuthor
e isModerator
. Tutte le funzioni devono terminare con un'istruzione di ritorno e la nostra restituirà un valore booleano che indica se una delle due variabili è vera:
function isAuthorOrModerator(post, auth) {
let isAuthor = auth.uid == post.authorUID;
let isModerator = auth.token.isModerator == true;
return isAuthor || isModerator;
}
Richiama la funzione
Ora aggiorna la regola per le bozze in modo che chiami la funzione, facendo attenzione a passare resource.data
come primo argomento:
// Draft blog posts
match /drafts/{draftID} {
...
// Can be deleted by author or moderator
allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
}
Ora puoi scrivere una condizione per l'aggiornamento dei post pubblicati che utilizza la nuova funzione:
allow update: if isAuthorOrModerator(resource.data, request.auth);
Aggiungi convalide
Alcuni campi di un post pubblicato non devono essere modificati, in particolare i campi url
, authorUID
e publishedAt
sono immutabili. Gli altri due campi, title
e content
, e visible
devono essere ancora presenti dopo un aggiornamento. Aggiungi le condizioni per applicare questi requisiti agli aggiornamenti dei post pubblicati:
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
])
Creare una funzione personalizzata autonomamente
Infine, aggiungi una condizione che il titolo non superi i 50 caratteri. Poiché si tratta di una logica riutilizzata, potresti farlo creando una nuova funzione, titleIsUnder50Chars
. Con la nuova funzione, la condizione per aggiornare un post pubblicato diventa:
allow update: if
isAuthorOrModerator(resource.data, request.auth) &&
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
]) &&
titleIsUnder50Chars(request.resource.data);
Il file delle regole completo è:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Returns true if user is post author or a moderator
function isAuthorOrModerator(post, auth) {
let isAuthor = auth.uid == post.authorUID;
let isModerator = auth.token.isModerator == true;
return isAuthor || isModerator;
}
function titleIsUnder50Chars(post) {
return post.title.size() < 50;
}
// Draft blog posts
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
titleIsUnder50Chars(request.resource.data);
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
titleIsUnder50Chars(request.resource.data);
// Can be read or deleted by author or moderator
allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
}
// Published blog posts are denormalized from drafts
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
allow update: if
isAuthorOrModerator(resource.data, request.auth) &&
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
]) &&
titleIsUnder50Chars(request.resource.data);
}
}
}
Esegui di nuovo i test. A questo punto, dovresti avere 5 test superati e 4 test non riusciti.
9. Commenti: autorizzazioni per fornitori di accesso e raccolte secondarie
I post pubblicati consentono i commenti, che vengono archiviati in una sottoraccolta del post pubblicato (/published/{postID}/comments/{commentID}
). Per impostazione predefinita, le regole di una raccolta non si applicano alle sottoraccolte. Non vuoi che ai commenti vengano applicate le stesse regole applicate al documento principale del post pubblicato, ma ne creerai di diverse.
Per scrivere regole per accedere ai commenti, inizia con l'istruzione di corrispondenza:
match /published/{postID}/comments/{commentID} {
// `authorUID`: string, required
// `comment`: string, < 500 characters, required
// `createdAt`: timestamp, required
// `editedAt`: timestamp, optional
Lettura dei commenti: non è possibile essere anonimi
Per questa app, solo gli utenti che hanno creato un account permanente e non un account anonimo possono leggere i commenti. Per applicare questa regola, cerca l'attributo sign_in_provider
che si trova in ogni oggetto auth.token
:
allow read: if request.auth.token.firebase.sign_in_provider != "anonymous";
Esegui di nuovo i test e verifica che un altro test sia superato.
Creazione di commenti: controllo di un elenco di esclusione
Esistono tre condizioni per creare un commento:
- un utente deve avere un indirizzo e-mail verificato
- il commento deve contenere meno di 500 caratteri e
- non possono essere inclusi in un elenco di utenti vietati, memorizzato in Firestore nella raccolta
bannedUsers
. Prendendo queste condizioni una alla volta:
request.auth.token.email_verified == true
request.resource.data.comment.size() < 500
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
L'ultima regola per la creazione di commenti è:
allow create: if
// User has verified email
(request.auth.token.email_verified == true) &&
// UID is not on bannedUsers list
!(exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
L'intero file delle regole è ora:
For bottom of step 9
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Returns true if user is post author or a moderator
function isAuthorOrModerator(post, auth) {
let isAuthor = auth.uid == post.authorUID;
let isModerator = auth.token.isModerator == true;
return isAuthor || isModerator;
}
function titleIsUnder50Chars(post) {
return post.title.size() < 50;
}
// Draft blog posts
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User is author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and createdAt fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
titleIsUnder50Chars(request.resource.data);
allow update: if
// User is author
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
titleIsUnder50Chars(request.resource.data);
// Can be read or deleted by author or moderator
allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
}
// Published blog posts are denormalized from drafts
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
allow update: if
isAuthorOrModerator(resource.data, request.auth) &&
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
]) &&
titleIsUnder50Chars(request.resource.data);
}
match /published/{postID}/comments/{commentID} {
// `authorUID`: string, required
// `createdAt`: timestamp, required
// `editedAt`: timestamp, optional
// `comment`: string, < 500 characters, required
// Must have permanent account to read comments
allow read: if !(request.auth.token.firebase.sign_in_provider == "anonymous");
allow create: if
// User has verified email
request.auth.token.email_verified == true &&
// Comment is under 500 characters
request.resource.data.comment.size() < 500 &&
// UID is not on the block list
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
}
}
}
Esegui di nuovo i test e assicurati che un altro test abbia esito positivo.
10. Aggiornamento dei commenti: regole basate sull'ora
La logica di business per i commenti è che possono essere modificati dall'autore per un'ora dopo la creazione. Per implementarlo, utilizza il timestamp createdAt
.
Innanzitutto, per stabilire che l'utente è l'autore:
request.auth.uid == resource.data.authorUID
Ora che il commento è stato creato nell'ultima ora:
(request.time - resource.data.createdAt) < duration.value(1, 'h');
Combinando questi elementi con l'operatore logico AND, la regola per l'aggiornamento dei commenti diventa:
allow update: if
// is author
request.auth.uid == resource.data.authorUID &&
// within an hour of comment creation
(request.time - resource.data.createdAt) < duration.value(1, 'h');
Esegui di nuovo i test e assicurati che un altro test sia superato.
11. Eliminazione dei commenti: controllo della proprietà principale
I commenti possono essere eliminati dall'autore, da un moderatore o dall'autore del post del blog.
Innanzitutto, poiché la funzione di supporto che hai aggiunto in precedenza verifica la presenza di un campo authorUID
che potrebbe esistere in un post o in un commento, puoi riutilizzare la funzione di supporto per verificare se l'utente è l'autore o il moderatore:
isAuthorOrModerator(resource.data, request.auth)
Per verificare se l'utente è l'autore del post del blog, utilizza un get
per cercare il post in Firestore:
request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID
Poiché una di queste condizioni è sufficiente, utilizza un operatore logico OR tra le condizioni:
allow delete: if
// is comment author or moderator
isAuthorOrModerator(resource.data, request.auth) ||
// is blog post author
request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID;
Esegui di nuovo i test e assicurati che un altro test abbia esito positivo.
L'intero file delle regole è:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Returns true if user is post author or a moderator
function isAuthorOrModerator(post, auth) {
let isAuthor = auth.uid == post.authorUID;
let isModerator = auth.token.isModerator == true;
return isAuthor || isModerator;
}
function titleIsUnder50Chars(post) {
return post.title.size() < 50;
}
// Draft blog posts
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User is author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and createdAt fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
titleIsUnder50Chars(request.resource.data);
allow update: if
// User is author
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
titleIsUnder50Chars(request.resource.data);
// Can be read or deleted by author or moderator
allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
}
// Published blog posts are denormalized from drafts
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
allow update: if
isAuthorOrModerator(resource.data, request.auth) &&
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
]) &&
titleIsUnder50Chars(request.resource.data);
}
match /published/{postID}/comments/{commentID} {
// `authorUID`: string, required
// `createdAt`: timestamp, required
// `editedAt`: timestamp, optional
// `comment`: string, < 500 characters, required
// Must have permanent account to read comments
allow read: if !(request.auth.token.firebase.sign_in_provider == "anonymous");
allow create: if
// User has verified email
request.auth.token.email_verified == true &&
// Comment is under 500 characters
request.resource.data.comment.size() < 500 &&
// UID is not on the block list
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
allow update: if
// is author
request.auth.uid == resource.data.authorUID &&
// within an hour of comment creation
(request.time - resource.data.createdAt) < duration.value(1, 'h');
allow delete: if
// is comment author or moderator
isAuthorOrModerator(resource.data, request.auth) ||
// is blog post author
request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID;
}
}
}
12. Passaggi successivi
Complimenti! Hai scritto le regole di sicurezza che hanno consentito il superamento di tutti i test e la protezione dell'applicazione.
Ecco alcuni argomenti correlati che approfondiremo:
- Post del blog: Come eseguire la revisione del codice delle regole di sicurezza
- Codelab: procedura dettagliata dello sviluppo locale con gli emulatori
- Video: come utilizzare la configurazione di CI per i test basati su emulatore con Azioni GitHub