1. Bevor Sie beginnen
Cloud Firestore, Cloud Storage for Firebase und die Echtzeitdatenbank verlassen sich auf Konfigurationsdateien, die Sie schreiben, um Lese- und Schreibzugriff zu gewähren. Diese als Sicherheitsregeln bezeichnete Konfiguration kann auch als eine Art Schema für Ihre App fungieren. Dies ist einer der wichtigsten Teile bei der Entwicklung Ihrer Anwendung. Und dieses Codelab wird Sie dabei begleiten.
Voraussetzungen
- Ein einfacher Editor wie Visual Studio Code, Atom oder Sublime Text
- Node.js 8.6.0 oder höher (um Node.js zu installieren, verwenden Sie nvm ; um Ihre Version zu überprüfen, führen Sie
node --version
aus) - Java 7 oder höher (um Java zu installieren, verwenden Sie diese Anweisungen ; um Ihre Version zu überprüfen, führen Sie
java -version
aus)
Was du tun wirst
In diesem Codelab sichern Sie eine einfache Blog-Plattform, die auf Firestore basiert. Sie verwenden den Firestore-Emulator, um Einheitentests für die Sicherheitsregeln auszuführen und sicherzustellen, dass die Regeln den erwarteten Zugriff zulassen oder verbieten.
Sie lernen, wie Sie:
- Gewähren Sie granulare Berechtigungen
- Erzwingen Sie Daten- und Typvalidierungen
- Implementieren Sie eine attributbasierte Zugriffskontrolle
- Gewähren Sie Zugriff basierend auf der Authentifizierungsmethode
- Erstellen Sie benutzerdefinierte Funktionen
- Erstellen Sie zeitbasierte Sicherheitsregeln
- Implementieren Sie eine Ablehnungsliste und vorläufige Löschungen
- Verstehen Sie, wann Daten denormalisiert werden müssen, um mehrere Zugriffsmuster zu erfüllen
2. Einrichten
Dies ist eine Blogging-Anwendung. Hier ist eine Zusammenfassung der Anwendungsfunktionalität auf hoher Ebene:
Blogbeiträge entwerfen:
- Benutzer können Entwürfe von Blogbeiträgen erstellen, die in der
drafts
enthalten sind. - Der Autor kann einen Entwurf weiterhin aktualisieren, bis er zur Veröffentlichung bereit ist.
- Wenn es zur Veröffentlichung bereit ist, wird eine Firebase-Funktion ausgelöst, die ein neues Dokument in der
published
Sammlung erstellt. - Entwürfe können vom Autor oder von Site-Moderatoren gelöscht werden
Veröffentlichte Blogbeiträge:
- Veröffentlichte Beiträge können nicht von Benutzern erstellt werden, sondern nur über eine Funktion.
- Sie können nur vorläufig gelöscht werden, wodurch ein
visible
Attribut auf „false“ aktualisiert wird.
Kommentare
- Veröffentlichte Posts erlauben Kommentare, die eine Untersammlung zu jedem veröffentlichten Post darstellen.
- Um Missbrauch zu reduzieren, müssen Benutzer eine verifizierte E-Mail-Adresse haben und dürfen nicht auf einem Leugner sein, um einen Kommentar zu hinterlassen.
- Kommentare können nur innerhalb einer Stunde nach der Veröffentlichung aktualisiert werden.
- Kommentare können vom Kommentarautor, dem Autor des ursprünglichen Beitrags oder von Moderatoren gelöscht werden.
Zusätzlich zu den Zugriffsregeln erstellen Sie Sicherheitsregeln, die erforderliche Felder und Datenvalidierungen erzwingen.
Alles geschieht lokal mit der Firebase Emulator Suite.
Holen Sie sich den Quellcode
In diesem Codelab beginnen Sie mit Tests für die Sicherheitsregeln, aber minimale Sicherheitsregeln selbst, also müssen Sie als erstes die Quelle klonen, um die Tests auszuführen:
$ git clone https://github.com/FirebaseExtended/codelab-rules.git
Wechseln Sie dann in das Ausgangsverzeichnis, wo Sie für den Rest dieses Codelabs arbeiten werden:
$ cd codelab-rules/initial-state
Installieren Sie nun die Abhängigkeiten, damit Sie die Tests ausführen können. Wenn Sie eine langsamere Internetverbindung verwenden, kann dies ein oder zwei Minuten dauern:
# Move into the functions directory, install dependencies, jump out. $ cd functions && npm install && cd -
Holen Sie sich die Firebase-CLI
Die Emulator Suite, die Sie zum Ausführen der Tests verwenden, ist Teil der Firebase CLI (Befehlszeilenschnittstelle), die mit dem folgenden Befehl auf Ihrem Computer installiert werden kann:
$ npm install -g firebase-tools
Bestätigen Sie als Nächstes, dass Sie über die neueste Version der CLI verfügen. Dieses Codelab sollte mit Version 8.4.0 oder höher funktionieren, aber spätere Versionen enthalten mehr Fehlerbehebungen.
$ firebase --version 9.10.2
3. Führen Sie die Tests durch
In diesem Abschnitt führen Sie die Tests lokal aus. Dies bedeutet, dass es an der Zeit ist, die Emulator Suite zu starten.
Starten Sie die Emulatoren
Die Anwendung, mit der Sie arbeiten werden, verfügt über drei Firestore-Hauptsammlungen: drafts
enthalten Blogbeiträge, die in Bearbeitung sind, die published
Sammlung enthält die veröffentlichten Blogbeiträge und comments
sind eine Untersammlung veröffentlichter Beiträge. Das Repository enthält Komponententests für die Sicherheitsregeln, die die Benutzerattribute und andere Bedingungen definieren, die ein Benutzer zum Erstellen, Lesen, Aktualisieren und Löschen von Dokumenten in drafts
, published
und comments
Sammlungen benötigt. Sie schreiben die Sicherheitsregeln, damit diese Tests bestehen.
Zu Beginn ist Ihre Datenbank gesperrt: Lese- und Schreibzugriffe auf die Datenbank werden allgemein verweigert, und alle Tests schlagen fehl. Während Sie Sicherheitsregeln schreiben, werden die Tests bestanden. Um die Tests anzuzeigen, öffnen Sie functions/test.js
in Ihrem Editor.
Starten Sie in der Befehlszeile die Emulatoren mit emulators:exec
und führen Sie die Tests aus:
$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test"
Scrollen Sie zum Anfang der Ausgabe:
$ 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 ...
Im Moment sind 9 Fehler vorhanden. Während Sie die Regeldatei erstellen, können Sie den Fortschritt messen, indem Sie beobachten, wie mehr Tests bestanden werden.
4. Erstellen Sie Blogpost-Entwürfe.
Da sich der Zugriff auf Blogpostentwürfe so sehr vom Zugriff auf veröffentlichte Blogposts unterscheidet, speichert diese Blogging-App Blogpostentwürfe in einer separaten Sammlung, /drafts
. Auf Entwürfe kann nur vom Autor oder einem Moderator zugegriffen werden, und es gibt Validierungen für erforderliche und unveränderliche Felder.
Wenn Sie die Datei firestore.rules
öffnen, finden Sie eine Standardregeldatei:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
}
}
Die match-Anweisung match /{document=**}
verwendet die **
Syntax, um rekursiv auf alle Dokumente in Untersammlungen anzuwenden. Und weil es sich auf der obersten Ebene befindet, gilt im Moment dieselbe pauschale Regel für alle Anfragen, unabhängig davon, wer die Anfrage stellt oder welche Daten er zu lesen oder zu schreiben versucht.
Entfernen Sie zunächst die innerste Match-Anweisung und ersetzen Sie sie durch match /drafts/{draftID}
. (Kommentare zur Struktur von Dokumenten können in Regeln hilfreich sein und werden in dieses Codelab aufgenommen; sie sind immer optional.)
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
}
}
}
Die erste Regel, die Sie für Entwürfe schreiben, steuert, wer die Dokumente erstellen kann. In dieser Anwendung können Entwürfe nur von der als Autor aufgeführten Person erstellt werden. Überprüfen Sie, ob die UID der Person, die den Antrag stellt, dieselbe UID ist, die im Dokument aufgeführt ist.
Die erste Bedingung für die Erstellung wird sein:
request.resource.data.authorUID == request.auth.uid
Als Nächstes können Dokumente nur erstellt werden, wenn sie die drei erforderlichen Felder authorUID
, createdAt
und title
enthalten. (Der Benutzer gibt das Feld createdAt
nicht an; dies erzwingt, dass die App es hinzufügen muss, bevor sie versucht, ein Dokument zu erstellen.) Da Sie nur überprüfen müssen, ob die Attribute erstellt werden, können Sie überprüfen, ob request.resource
all hat diese Schlüssel:
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
])
Die letzte Voraussetzung für die Erstellung eines Blogbeitrags ist, dass der Titel nicht mehr als 50 Zeichen lang sein darf:
request.resource.data.title.size() < 50
Da alle diese Bedingungen wahr sein müssen, verketten Sie diese mit dem logischen UND-Operator &&
. Die erste Regel lautet:
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;
}
}
}
Führen Sie im Terminal die Tests erneut aus und bestätigen Sie, dass der erste Test bestanden wurde.
5. Blogpost-Entwürfe aktualisieren.
Wenn die Autoren als Nächstes ihre Entwürfe für Blogbeiträge verfeinern, bearbeiten sie die Entwurfsdokumente. Erstellen Sie eine Regel für die Bedingungen, wann ein Beitrag aktualisiert werden kann. Erstens kann nur der Autor seine Entwürfe aktualisieren. Beachten Sie, dass Sie hier die bereits geschriebene UID überprüfen, resource.data.authorUID
:
resource.data.authorUID == request.auth.uid
Die zweite Voraussetzung für ein Update ist, dass sich zwei Attribute, authorUID
und createdAt
nicht ändern sollten:
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]);
Und schließlich sollte der Titel maximal 50 Zeichen lang sein:
request.resource.data.title.size() < 50;
Da diese Bedingungen alle erfüllt sein müssen, verketten Sie sie mit &&
:
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;
Die vollständigen Regeln werden zu:
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;
}
}
}
Führen Sie Ihre Tests erneut aus und bestätigen Sie, dass ein weiterer Test bestanden wird.
6. Entwürfe löschen und lesen: Attribute Based Access Control
So wie Autoren Entwürfe erstellen und aktualisieren können, können sie auch Entwürfe löschen.
resource.data.authorUID == request.auth.uid
Außerdem dürfen Autoren mit einem isModerator
Attribut auf ihrem Authentifizierungstoken Entwürfe löschen:
request.auth.token.isModerator == true
Da jede dieser Bedingungen für einen Löschvorgang ausreicht, verketten Sie sie mit einem logischen ODER-Operator, ||
:
allow delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true
Für Lesevorgänge gelten die gleichen Bedingungen, damit der Regel eine Berechtigung hinzugefügt werden kann:
allow read, delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true
Die vollständigen Regeln lauten jetzt:
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;
}
}
}
Führen Sie Ihre Tests erneut aus und bestätigen Sie, dass ein weiterer Test nun bestanden wurde.
7. Liest, erstellt und löscht veröffentlichte Posts: Denormalisierung für unterschiedliche Zugriffsmuster
Da die Zugriffsmuster für veröffentlichte Posts und Postentwürfe so unterschiedlich sind, denormalisiert diese App die Posts in separate Sammlungen draft
und published
Posts. Beispielsweise können veröffentlichte Beiträge von jedem gelesen, aber nicht hart gelöscht werden, während Entwürfe gelöscht, aber nur vom Autor und Moderatoren gelesen werden können. Wenn ein Benutzer in dieser App einen Entwurf eines Blogbeitrags veröffentlichen möchte, wird eine Funktion ausgelöst, die den neuen veröffentlichten Beitrag erstellt.
Als Nächstes schreiben Sie die Regeln für veröffentlichte Posts. Die einfachsten Regeln zum Schreiben sind, dass veröffentlichte Beiträge von jedem gelesen und von niemandem erstellt oder gelöscht werden können. Fügen Sie diese Regeln hinzu:
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;
}
Wenn Sie diese zu den vorhandenen Regeln hinzufügen, wird die gesamte Regeldatei zu:
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;
}
}
}
Führen Sie die Tests erneut aus und bestätigen Sie, dass ein weiterer Test bestanden wird.
8. Aktualisieren veröffentlichter Beiträge: Benutzerdefinierte Funktionen und lokale Variablen
Die Bedingungen zum Aktualisieren eines veröffentlichten Beitrags sind:
- es kann nur vom Autor oder Moderator durchgeführt werden, und
- es muss alle erforderlichen Felder enthalten.
Da Sie bereits Bedingungen für die Tätigkeit als Autor oder Moderator geschrieben haben, könnten Sie die Bedingungen kopieren und einfügen, aber mit der Zeit könnte das Lesen und Pflegen schwierig werden. Stattdessen erstellen Sie eine benutzerdefinierte Funktion, die die Logik kapselt, um entweder Autor oder Moderator zu sein. Dann rufen Sie es aus mehreren Bedingungen auf.
Erstellen Sie eine benutzerdefinierte Funktion
Erstellen Sie über der Match-Anweisung für Entwürfe eine neue Funktion namens isAuthorOrModerator
, die als Argumente ein Beitragsdokument (dies funktioniert sowohl für Entwürfe als auch für veröffentlichte Beiträge) und das Authentifizierungsobjekt des Benutzers verwendet:
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: ...
}
}
}
Verwenden Sie lokale Variablen
Verwenden Sie innerhalb der Funktion das Schlüsselwort let
, um die Variablen isAuthor
und isModerator
festzulegen. Alle Funktionen müssen mit einer return-Anweisung enden, und unsere gibt einen booleschen Wert zurück, der angibt, ob eine der Variablen wahr ist:
function isAuthorOrModerator(post, auth) {
let isAuthor = auth.uid == post.authorUID;
let isModerator = auth.token.isModerator == true;
return isAuthor || isModerator;
}
Rufen Sie die Funktion auf
Jetzt aktualisieren Sie die Regel für Entwürfe, um diese Funktion aufzurufen, wobei Sie darauf achten, resource.data
als erstes Argument zu übergeben:
// Draft blog posts
match /drafts/{draftID} {
...
// Can be deleted by author or moderator
allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
}
Jetzt können Sie eine Bedingung für die Aktualisierung veröffentlichter Beiträge schreiben, die ebenfalls die neue Funktion verwendet:
allow update: if isAuthorOrModerator(resource.data, request.auth);
Validierungen hinzufügen
Einige der Felder eines veröffentlichten Beitrags sollten nicht geändert werden, insbesondere die Felder url
, authorUID
und publishedAt
sind unveränderlich. Die beiden anderen Felder title
und content
sowie visible
müssen auch nach einem Update noch vorhanden sein. Fügen Sie Bedingungen hinzu, um diese Anforderungen für Aktualisierungen veröffentlichter Beiträge durchzusetzen:
// 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"
])
Erstellen Sie selbst eine benutzerdefinierte Funktion
Fügen Sie schließlich eine Bedingung hinzu, dass der Titel weniger als 50 Zeichen lang ist. Da es sich um wiederverwendete Logik handelt, könnten Sie dies tun, indem Sie eine neue Funktion namens titleIsUnder50Chars
erstellen. Mit der neuen Funktion wird die Bedingung für die Aktualisierung eines veröffentlichten Beitrags zu:
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);
Und die vollständige Regeldatei ist:
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);
}
}
}
Führen Sie die Tests erneut aus. An diesem Punkt sollten Sie 5 bestandene Tests und 4 nicht bestandene Tests haben.
9. Kommentare: Untersammlungen und Anmeldeanbieterberechtigungen
Die veröffentlichten Posts erlauben Kommentare, und die Kommentare werden in einer Untersammlung des veröffentlichten Posts gespeichert ( /published/{postID}/comments/{commentID}
). Standardmäßig gelten die Regeln einer Sammlung nicht für Untersammlungen. Sie möchten nicht, dass die gleichen Regeln, die für das übergeordnete Dokument des veröffentlichten Beitrags gelten, auch für die Kommentare gelten; Sie werden verschiedene herstellen.
Um Regeln für den Zugriff auf die Kommentare zu schreiben, beginnen Sie mit der match-Anweisung:
match /published/{postID}/comments/{commentID} {
// `authorUID`: string, required
// `comment`: string, < 500 characters, required
// `createdAt`: timestamp, required
// `editedAt`: timestamp, optional
Lesen von Kommentaren: Kann nicht anonym sein
Für diese App können nur Benutzer, die ein permanentes Konto erstellt haben, kein anonymes Konto, die Kommentare lesen. Um diese Regel durchzusetzen, suchen Sie das sign_in_provider
Attribut, das sich auf jedem auth.token
Objekt befindet:
allow read: if request.auth.token.firebase.sign_in_provider != "anonymous";
Führen Sie Ihre Tests erneut aus und bestätigen Sie, dass ein weiterer Test bestanden wurde.
Kommentare erstellen: Überprüfung einer Ablehnungsliste
Es gibt drei Bedingungen für das Erstellen eines Kommentars:
- Ein Benutzer muss über eine verifizierte E-Mail-Adresse verfügen
- der Kommentar muss weniger als 500 Zeichen lang sein, und
- Sie können nicht auf einer Liste gesperrter Benutzer stehen, die in Firestore in der Sammlung
bannedUsers
gespeichert ist. Nehmen Sie diese Bedingungen einzeln an:
request.auth.token.email_verified == true
request.resource.data.comment.size() < 500
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
Die letzte Regel zum Erstellen von Kommentaren lautet:
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));
Die gesamte Regeldatei ist jetzt:
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));
}
}
}
Führen Sie die Tests erneut aus und stellen Sie sicher, dass ein weiterer Test bestanden wird.
10. Kommentare aktualisieren: Zeitbasierte Regeln
Die Geschäftslogik für Kommentare besteht darin, dass sie nach der Erstellung eine Stunde lang vom Kommentarautor bearbeitet werden können. Um dies zu implementieren, verwenden Sie den createdAt
Zeitstempel.
Um zunächst festzustellen, dass der Benutzer der Autor ist:
request.auth.uid == resource.data.authorUID
Als nächstes, dass der Kommentar innerhalb der letzten Stunde erstellt wurde:
(request.time - resource.data.createdAt) < duration.value(1, 'h');
Kombiniert man diese mit dem logischen UND-Operator, wird die Regel zum Aktualisieren von Kommentaren zu:
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');
Führen Sie die Tests erneut aus und stellen Sie sicher, dass ein weiterer Test bestanden wird.
11. Löschen von Kommentaren: Prüfung auf übergeordneten Besitz
Kommentare können vom Kommentarautor, einem Moderator oder dem Autor des Blogbeitrags gelöscht werden.
Erstens, da die Hilfsfunktion, die Sie zuvor hinzugefügt haben, nach einem authorUID
Feld sucht, das entweder in einem Beitrag oder einem Kommentar vorhanden sein könnte, können Sie die Hilfsfunktion wiederverwenden, um zu überprüfen, ob der Benutzer der Autor oder Moderator ist:
isAuthorOrModerator(resource.data, request.auth)
Um zu überprüfen, ob der Benutzer der Autor des Blogposts ist, suchen Sie den Post in Firestore get
nach:
request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID
Da jede dieser Bedingungen ausreichend ist, verwenden Sie einen logischen ODER-Operator zwischen ihnen:
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;
Führen Sie die Tests erneut aus und stellen Sie sicher, dass ein weiterer Test bestanden wird.
Und die gesamte Regeldatei ist:
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. Nächste Schritte
Glückwunsch! Sie haben die Sicherheitsregeln geschrieben, die alle Tests bestanden und die Anwendung gesichert haben!
Hier sind einige verwandte Themen, in die Sie als Nächstes eintauchen können:
- Blog-Beitrag : Wie man Sicherheitsregeln überprüft
- Codelab : Gehen Sie mit den Emulatoren durch die lokale Erstentwicklung
- Video : Verwendung von Einrichtungs-CI für emulatorbasierte Tests mit GitHub-Aktionen