Firestore-Daten mit Firebase-Sicherheitsregeln schützen

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:

  • Blogpost: How to code review Security Rules (auf Englisch)
  • Codelab: Lokale Entwicklung mit den Emulatoren
  • Video: CI für emulatorbasierte Tests mit GitHub Actions einrichten