1. Hinweis
Cloud Firestore, Cloud Storage for Firebase und die Realtime Database erfordern Konfigurationsdateien, die Sie schreiben, um Lese- und Schreibzugriff zu gewähren. Diese Konfiguration, die als Sicherheitsregeln bezeichnet wird, kann auch als Schema für Ihre App fungieren. Sie ist einer der wichtigsten Bestandteile der Entwicklung Ihrer Anwendung. In diesem Codelab zeigen wir Ihnen, wie das geht.
Voraussetzungen
- Einen einfachen Editor wie Visual Studio Code, Atom oder Sublime Text
- Node.js 8.6.0 oder höher (zum Installieren von Node.js verwenden Sie nvm. Um Ihre Version zu prüfen, führen Sie
node --version
aus) - Java 7 oder höher. Folgen Sie dieser Anleitung, um Java zu installieren. Um Ihre Version zu prüfen, führen Sie
java -version
aus.
Aufgabe
In diesem Codelab sichern Sie eine einfache Blogplattform, die auf Firestore basiert. Sie verwenden den Firestore-Emulator, um mit den Sicherheitsregeln Einheitentests auszuführen und dafür zu sorgen, dass die Regeln den erwarteten Zugriff zulassen oder verweigern.
Die folgenden Themen werden behandelt:
- Detaillierte Berechtigungen gewähren
- Daten- und Typvalidierungen erzwingen
- Attributbasierte Zugriffssteuerung implementieren
- Zugriff anhand der Authentifizierungsmethode gewähren
- Benutzerdefinierte Funktionen erstellen
- Zeitbasierte Sicherheitsregeln erstellen
- Sperrliste und vorläufiges Löschen implementieren
- Wann Daten denormalisiert werden sollten, um mehreren Zugriffsmustern gerecht zu werden
2. Einrichten
Dies ist eine Blogging-Anwendung. Hier eine allgemeine Zusammenfassung der Anwendungsfunktionen:
Blogpost-Entwürfe:
- Nutzer können Blogposts erstellen, die sich in der Sammlung
drafts
befinden. - Der Autor kann den Entwurf so lange aktualisieren, bis er zur Veröffentlichung bereit ist.
- Wenn die Daten veröffentlicht werden können, wird eine Firebase-Funktion ausgelöst, die ein neues Dokument in der Sammlung
published
erstellt. - Entwürfe können vom Autor oder von Websitemoderatoren gelöscht werden.
Veröffentlichte Blogposts:
- Veröffentlichte Beiträge können nicht von Nutzern erstellt werden, sondern nur über eine Funktion.
- Sie können nur vorläufig gelöscht werden. Dadurch wird ein
visible
-Attribut auf „false“ gesetzt.
Kommentare
- Bei veröffentlichten Beiträgen können Kommentare hinzugefügt werden. Sie sind eine Untersammlung zu jedem veröffentlichten Beitrag.
- Um Missbrauch zu verhindern, müssen Nutzer eine bestätigte E-Mail-Adresse haben und dürfen nicht gesperrt sein, um einen Kommentar abgeben zu können.
- Kommentare können nur innerhalb einer Stunde nach ihrer Veröffentlichung aktualisiert werden.
- Kommentare können vom Verfasser des Kommentars, vom Verfasser des ursprünglichen Beitrags oder von Moderatoren gelöscht werden.
Zusätzlich zu Zugriffsregeln erstellen Sie Sicherheitsregeln, die Pflichtfelder und Datenvalidierungen erzwingen.
Alles läuft lokal mit der Firebase Emulator Suite.
Quellcode abrufen
In diesem Codelab beginnen Sie mit Tests für die Sicherheitsregeln, aber nur minimalen Sicherheitsregeln. Daher müssen Sie zuerst die Quelle klonen, um die Tests auszuführen:
$ git clone https://github.com/FirebaseExtended/codelab-rules.git
Wechseln Sie dann in das Verzeichnis „initial-state“, in dem Sie für den Rest dieses Codelabs arbeiten werden:
$ cd codelab-rules/initial-state
Installieren Sie jetzt die Abhängigkeiten, damit Sie die Tests ausführen können. Bei einer langsameren Internetverbindung kann dies ein bis zwei Minuten dauern:
# Move into the functions directory, install dependencies, jump out. $ cd functions && npm install && cd -
Firebase CLI abrufen
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
Prüfen Sie als Nächstes, ob Sie die neueste Version der Befehlszeile haben. Dieses Codelab sollte mit Version 8.4.0 oder höher funktionieren. Höhere Versionen enthalten jedoch weitere Fehlerkorrekturen.
$ firebase --version 9.10.2
3. Tests ausführen
In diesem Abschnitt führen Sie die Tests lokal aus. Jetzt ist es an der Zeit, die Emulator Suite zu starten.
Emulatoren starten
Die Anwendung, mit der Sie arbeiten, enthält drei Haupt-Firestore-Sammlungen: drafts
enthält Blogbeiträge, die in Bearbeitung sind, die Sammlung published
enthält die veröffentlichten Blogbeiträge und comments
ist eine untergeordnete Sammlung für veröffentlichte Beiträge. Das Repository enthält Einheitentests für die Sicherheitsregeln, die die Nutzerattribute und andere Bedingungen definieren, die ein Nutzer zum Erstellen, Lesen, Aktualisieren und Löschen von Dokumenten in den Sammlungen drafts
, published
und comments
benötigt. Sie schreiben die Sicherheitsregeln so, dass diese Tests bestanden werden.
Zuerst ist Ihre Datenbank gesperrt: Lese- und Schreibzugriffe auf die Datenbank werden generell abgelehnt und alle Tests schlagen fehl. Wenn Sie Sicherheitsregeln schreiben, werden die Tests bestanden. Wenn Sie sich die Tests ansehen möchten, öffnen Sie functions/test.js
in Ihrem Editor.
Starten Sie die Emulatoren in der Befehlszeile mit emulators:exec
und führen Sie die Tests aus:
$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test"
Scrollen Sie in der Ausgabe nach oben:
$ 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 ...
Derzeit sind 9 Fehler aufgetreten. Während Sie die Regeldatei erstellen, können Sie den Fortschritt messen, indem Sie darauf achten, dass mehr Tests bestanden werden.
4. Entwürfe für Blogposts erstellen
Da sich der Zugriff auf Blogpost-Entwürfe stark vom Zugriff für veröffentlichte Blogposts unterscheidet, speichert diese Blogging-App Blogpost-Entwürfe in einer separaten Sammlung namens /drafts
. Nur der Autor oder ein Moderator kann auf Entwürfe zugreifen. Pflichtfelder und unveränderliche Felder können validiert werden.
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 da es sich auf der obersten Ebene befindet, gilt derzeit dieselbe allgemeine Regel für alle Anfragen, unabhängig davon, wer die Anfrage stellt oder welche Daten sie lesen oder schreiben möchten.
Entfernen Sie zuerst die innerste Abgleichsbeschreibung und ersetzen Sie sie durch match /drafts/{draftID}
. Kommentare zur Struktur von Dokumenten können in Regeln hilfreich sein und werden in diesem Codelab verwendet. 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
}
}
}
Mit der ersten Regel, die Sie für Entwürfe erstellen, wird festgelegt, wer die Dokumente erstellen darf. In dieser Anwendung können Entwürfe nur von der Person erstellt werden, die als Autor aufgeführt ist. Prüfen Sie, ob die UID der Person, die die Anfrage stellt, mit der im Dokument aufgeführten UID übereinstimmt.
Die erste Bedingung für die Erstellung lautet:
request.resource.data.authorUID == request.auth.uid
Anschließend können Dokumente nur erstellt werden, wenn sie die drei erforderlichen Felder authorUID
, createdAt
und title
enthalten. Der Nutzer gibt das Feld createdAt
nicht an. Dadurch muss die App es hinzufügen, bevor versucht wird, ein Dokument zu erstellen. Da Sie nur prüfen müssen, ob die Attribute erstellt werden, können Sie prüfen, ob request.resource
alle diese Schlüssel enthält:
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
])
Die letzte Anforderung beim Erstellen eines Blogposts 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, können Sie sie mit dem logischen AND-Operator &&
verknüpfen. Die erste Regel lautet dann:
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 die Tests im Terminal noch einmal aus und prüfen Sie, ob der erste Test bestanden wurde.
5. Blogpostentwürfe aktualisieren
Als Nächstes bearbeiten die Autoren die Entwürfe ihrer Blogbeiträge. Erstellen Sie eine Regel für die Bedingungen, unter denen ein Beitrag aktualisiert werden kann. Erstens: Nur der Autor kann seine Entwürfe aktualisieren. Hier prüfen Sie die bereits geschriebene UIDresource.data.authorUID
:
resource.data.authorUID == request.auth.uid
Die zweite Anforderung für eine Aktualisierung besteht darin, dass sich zwei Attribute, authorUID
und createdAt
, nicht ändern dürfen:
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]);
Der Titel darf 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 sehen dann Folgendes aus:
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 die Tests noch einmal aus und prüfen Sie, ob ein anderer Test bestanden wird.
6. Entwürfe löschen und lesen: Attributbasierte Zugriffssteuerung
Autoren können Entwürfe erstellen und aktualisieren, aber auch löschen.
resource.data.authorUID == request.auth.uid
Außerdem können Autoren mit einem isModerator
-Attribut als Authentifizierungstoken Entwürfe löschen:
request.auth.token.isModerator == true
Da jede dieser Bedingungen für das Löschen ausreicht, können Sie sie mit dem logischen OR-Operator ||
verknüpfen:
allow delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true
Für Lesevorgänge gelten dieselben Bedingungen, sodass 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 die Tests noch einmal aus und prüfen Sie, ob ein anderer Test erfolgreich war.
7. Liest, erstellt und löscht für veröffentlichte Beiträge: Denormalisierung für unterschiedliche Zugriffsmuster
Da die Zugriffsmuster für veröffentlichte Beiträge und Beitragsentwürfe sehr unterschiedlich sind, denormalisiert diese App die Beiträge in separate draft
- und published
-Sammlungen. Beispielsweise können veröffentlichte Beiträge von jedem gelesen, aber nicht endgültig gelöscht werden. Entwürfe können zwar gelöscht werden, aber nur vom Autor und von Moderatoren gelesen werden können. Wenn ein Nutzer in dieser App einen Blogpost-Entwurf veröffentlichen möchte, wird eine Funktion ausgelöst, die den neuen veröffentlichten Beitrag erstellt.
Als Nächstes verfassen Sie die Regeln für veröffentlichte Posts. Die einfachsten Regeln sind, dass veröffentlichte Beiträge von allen gelesen werden können, aber von niemandem erstellt oder gelöscht werden können. Fügen Sie die folgenden 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 den bestehenden Regeln hinzugefügt werden, 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 noch einmal aus und prüfen Sie, ob ein anderer Test bestanden wurde.
8. Veröffentlichte Beiträge aktualisieren: Benutzerdefinierte Funktionen und lokale Variablen
Voraussetzungen für die Aktualisierung eines veröffentlichten Beitrags:
- Die Änderung kann nur vom Autor oder Moderator vorgenommen werden.
- Er muss alle erforderlichen Felder enthalten.
Da Sie bereits die Bedingungen für die Tätigkeit als Autor oder Moderator verfasst haben, können Sie diese kopieren und einfügen, aber mit der Zeit könnte es schwierig werden, sie zu lesen und zu pflegen. Stattdessen erstellen Sie eine benutzerdefinierte Funktion, die die Logik für Ihre Rolle als Autor oder Moderator enthält. Dann rufen Sie sie aus mehreren Bedingungen auf.
Benutzerdefinierte Funktion erstellen
Erstellen Sie über der Übereinstimmungsanweisung für Entwürfe eine neue Funktion mit dem Namen isAuthorOrModerator
, die als Argumente ein Post-Dokument (dies funktioniert sowohl für Entwürfe oder veröffentlichte Beiträge) als auch das auth-Objekt des Nutzers annimmt:
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: ...
}
}
}
Lokale Variablen verwenden
Verwenden Sie innerhalb der Funktion das Schlüsselwort let
, um isAuthor
- und isModerator
-Variablen festzulegen. Alle Funktionen müssen mit einer Rückgabeanweisung enden. Unsere Funktion 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;
}
Funktion aufrufen
Aktualisieren Sie nun die Regel für Entwürfe, um diese Funktion aufzurufen. Achten Sie dabei darauf, 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 zum Aktualisieren veröffentlichter Beiträge schreiben, die auch die neue Funktion verwendet:
allow update: if isAuthorOrModerator(resource.data, request.auth);
Validierungen hinzufügen
Einige Felder eines veröffentlichten Beitrags sollten nicht geändert werden. Insbesondere die Felder url
, authorUID
und publishedAt
sind unveränderlich. Die anderen beiden Felder title
und content
sowie visible
müssen nach einer Aktualisierung weiterhin vorhanden sein. Fügen Sie Bedingungen hinzu, um diese Anforderungen für Aktualisierungen veröffentlichter Beiträge zu erzwingen:
// 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"
])
Benutzerdefinierte Funktion selbst erstellen
Fügen Sie abschließend eine Bedingung hinzu, die besagt, dass der Titel weniger als 50 Zeichen lang sein darf. Da es sich um eine wiederverwendete Logik handelt, können Sie dazu eine neue Funktion namens titleIsUnder50Chars
erstellen. Mit der neuen Funktion ist die Bedingung für die Aktualisierung eines veröffentlichten Beitrags wie folgt:
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);
Die vollständige Regeldatei sieht so aus:
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 noch einmal aus. Sie sollten nun 5 bestandene und 4 fehlgeschlagene Tests haben.
9. Kommentare: Berechtigungen für untergeordnete Sammlungen und Anmeldeanbieter
Für die veröffentlichten Beiträge sind Kommentare zulässig. Die Kommentare werden in einer Untersammlung des veröffentlichten Beitrags (/published/{postID}/comments/{commentID}
) gespeichert. Standardmäßig gelten die Regeln einer Sammlung nicht für Untersammlungen. Sie möchten nicht, dass dieselben Regeln wie für das übergeordnete Dokument des veröffentlichten Beitrags auch auf die Kommentare angewendet werden. Sie erstellen dann unterschiedliche Regeln.
Wenn Sie Regeln für den Zugriff auf Kommentare schreiben möchten, beginnen Sie mit der Übereinstimmungsanweisung:
match /published/{postID}/comments/{commentID} {
// `authorUID`: string, required
// `comment`: string, < 500 characters, required
// `createdAt`: timestamp, required
// `editedAt`: timestamp, optional
Lesen von Kommentaren: Anonymität ist nicht möglich
Bei dieser App können nur Nutzer, die ein dauerhaftes Konto erstellt haben, die Kommentare lesen. Anonyme Konten sind nicht zulässig. Um diese Regel zu erzwingen, suchen Sie nach dem Attribut sign_in_provider
, das sich auf jedem auth.token
-Objekt befindet:
allow read: if request.auth.token.firebase.sign_in_provider != "anonymous";
Führen Sie die Tests noch einmal aus und prüfen Sie, ob ein weiterer Test bestanden wurde.
Kommentare erstellen: Sperrliste prüfen
Für das Erstellen eines Kommentars gibt es drei Bedingungen:
- Ein Nutzer muss eine bestätigte E-Mail-Adresse haben.
- Der Kommentar darf maximal 500 Zeichen lang sein.
- Sie dürfen nicht auf der Liste der gesperrten Nutzer stehen, die in Firestore in der Sammlung
bannedUsers
gespeichert ist. Einzelne Bedingungen:
request.auth.token.email_verified == true
request.resource.data.comment.size() < 500
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
Die letzte Regel beim 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 vollständige Regeldatei sieht jetzt so aus:
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));
}
}
}
Führen Sie die Tests noch einmal aus und prüfen Sie, ob ein weiterer Test bestanden wurde.
10. Kommentare aktualisieren: Zeitbasierte Regeln
Kommentare können vom Verfasser eine Stunde nach dem Erstellen bearbeitet werden. Verwenden Sie dazu den Zeitstempel createdAt
.
Prüfen Sie zuerst, ob der Nutzer der Autor ist:
request.auth.uid == resource.data.authorUID
Prüfen Sie als Nächstes, ob der Kommentar innerhalb der letzten Stunde erstellt wurde:
(request.time - resource.data.createdAt) < duration.value(1, 'h');
In Kombination mit dem logischen AND-Operator ergibt sich folgende Regel zum Aktualisieren von Kommentaren:
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');
Wiederholen Sie die Tests und achten Sie darauf, dass mindestens ein Test bestanden wird.
11. Kommentare löschen: Überprüfung der Inhaberschaft des übergeordneten Elements
Kommentare können vom Verfasser, einem Moderator oder dem Autor des Blogposts gelöscht werden.
Erstens: Da die zuvor hinzugefügte Helper-Funktion nach einem authorUID
-Feld sucht, das in einem Beitrag oder Kommentar vorhanden sein kann, kannst du die Helper-Funktion wiederverwenden, um zu prüfen, ob der Nutzer der Autor oder Moderator ist:
isAuthorOrModerator(resource.data, request.auth)
Um zu prüfen, ob der Nutzer der Autor des Blogposts ist, kannst du mit einer get
den Beitrag in Firestore abrufen:
request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID
Da jede dieser Bedingungen ausreichend ist, setzen Sie zwischen ihnen einen logischen ODER-Operator:
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 noch einmal aus und prüfen Sie, ob ein weiterer Test bestanden wurde.
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 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. Nächste Schritte
Glückwunsch! Sie haben die Sicherheitsregeln geschrieben, die alle Tests bestanden haben und die Anwendung geschützt haben.
Hier sind ein paar verwandte Themen, mit denen Sie sich als Nächstes beschäftigen können: