1. Hinweis
Cloud Firestore, Cloud Storage for Firebase und die Realtime Database basieren auf Konfigurationsdateien, die Sie schreiben, um Lese- und Schreibzugriff zu gewähren. Diese Konfiguration, die als Sicherheitsregeln bezeichnet wird, kann auch als eine Art Schema für Ihre App dienen. Sie ist einer der wichtigsten Aspekte bei der Entwicklung Ihrer Anwendung. In diesem Codelab erfahren Sie, wie das geht.
Voraussetzungen
- Ein einfacher Editor wie Visual Studio Code, Atom oder Sublime Text
- Node.js 8.6.0 oder höher (nvm verwenden, um Node.js zu installieren;
node --version
ausführen, um die Version zu prüfen) - Java 7 oder höher (Installationsanleitung; Version prüfen:
java -version
)
Aufgabe
In diesem Codelab sichern Sie eine einfache Blogplattform, die auf Firestore basiert. Mit dem Firestore-Emulator führen Sie Einheitentests für die Sicherheitsregeln aus, um sicherzustellen, dass die Regeln den erwarteten Zugriff zulassen und verweigern.
Die folgenden Themen werden behandelt:
- Detaillierte Berechtigungen erteilen
- Daten- und Typvalidierungen erzwingen
- Attributbasierte Zugriffssteuerung implementieren
- Zugriff basierend auf der Authentifizierungsmethode gewähren
- Benutzerdefinierte Funktionen erstellen
- Zeitbasierte Sicherheitsregeln erstellen
- Sperrliste und vorläufiges Löschen implementieren
- Wissen, wann Daten denormalisiert werden sollten, um mehrere Zugriffsmuster zu erfüllen
2. Einrichten
Dies ist eine Blogging-Anwendung. Hier eine kurze Zusammenfassung der Anwendungsfunktionen:
Blogposts entwerfen:
- Nutzer können Blogbeiträge als Entwurf erstellen, die in der Sammlung
drafts
gespeichert werden. - Der Autor kann einen Entwurf weiter bearbeiten, bis er veröffentlicht werden kann.
- Wenn sie bereit für die Veröffentlichung ist, wird eine Firebase-Funktion ausgelöst, die ein neues Dokument in der Sammlung
published
erstellt. - Entwürfe können vom Autor oder von Website-Moderatoren 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. Dabei wird das Attribut
visible
auf „false“ aktualisiert.
Kommentare
- Bei veröffentlichten Beiträgen sind Kommentare zulässig. Diese sind eine untergeordnete Sammlung für jeden veröffentlichten Beitrag.
- Um Missbrauch zu reduzieren, müssen Nutzer eine bestätigte E-Mail-Adresse haben und dürfen nicht auf einer Sperrliste stehen, um einen Kommentar zu hinterlassen.
- Kommentare können nur innerhalb einer Stunde nach der 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, mit denen erforderliche Felder und Datenvalidierungen erzwungen werden.
Alles erfolgt lokal mit der Firebase Emulator Suite.
Quellcode abrufen
In diesem Codelab beginnen Sie mit Tests für die Sicherheitsregeln, aber mit minimalen Sicherheitsregeln selbst. Als Erstes müssen Sie also 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 nun die Abhängigkeiten, damit Sie die Tests ausführen können. Wenn Sie eine langsamere Internetverbindung haben, kann dies ein bis zwei Minuten dauern:
# Move into the functions directory, install dependencies, jump out. $ cd functions && npm install && cd -
Firebase CLI herunterladen
Die Emulator Suite, mit der Sie die Tests ausführen, 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 aktuelle Version der CLI haben. Dieses Codelab sollte mit Version 8.4.0 oder höher funktionieren. Spätere Versionen enthalten jedoch mehr Fehlerkorrekturen.
$ firebase --version 9.10.2
3. Tests ausführen
In diesem Abschnitt führen Sie die Tests lokal aus. Das bedeutet, dass es an der Zeit ist, die Emulator Suite zu starten.
Emulatoren starten
Die Anwendung, mit der Sie arbeiten, hat drei Haupt-Firestore-Sammlungen: drafts
enthält Blogbeiträge, die sich in Bearbeitung befinden, 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 Unit-Tests für die Sicherheitsregeln, die die Nutzerattribute und andere Bedingungen definieren, die ein Nutzer benötigt, um Dokumente in den Sammlungen drafts
, published
und comments
zu erstellen, zu lesen, zu aktualisieren und zu löschen. Sie schreiben die Sicherheitsregeln, damit diese Tests bestanden werden.
Zu Beginn 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. Öffnen Sie functions/test.js
in Ihrem Editor, um die Tests zu sehen.
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 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 ...
Derzeit gibt es 9 Fehler. Während Sie die Regelfile erstellen, können Sie den Fortschritt daran erkennen, dass immer mehr Tests bestanden werden.
4. Entwürfe für Blogposts erstellen
Da sich der Zugriff auf Blogpost-Entwürfe so stark vom Zugriff auf veröffentlichte Blogposts unterscheidet, werden Blogpost-Entwürfe in dieser Blogging-App in einer separaten Sammlung, /drafts
, gespeichert. Auf Entwürfe können nur der Autor oder ein Moderator zugreifen. Außerdem werden erforderliche und unveränderliche Felder validiert.
Wenn Sie die Datei firestore.rules
öffnen, sehen Sie eine Standardregeln-Datei:
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 angewendet zu werden. Da sie 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 gelesen oder geschrieben werden sollen.
Entfernen Sie zuerst 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 diesem Codelab berücksichtigt. Sie sind jedoch 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, legen Sie fest, 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 UID im Dokument übereinstimmt.
Die erste Bedingung für das Erstellen lautet:
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 Nutzer gibt das Feld createdAt
nicht an. Dadurch wird erzwungen, dass die App es hinzufügt, bevor sie versucht, 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 für das Erstellen eines Blogposts ist, dass der Titel nicht länger als 50 Zeichen sein darf:
request.resource.data.title.size() < 50
Da alle diese Bedingungen zutreffen müssen, werden sie mit dem logischen UND-Operator &&
verkettet. 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 bestätigen Sie, dass der erste Test bestanden wird.
5. Blogpost-Entwürfe aktualisieren
Als Nächstes bearbeiten die Autoren die Entwurfsdokumente, um ihre Blogposts zu optimieren. Regel für die Bedingungen erstellen, unter denen ein Beitrag aktualisiert werden kann Erstens kann nur der Autor seine Entwürfe aktualisieren. Prüfen Sie hier die bereits eingetragene UID:resource.data.authorUID
resource.data.authorUID == request.auth.uid
Die zweite Anforderung für ein Update ist, dass sich zwei Attribute nicht ändern dürfen: authorUID
und createdAt
.
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 alle diese Bedingungen erfüllt sein müssen, werden sie mit &&
verkettet:
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 lauten:
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 bestätigen Sie, dass ein anderer Test bestanden wurde.
6. Entwürfe löschen und lesen: Attributbasierte Zugriffssteuerung
Autoren können nicht nur Entwürfe erstellen und aktualisieren, sondern auch löschen.
resource.data.authorUID == request.auth.uid
Außerdem dürfen Autoren mit dem Attribut isModerator
in ihrem Autorisierungstoken Entwürfe löschen:
request.auth.token.isModerator == true
Da eine der beiden Bedingungen für das Löschen ausreicht, werden sie mit einem logischen ODER-Operator (||
) verkettet:
allow delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true
Für Lesezugriffe gelten dieselben Bedingungen, sodass die Berechtigung der Regel 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 jetzt bestanden wird.
7. Lesen, Erstellen und Löschen von veröffentlichten Beiträgen: Denormalisierung für verschiedene Zugriffsmuster
Da sich die Zugriffsmuster für die veröffentlichten und die Entwurfsbeiträge so stark unterscheiden, werden die Beiträge in dieser App in separaten draft
- und published
-Sammlungen denormalisiert. Veröffentlichte Beiträge können beispielsweise von jedem gelesen, aber nicht endgültig gelöscht werden. Entwürfe können gelöscht werden, aber nur der Autor und Moderatoren können sie lesen. Wenn ein Nutzer in dieser App einen Blogpost-Entwurf veröffentlichen möchte, wird eine Funktion ausgelöst, die den neuen veröffentlichten Post erstellt.
Als Nächstes schreiben Sie die Regeln für veröffentlichte Beiträge. Die einfachsten Regeln sind, dass veröffentlichte Beiträge von jedem gelesen werden können 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 den vorhandenen Regeln hinzufügen, sieht die gesamte Regelfile so 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;
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 wird.
8. Veröffentlichte Beiträge aktualisieren: Benutzerdefinierte Funktionen und lokale Variablen
Die Bedingungen für die Aktualisierung eines veröffentlichten Beitrags sind:
- nur der Autor oder Moderator kann dies tun und
- Er muss alle erforderlichen Felder enthalten.
Da Sie bereits Bedingungen für die Rolle als Autor oder Moderator geschrieben haben, könnten Sie die Bedingungen kopieren und einfügen. Mit der Zeit könnte das jedoch schwer lesbar und zu pflegen sein. Stattdessen erstellen Sie eine benutzerdefinierte Funktion, die die Logik für die Rolle als Autor oder Moderator kapselt. Anschließend rufen Sie sie über mehrere Bedingungen auf.
Benutzerdefinierte Funktion erstellen
Erstellen Sie über der Match-Anweisung für Entwürfe eine neue Funktion namens isAuthorOrModerator
, die als Argumente ein Post-Dokument (dies funktioniert sowohl für Entwürfe als auch für veröffentlichte Beiträge) und das Auth-Objekt des Nutzers 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: ...
}
}
}
Lokale Variablen verwenden
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. Unsere Funktion gibt einen booleschen Wert zurück, der angibt, ob eine der beiden Variablen „true“ 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 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);
}
Sie können jetzt 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 Posts 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 von veröffentlichten Beiträgen 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 schließlich eine Bedingung hinzu, dass der Titel weniger als 50 Zeichen lang sein muss. Da es sich um wiederverwendete Logik handelt, können Sie dazu eine neue Funktion, titleIsUnder50Chars
, erstellen. Mit der neuen Funktion lautet die Bedingung für die Aktualisierung eines veröffentlichten Beitrags:
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);
}
}
}
Wiederholen Sie die Tests. Jetzt sollten Sie 5 bestandene und 4 fehlgeschlagene Tests haben.
9. Kommentare: Untergeordnete Sammlungen und Berechtigungen für Anmeldeanbieter
Für die veröffentlichten Beiträge sind Kommentare zulässig und die Kommentare werden in einer Untergruppe des veröffentlichten Beitrags (/published/{postID}/comments/{commentID}
) gespeichert. Standardmäßig gelten die Regeln einer Sammlung nicht für Untergruppen. Sie möchten nicht, dass dieselben Regeln, die für das übergeordnete Dokument des veröffentlichten Beitrags gelten, auch für die Kommentare gelten. Daher erstellen Sie andere Regeln.
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
Kommentare lesen: Nicht anonym möglich
In dieser App können nur Nutzer, die ein dauerhaftes Konto erstellt haben, nicht aber Nutzer mit einem anonymen Konto, die Kommentare lesen. Um diese Regel durchzusetzen, suchen Sie das Attribut sign_in_provider
, das sich in 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 wird.
Kommentare erstellen: Sperrliste prüfen
Es gibt drei Bedingungen für das Erstellen eines Kommentars:
- Ein Nutzer muss eine bestätigte E-Mail-Adresse haben.
- Der Kommentar darf maximal 500 Zeichen lang sein.
- Sie dürfen nicht auf einer Liste gesperrter Nutzer stehen, die in Firestore in der Sammlung
bannedUsers
gespeichert ist. Sehen wir uns 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 endgültige Regel für das 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 achten Sie darauf, dass ein weiterer Test bestanden wird.
10. Kommentare aktualisieren: Zeitbasierte Regeln
Kommentare können vom jeweiligen Ersteller eine Stunde lang nach der Erstellung bearbeitet werden. Verwenden Sie dazu den Zeitstempel createdAt
.
Zuerst muss nachgewiesen werden, dass der Nutzer der Autor ist:
request.auth.uid == resource.data.authorUID
Als Nächstes wird geprüft, 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 lautet die Regel zum Aktualisieren von Kommentaren so:
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 noch einmal aus und achten Sie darauf, dass ein weiterer Test bestanden wird.
11. Kommentare löschen: Überprüfung der Inhaberschaft des übergeordneten Elements
Kommentare können vom Verfasser des Kommentars, von einem Moderator oder vom Verfasser des Blogbeitrags gelöscht werden.
Da die zuvor hinzugefügte Hilfsfunktion nach einem authorUID
-Feld sucht, das entweder in einem Beitrag oder in einem Kommentar vorhanden sein kann, können Sie die Hilfsfunktion wiederverwenden, um zu prüfen, ob der Nutzer der Autor oder Moderator ist:
isAuthorOrModerator(resource.data, request.auth)
Verwenden Sie ein get
, um den Beitrag in Firestore zu suchen und zu prüfen, ob der Nutzer der Autor des Blogbeitrags ist:
request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID
Da jede dieser Bedingungen ausreicht, 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 noch einmal aus und achten Sie darauf, dass ein weiterer Test bestanden wird.
Die gesamte 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 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, mit denen alle Tests bestanden wurden und die Anwendung gesichert ist.
Hier sind einige verwandte Themen, die Sie sich als Nächstes ansehen können: