Schützen Sie Ihre Firestore-Daten mit Firebase-Sicherheitsregeln

1. Bevor Sie beginnen

Cloud Firestore, Cloud Storage für Firebase und die Echtzeitdatenbank basieren 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 dienen. Dies ist einer der wichtigsten Teile der Entwicklung Ihrer Anwendung. Und dieses Codelab führt Sie dabei durch.

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 Komponententests anhand der Sicherheitsregeln durchzuführen und sicherzustellen, dass die Regeln den erwarteten Zugriff zulassen und nicht zulassen.

Sie erfahren, wie Sie:

  • Gewähren Sie detaillierte 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 Verweigerungsliste und vorläufige Löschvorgänge
  • Verstehen Sie, wann Daten denormalisiert werden müssen, um mehreren Zugriffsmustern gerecht zu werden

2. Einrichten

Dies ist eine Blogging-Anwendung. Hier ist eine allgemeine Zusammenfassung der Anwendungsfunktionalität:

Entwürfe für Blogbeiträge:

  • Benutzer können Entwürfe für Blogbeiträge erstellen, die in der drafts gespeichert sind.
  • Der Autor kann einen Entwurf so lange 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 Beiträge erlauben Kommentare, die eine Untersammlung zu jedem veröffentlichten Beitrag darstellen.
  • Um Missbrauch vorzubeugen, müssen Benutzer über eine verifizierte E-Mail-Adresse verfügen und dürfen sich nicht in einer Sperrliste befinden, um einen Kommentar hinterlassen zu können.
  • 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, jedoch mit minimalen Sicherheitsregeln selbst. Sie müssen also zunächst 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“, 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 haben, 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, spätere Versionen enthalten jedoch weitere Fehlerkorrekturen.

$ 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 Zeit ist, die Emulator Suite zu starten.

Starten Sie die Emulatoren

Die Anwendung, mit der Sie arbeiten, verfügt über drei Hauptsammlungen von Firestore: drafts enthalten Blogbeiträge, die in Bearbeitung sind, die published Sammlung enthält die Blogbeiträge, die veröffentlicht wurden, und comments sind eine Untersammlung zu veröffentlichten Beiträgen. Das Repo 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 Sammlungen und comments benötigt. Sie schreiben die Sicherheitsregeln, damit diese Tests bestehen.

Zunächst ist Ihre Datenbank gesperrt: Lese- und Schreibvorgänge in der 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 gibt es 9 Ausfälle. Während Sie die Regeldatei erstellen, können Sie den Fortschritt messen, indem Sie beobachten, wie weitere Tests durchgeführt werden.

4. Erstellen Sie Blog-Post-Entwürfe.

Da sich der Zugriff auf Entwürfe von Blogbeiträgen stark vom Zugriff auf veröffentlichte Blogbeiträge unterscheidet, speichert diese Blogging-App Entwürfe von Blogbeiträgen in einer separaten Sammlung, /drafts . Auf Entwürfe kann nur der Autor oder ein Moderator zugreifen und verfügt über 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 da es sich um die oberste Ebene handelt, gilt derzeit für alle Anfragen die gleiche pauschale Regel, unabhängig davon, wer die Anfrage stellt oder welche Daten sie lesen oder schreiben möchte.

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 diesem Antrag 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 gestellt hat, mit der im Dokument aufgeführten UID übereinstimmt.

Die erste Bedingung für die Erstellung ist:

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 über alle verfügt 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 sie 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 erfolgreich ist.

5. Aktualisieren Sie die Entwürfe von Blogbeiträgen.

Als Nächstes bearbeiten Autoren die Entwurfsdokumente, während sie ihre Entwürfe für Blogbeiträge verfeinern. Erstellen Sie eine Regel für die Bedingungen, unter denen ein Beitrag aktualisiert werden kann. Erstens kann nur der Autor seine Entwürfe aktualisieren. Beachten Sie, dass Sie hier die bereits geschriebene UID resource.data.authorUID überprüfen:

resource.data.authorUID == request.auth.uid

Die zweite Voraussetzung für ein Update besteht darin, dass sich zwei Attribute, authorUID “ und createdAt , nicht ändern dürfen:

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 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 Ihre Tests erneut durch und bestätigen Sie, dass ein weiterer Test erfolgreich ist.

6. Entwürfe löschen und lesen: Attributbasierte Zugriffskontrolle

Ebenso wie Autoren Entwürfe erstellen und aktualisieren können, können sie auch Entwürfe löschen.

resource.data.authorUID == request.auth.uid

Darüber hinaus dürfen Autoren mit einem isModerator Attribut in 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, 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 Ihre Tests erneut durch und bestätigen Sie, dass ein weiterer Test nun erfolgreich ist.

7. Liest, erstellt und löscht veröffentlichte Beiträge: Denormalisierung für unterschiedliche Zugriffsmuster

Da die Zugriffsmuster für veröffentlichte Beiträge und Entwurfsbeiträge so unterschiedlich sind, denormalisiert diese App die Beiträge in separate draft und published Sammlungen. Veröffentlichte Beiträge können beispielsweise von jedem gelesen, aber nicht endgültig gelöscht werden, während Entwürfe gelöscht werden können, aber nur vom Autor und den 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 Beiträge. Die einfachsten Regeln beim 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 erfolgreich ist.

8. Aktualisieren veröffentlichter Beiträge: Benutzerdefinierte Funktionen und lokale Variablen

Die Bedingungen für die Aktualisierung eines veröffentlichten Beitrags sind:

  • Dies kann nur vom Autor oder Moderator durchgeführt werden
  • 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 es schwierig werden, sie zu lesen und beizubehalten. Stattdessen erstellen Sie eine benutzerdefinierte Funktion, die die Logik für die Rolle als Autor oder Moderator kapselt. Anschließend rufen Sie es unter 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 Post-Dokument (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, und achten 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 der 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 einem Update weiterhin 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

Und schließlich fügen Sie eine Bedingung hinzu, dass der Titel weniger als 50 Zeichen lang sein muss. Da es sich hierbei um wiederverwendete Logik handelt, können Sie dies erreichen, indem Sie eine neue Funktion namens 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);

Und die vollständige Regeldatei lautet:

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. Zu diesem Zeitpunkt sollten Sie 5 Tests bestanden und 4 Tests nicht bestanden haben.

9. Kommentare: Untersammlungen und Anmeldeanbieterberechtigungen

Die veröffentlichten Beiträge erlauben Kommentare und die Kommentare werden in einer Untersammlung des veröffentlichten Beitrags gespeichert ( /published/{postID}/comments/{commentID} ). Standardmäßig gelten die Regeln einer Sammlung nicht für Untersammlungen. Sie möchten nicht, dass für die Kommentare dieselben Regeln gelten, die für das übergeordnete Dokument des veröffentlichten Beitrags gelten. Du wirst 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

Kommentare lesen: Kann nicht anonym sein

Bei dieser App können nur Benutzer die Kommentare lesen, die ein dauerhaftes Konto erstellt haben, kein anonymes Konto. Um diese Regel durchzusetzen, suchen Sie nach dem 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 Ihre Tests erneut durch und bestätigen Sie, dass ein weiterer Test erfolgreich ist.

Kommentare erstellen: Überprüfung einer Deny-Liste

Es gibt drei Bedingungen zum Erstellen eines Kommentars:

  • Ein Benutzer muss über eine bestätigte E-Mail-Adresse verfügen
  • Der Kommentar muss weniger als 500 Zeichen lang sein
  • Sie können nicht auf einer Liste gesperrter Benutzer stehen, die im Firestore in der Sammlung bannedUsers gespeichert ist. Nehmen Sie diese Bedingungen einzeln:
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 lautet 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 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 erneut aus und stellen Sie sicher, dass ein weiterer Test erfolgreich ist.

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 Zeitstempel createdAt .

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, 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');

Führen Sie die Tests erneut aus und stellen Sie sicher, dass ein weiterer Test erfolgreich ist.

11. Kommentare löschen: Überprüfung auf übergeordnete Eigentümerschaft

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 Feld authorUID 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 Blog-Beitrags ist, verwenden Sie einen get Befehl, um den Beitrag im Firestore nachzuschlagen:

request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID

Da jede dieser Bedingungen ausreichend ist, verwenden 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 erneut aus und stellen Sie sicher, dass ein weiterer Test erfolgreich ist.

Und die gesamte Regeldatei lautet:

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 und die Anwendung gesichert haben!

Hier sind einige verwandte Themen, in die Sie als Nächstes eintauchen sollten:

  • Blogbeitrag : So überprüfen Sie Sicherheitsregeln im Code
  • Codelab : Spaziergang durch die lokale Erstentwicklung mit den Emulatoren
  • Video : So verwenden Sie das Setup-CI für emulatorbasierte Tests mithilfe von GitHub-Aktionen