Firestore-Daten mit Firebase-Sicherheitsregeln schützen

1. Hinweis

Cloud Firestore, Cloud Storage for Firebase und die Realtime Database erfordern Konfigurationsdateien, die Sie schreiben, um Lese- und Schreibzugriff zu gewähren. Diese Konfiguration, die als Sicherheitsregeln bezeichnet wird, kann auch als Schema für Ihre App fungieren. Sie ist einer der wichtigsten Bestandteile der Entwicklung Ihrer Anwendung. In diesem Codelab zeigen wir Ihnen, wie das geht.

Voraussetzungen

  • Einen einfachen Editor wie Visual Studio Code, Atom oder Sublime Text
  • Node.js 8.6.0 oder höher (zum Installieren von Node.js verwenden Sie nvm. Um Ihre Version zu prüfen, führen Sie node --version aus)
  • Java 7 oder höher. Folgen Sie dieser Anleitung, um Java zu installieren. Um Ihre Version zu prüfen, führen Sie java -version aus.

Aufgabe

In diesem Codelab sichern Sie eine einfache Blogplattform, die auf Firestore basiert. Sie verwenden den Firestore-Emulator, um mit den Sicherheitsregeln Einheitentests auszuführen und dafür zu sorgen, dass die Regeln den erwarteten Zugriff zulassen oder verweigern.

Die folgenden Themen werden behandelt:

  • Detaillierte Berechtigungen gewähren
  • Daten- und Typvalidierungen erzwingen
  • Attributbasierte Zugriffssteuerung implementieren
  • Zugriff anhand der Authentifizierungsmethode gewähren
  • Benutzerdefinierte Funktionen erstellen
  • Zeitbasierte Sicherheitsregeln erstellen
  • Sperrliste und vorläufiges Löschen implementieren
  • Wann Daten denormalisiert werden sollten, um mehreren Zugriffsmustern gerecht zu werden

2. Einrichten

Dies ist eine Blogging-Anwendung. Hier eine allgemeine Zusammenfassung der Anwendungsfunktionen:

Blogpost-Entwürfe:

  • Nutzer können Blogposts erstellen, die sich in der Sammlung drafts befinden.
  • Der Autor kann den Entwurf so lange aktualisieren, bis er zur Veröffentlichung bereit ist.
  • Wenn die Daten veröffentlicht werden können, wird eine Firebase-Funktion ausgelöst, die ein neues Dokument in der Sammlung published erstellt.
  • Entwürfe können vom Autor oder von Websitemoderatoren gelöscht werden.

Veröffentlichte Blogposts:

  • Veröffentlichte Beiträge können nicht von Nutzern erstellt werden, sondern nur über eine Funktion.
  • Sie können nur vorläufig gelöscht werden. Dadurch wird ein visible-Attribut auf „false“ gesetzt.

Kommentare

  • Bei veröffentlichten Beiträgen können Kommentare hinzugefügt werden. Sie sind eine Untersammlung zu jedem veröffentlichten Beitrag.
  • Um Missbrauch zu verhindern, müssen Nutzer eine bestätigte E-Mail-Adresse haben und dürfen nicht gesperrt sein, um einen Kommentar abgeben zu können.
  • Kommentare können nur innerhalb einer Stunde nach ihrer Veröffentlichung aktualisiert werden.
  • Kommentare können vom Verfasser des Kommentars, vom Verfasser des ursprünglichen Beitrags oder von Moderatoren gelöscht werden.

Zusätzlich zu Zugriffsregeln erstellen Sie Sicherheitsregeln, die Pflichtfelder und Datenvalidierungen erzwingen.

Alles läuft lokal mit der Firebase Emulator Suite.

Quellcode abrufen

In diesem Codelab beginnen Sie mit Tests für die Sicherheitsregeln, aber nur minimalen Sicherheitsregeln. Daher müssen Sie zuerst die Quelle klonen, um die Tests auszuführen:

$ git clone https://github.com/FirebaseExtended/codelab-rules.git

Wechseln Sie dann in das Verzeichnis „initial-state“, in dem Sie für den Rest dieses Codelabs arbeiten werden:

$ cd codelab-rules/initial-state

Installieren Sie jetzt die Abhängigkeiten, damit Sie die Tests ausführen können. Bei einer langsameren Internetverbindung kann dies ein bis zwei Minuten dauern:

# Move into the functions directory, install dependencies, jump out.
$ cd functions && npm install && cd -

Firebase CLI abrufen

Die Emulator Suite, die Sie zum Ausführen der Tests verwenden, ist Teil der Firebase CLI (Befehlszeilenschnittstelle), die mit dem folgenden Befehl auf Ihrem Computer installiert werden kann:

$ npm install -g firebase-tools

Prüfen Sie als Nächstes, ob Sie die neueste Version der Befehlszeile haben. Dieses Codelab sollte mit Version 8.4.0 oder höher funktionieren. Höhere Versionen enthalten jedoch weitere Fehlerkorrekturen.

$ firebase --version
9.10.2

3. Tests ausführen

In diesem Abschnitt führen Sie die Tests lokal aus. Jetzt ist es an der Zeit, die Emulator Suite zu starten.

Emulatoren starten

Die Anwendung, mit der Sie arbeiten, enthält drei Haupt-Firestore-Sammlungen: drafts enthält Blogbeiträge, die in Bearbeitung sind, die Sammlung published enthält die veröffentlichten Blogbeiträge und comments ist eine untergeordnete Sammlung für veröffentlichte Beiträge. Das Repository enthält Einheitentests für die Sicherheitsregeln, die die Nutzerattribute und andere Bedingungen definieren, die ein Nutzer zum Erstellen, Lesen, Aktualisieren und Löschen von Dokumenten in den Sammlungen drafts, published und comments benötigt. Sie schreiben die Sicherheitsregeln so, dass diese Tests bestanden werden.

Zuerst ist Ihre Datenbank gesperrt: Lese- und Schreibzugriffe auf die Datenbank werden generell abgelehnt und alle Tests schlagen fehl. Wenn Sie Sicherheitsregeln schreiben, werden die Tests bestanden. Wenn Sie sich die Tests ansehen möchten, öffnen Sie functions/test.js in Ihrem Editor.

Starten Sie die Emulatoren in der Befehlszeile mit emulators:exec und führen Sie die Tests aus:

$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test"

Scrollen Sie in der Ausgabe nach oben:

$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test"
i  emulators: Starting emulators: functions, firestore, hosting
⚠  functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: auth, database, pubsub
⚠  functions: Unable to fetch project Admin SDK configuration, Admin SDK behavior in Cloud Functions emulator may be incorrect.
i  firestore: Importing data from /Users/user/src/firebase/rules-codelab/initial-state/.seed/firestore_export/firestore_export.overall_export_metadata
i  firestore: Firestore Emulator logging to firestore-debug.log
⚠  hosting: Authentication error when trying to fetch your current web app configuration, have you run firebase login?
⚠  hosting: Could not fetch web app configuration and there is no cached configuration on this machine. Check your internet connection and make sure you are authenticated. To continue, you must call firebase.initializeApp({...}) in your code before using Firebase.
i  hosting: Serving hosting files from: public
✔  hosting: Local server: http://localhost:5000
i  functions: Watching "/Users/user/src/firebase/rules-codelab/initial-state/functions" for Cloud Functions...
✔  functions[publishPost]: http function initialized (http://localhost:5001/codelab/us-central1/publishPost).
✔  functions[softDelete]: http function initialized (http://localhost:5001/codelab/us-central1/softDelete).
i  Running script: pushd functions; npm test
~/src/firebase/rules-codelab/initial-state/functions ~/src/firebase/rules-codelab/initial-state

> functions@ test /Users/user/src/firebase/rules-codelab/initial-state/functions
> mocha

(node:76619) ExperimentalWarning: Conditional exports is an experimental feature. This feature could change at any time


  Draft blog posts
    1) can be created with required fields by the author
    2) can be updated by author if immutable fields are unchanged
    3) can be read by the author and moderator

  Published blog posts
    4) can be read by everyone; created or deleted by no one
    5) can be updated by author or moderator

  Comments on published blog posts
    6) can be read by anyone with a permanent account
    7) can be created if email is verfied and not blocked
    8) can be updated by author for 1 hour after creation
    9) can be deleted by an author or moderator


  0 passing (848ms)
  9 failing

...

Derzeit sind 9 Fehler aufgetreten. Während Sie die Regeldatei erstellen, können Sie den Fortschritt messen, indem Sie darauf achten, dass mehr Tests bestanden werden.

4. Entwürfe für Blogposts erstellen

Da sich der Zugriff auf Blogpost-Entwürfe stark vom Zugriff für veröffentlichte Blogposts unterscheidet, speichert diese Blogging-App Blogpost-Entwürfe in einer separaten Sammlung namens /drafts. Nur der Autor oder ein Moderator kann auf Entwürfe zugreifen. Pflichtfelder und unveränderliche Felder können validiert werden.

Wenn Sie die Datei firestore.rules öffnen, finden Sie eine Standardregeldatei:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if false;
    }
  }
}

Die Match-Anweisung match /{document=**} verwendet die Syntax **, um rekursiv auf alle Dokumente in Untersammlungen anzuwenden. Und da es sich auf der obersten Ebene befindet, gilt derzeit dieselbe allgemeine Regel für alle Anfragen, unabhängig davon, wer die Anfrage stellt oder welche Daten sie lesen oder schreiben möchten.

Entfernen Sie zuerst die innerste Abgleichsbeschreibung und ersetzen Sie sie durch match /drafts/{draftID}. Kommentare zur Struktur von Dokumenten können in Regeln hilfreich sein und werden in diesem Codelab verwendet. Sie sind immer optional.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional
    }
  }
}

Mit der ersten Regel, die Sie für Entwürfe erstellen, wird festgelegt, wer die Dokumente erstellen darf. In dieser Anwendung können Entwürfe nur von der Person erstellt werden, die als Autor aufgeführt ist. Prüfen Sie, ob die UID der Person, die die Anfrage stellt, mit der im Dokument aufgeführten UID übereinstimmt.

Die erste Bedingung für die Erstellung lautet:

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

Anschließend können Dokumente nur erstellt werden, wenn sie die drei erforderlichen Felder authorUID, createdAt und title enthalten. Der Nutzer gibt das Feld createdAt nicht an. Dadurch muss die App es hinzufügen, bevor versucht wird, ein Dokument zu erstellen. Da Sie nur prüfen müssen, ob die Attribute erstellt werden, können Sie prüfen, ob request.resource alle diese Schlüssel enthält:

request.resource.data.keys().hasAll([
  "authorUID",
  "createdAt",
  "title"
])

Die letzte Anforderung beim Erstellen eines Blogposts ist, dass der Titel nicht mehr als 50 Zeichen lang sein darf:

request.resource.data.title.size() < 50

Da alle diese Bedingungen wahr sein müssen, können Sie sie mit dem logischen AND-Operator && verknüpfen. Die erste Regel lautet dann:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;
    }
  }
}

Führen Sie die Tests im Terminal noch einmal aus und prüfen Sie, ob der erste Test bestanden wurde.

5. Blogpostentwürfe aktualisieren

Als Nächstes bearbeiten die Autoren die Entwürfe ihrer Blogbeiträge. Erstellen Sie eine Regel für die Bedingungen, unter denen ein Beitrag aktualisiert werden kann. Erstens: Nur der Autor kann seine Entwürfe aktualisieren. Hier prüfen Sie die bereits geschriebene UIDresource.data.authorUID:

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

Die zweite Anforderung für eine Aktualisierung besteht darin, dass sich zwei Attribute, authorUID und createdAt, nicht ändern dürfen:

request.resource.data.diff(resource.data).unchangedKeys().hasAll([
    "authorUID",
    "createdAt"
]);

Der Titel darf maximal 50 Zeichen lang sein:

request.resource.data.title.size() < 50;

Da diese Bedingungen alle erfüllt sein müssen, verketten Sie sie mit &&:

allow update: if
  // User is the author, and
  resource.data.authorUID == request.auth.uid &&
  // `authorUID` and `createdAt` are unchanged
  request.resource.data.diff(resource.data).unchangedKeys().hasAll([
    "authorUID",
    "createdAt"
  ]) &&
  // Title must be < 50 characters long
  request.resource.data.title.size() < 50;

Die vollständigen Regeln sehen dann Folgendes aus:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow update: if
        // User is the author, and
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;
    }
  }
}

Führen Sie die Tests noch einmal aus und prüfen Sie, ob ein anderer Test bestanden wird.

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

Autoren können Entwürfe erstellen und aktualisieren, aber auch löschen.

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

Außerdem können Autoren mit einem isModerator-Attribut als Authentifizierungstoken Entwürfe löschen:

request.auth.token.isModerator == true

Da jede dieser Bedingungen für das Löschen ausreicht, können Sie sie mit dem logischen OR-Operator || verknüpfen:

allow delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true

Für Lesevorgänge gelten dieselben Bedingungen, sodass der Regel eine Berechtigung hinzugefügt werden kann:

allow read, delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true

Die vollständigen Regeln lauten jetzt:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow update: if
        // User is the author, and
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow read, delete: if
        // User is draft author
        resource.data.authorUID == request.auth.uid ||
        // User is a moderator
        request.auth.token.isModerator == true;
    }
  }
}

Führen Sie die Tests noch einmal aus und prüfen Sie, ob ein anderer Test erfolgreich war.

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

Da die Zugriffsmuster für veröffentlichte Beiträge und Beitragsentwürfe sehr unterschiedlich sind, denormalisiert diese App die Beiträge in separate draft- und published-Sammlungen. Beispielsweise können veröffentlichte Beiträge von jedem gelesen, aber nicht endgültig gelöscht werden. Entwürfe können zwar gelöscht werden, aber nur vom Autor und von Moderatoren gelesen werden können. Wenn ein Nutzer in dieser App einen Blogpost-Entwurf veröffentlichen möchte, wird eine Funktion ausgelöst, die den neuen veröffentlichten Beitrag erstellt.

Als Nächstes verfassen Sie die Regeln für veröffentlichte Posts. Die einfachsten Regeln sind, dass veröffentlichte Beiträge von allen gelesen werden können, aber von niemandem erstellt oder gelöscht werden können. Fügen Sie die folgenden Regeln hinzu:

match /published/{postID} {
  // `authorUID`: string, required
  // `content`: string, required
  // `publishedAt`: timestamp, required
  // `title`: string, < 50 characters, required
  // `url`: string, required
  // `visible`: boolean, required

  // Can be read by everyone
  allow read: if true;

  // Published posts are created only via functions, never by users
  // No hard deletes; soft deletes update `visible` field.
  allow create, delete: if false;
}

Wenn sie den bestehenden Regeln hinzugefügt werden, wird die gesamte Regeldatei zu:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow update: if
        // User is the author, and
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow read, delete: if
        // User is draft author
        resource.data.authorUID == request.auth.uid ||
        // User is a moderator
        request.auth.token.isModerator == true;
    }

    match /published/{postID} {
      // `authorUID`: string, required
      // `content`: string, required
      // `publishedAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, required
      // `visible`: boolean, required

      // Can be read by everyone
      allow read: if true;

      // Published posts are created only via functions, never by users
      // No hard deletes; soft deletes update `visible` field.
      allow create, delete: if false;
    }
  }
}

Führen Sie die Tests noch einmal aus und prüfen Sie, ob ein anderer Test bestanden wurde.

8. Veröffentlichte Beiträge aktualisieren: Benutzerdefinierte Funktionen und lokale Variablen

Voraussetzungen für die Aktualisierung eines veröffentlichten Beitrags:

  • Die Änderung kann nur vom Autor oder Moderator vorgenommen werden.
  • Er muss alle erforderlichen Felder enthalten.

Da Sie bereits die Bedingungen für die Tätigkeit als Autor oder Moderator verfasst haben, können Sie diese kopieren und einfügen, aber mit der Zeit könnte es schwierig werden, sie zu lesen und zu pflegen. Stattdessen erstellen Sie eine benutzerdefinierte Funktion, die die Logik für Ihre Rolle als Autor oder Moderator enthält. Dann rufen Sie sie aus mehreren Bedingungen auf.

Benutzerdefinierte Funktion erstellen

Erstellen Sie über der Übereinstimmungsanweisung für Entwürfe eine neue Funktion mit dem Namen isAuthorOrModerator, die als Argumente ein Post-Dokument (dies funktioniert sowohl für Entwürfe oder veröffentlichte Beiträge) als auch das auth-Objekt des Nutzers annimmt:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Returns true if user is post author or a moderator
    function isAuthorOrModerator(post, auth) {

    }

    match /drafts/{postID} {
      allow create: ...
      allow update: ...
      ...
    }

    match /published/{postID} {
      allow read: ...
      allow create, delete: ...
    }
  }
}

Lokale Variablen verwenden

Verwenden Sie innerhalb der Funktion das Schlüsselwort let, um isAuthor- und isModerator-Variablen festzulegen. Alle Funktionen müssen mit einer Rückgabeanweisung enden. Unsere Funktion gibt einen booleschen Wert zurück, der angibt, ob eine der Variablen wahr ist:

function isAuthorOrModerator(post, auth) {
  let isAuthor = auth.uid == post.authorUID;
  let isModerator = auth.token.isModerator == true;
  return isAuthor || isModerator;
}

Funktion aufrufen

Aktualisieren Sie nun die Regel für Entwürfe, um diese Funktion aufzurufen. Achten Sie dabei darauf, resource.data als erstes Argument zu übergeben:

  // Draft blog posts
  match /drafts/{draftID} {
    ...
    // Can be deleted by author or moderator
    allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
  }

Jetzt können Sie eine Bedingung zum Aktualisieren veröffentlichter Beiträge schreiben, die auch die neue Funktion verwendet:

allow update: if isAuthorOrModerator(resource.data, request.auth);

Validierungen hinzufügen

Einige Felder eines veröffentlichten Beitrags sollten nicht geändert werden. Insbesondere die Felder url, authorUID und publishedAt sind unveränderlich. Die anderen beiden Felder title und content sowie visible müssen nach einer Aktualisierung weiterhin vorhanden sein. Fügen Sie Bedingungen hinzu, um diese Anforderungen für Aktualisierungen veröffentlichter Beiträge zu erzwingen:

// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
  "authorUID",
  "publishedAt",
  "url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
  "content",
  "title",
  "visible"
])

Benutzerdefinierte Funktion selbst erstellen

Fügen Sie abschließend eine Bedingung hinzu, die besagt, dass der Titel weniger als 50 Zeichen lang sein darf. Da es sich um eine wiederverwendete Logik handelt, können Sie dazu eine neue Funktion namens titleIsUnder50Chars erstellen. Mit der neuen Funktion ist die Bedingung für die Aktualisierung eines veröffentlichten Beitrags wie folgt:

allow update: if
  isAuthorOrModerator(resource.data, request.auth) &&
  // Immutable fields are unchanged
  request.resource.data.diff(resource.data).unchangedKeys().hasAll([
    "authorUID",
    "publishedAt",
    "url"
  ]) &&
  // Required fields are present
  request.resource.data.keys().hasAll([
    "content",
    "title",
    "visible"
  ]) &&
  titleIsUnder50Chars(request.resource.data);

Die vollständige Regeldatei sieht so aus:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Returns true if user is post author or a moderator
    function isAuthorOrModerator(post, auth) {
      let isAuthor = auth.uid == post.authorUID;
      let isModerator = auth.token.isModerator == true;
      return isAuthor || isModerator;
    }

    function titleIsUnder50Chars(post) {
      return post.title.size() < 50;
    }

    // Draft blog posts
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        titleIsUnder50Chars(request.resource.data);

      allow update: if
        // User is the author, and
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
          ]) &&
        titleIsUnder50Chars(request.resource.data);

      // Can be read or deleted by author or moderator
      allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
    }

    // Published blog posts are denormalized from drafts
    match /published/{postID} {
      // `authorUID`: string, required
      // `content`: string, required
      // `publishedAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, required
      // `visible`: boolean, required

      // Can be read by everyone
      allow read: if true;

      // Published posts are created only via functions, never by users
      // No hard deletes; soft deletes update `visible` field.
      allow create, delete: if false;

      allow update: if
        isAuthorOrModerator(resource.data, request.auth) &&
        // Immutable fields are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "publishedAt",
          "url"
        ]) &&
        // Required fields are present
        request.resource.data.keys().hasAll([
          "content",
          "title",
          "visible"
        ]) &&
        titleIsUnder50Chars(request.resource.data);
    }
  }
}

Führen Sie die Tests noch einmal aus. Sie sollten nun 5 bestandene und 4 fehlgeschlagene Tests haben.

9. Kommentare: Berechtigungen für untergeordnete Sammlungen und Anmeldeanbieter

Für die veröffentlichten Beiträge sind Kommentare zulässig. Die Kommentare werden in einer Untersammlung des veröffentlichten Beitrags (/published/{postID}/comments/{commentID}) gespeichert. Standardmäßig gelten die Regeln einer Sammlung nicht für Untersammlungen. Sie möchten nicht, dass dieselben Regeln wie für das übergeordnete Dokument des veröffentlichten Beitrags auch auf die Kommentare angewendet werden. Sie erstellen dann unterschiedliche Regeln.

Wenn Sie Regeln für den Zugriff auf Kommentare schreiben möchten, beginnen Sie mit der Übereinstimmungsanweisung:

match /published/{postID}/comments/{commentID} {
  // `authorUID`: string, required
  // `comment`: string, < 500 characters, required
  // `createdAt`: timestamp, required
  // `editedAt`: timestamp, optional

Lesen von Kommentaren: Anonymität ist nicht möglich

Bei dieser App können nur Nutzer, die ein dauerhaftes Konto erstellt haben, die Kommentare lesen. Anonyme Konten sind nicht zulässig. Um diese Regel zu erzwingen, suchen Sie nach dem Attribut sign_in_provider, das sich auf jedem auth.token-Objekt befindet:

allow read: if request.auth.token.firebase.sign_in_provider != "anonymous";

Führen Sie die Tests noch einmal aus und prüfen Sie, ob ein weiterer Test bestanden wurde.

Kommentare erstellen: Sperrliste prüfen

Für das Erstellen eines Kommentars gibt es drei Bedingungen:

  • Ein Nutzer muss eine bestätigte E-Mail-Adresse haben.
  • Der Kommentar darf maximal 500 Zeichen lang sein.
  • Sie dürfen nicht auf der Liste der gesperrten Nutzer stehen, die in Firestore in der Sammlung bannedUsers gespeichert ist. Einzelne Bedingungen:
request.auth.token.email_verified == true
request.resource.data.comment.size() < 500
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));

Die letzte Regel beim Erstellen von Kommentaren lautet:

allow create: if
  // User has verified email
  (request.auth.token.email_verified == true) &&
  // UID is not on bannedUsers list
  !(exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));

Die vollständige Regeldatei sieht jetzt so aus:

For bottom of step 9
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Returns true if user is post author or a moderator
    function isAuthorOrModerator(post, auth) {
      let isAuthor = auth.uid == post.authorUID;
      let isModerator = auth.token.isModerator == true;
      return isAuthor || isModerator;
    }

    function titleIsUnder50Chars(post) {
      return post.title.size() < 50;
    }

    // Draft blog posts
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User is author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and createdAt fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        titleIsUnder50Chars(request.resource.data);

      allow update: if
        // User is author
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
          ]) &&
        titleIsUnder50Chars(request.resource.data);

      // Can be read or deleted by author or moderator
      allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
    }

    // Published blog posts are denormalized from drafts
    match /published/{postID} {
      // `authorUID`: string, required
      // `content`: string, required
      // `publishedAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, required
      // `visible`: boolean, required

      // Can be read by everyone
      allow read: if true;

      // Published posts are created only via functions, never by users
      // No hard deletes; soft deletes update `visible` field.
      allow create, delete: if false;

      allow update: if
        isAuthorOrModerator(resource.data, request.auth) &&
        // Immutable fields are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "publishedAt",
          "url"
        ]) &&
        // Required fields are present
        request.resource.data.keys().hasAll([
          "content",
          "title",
          "visible"
        ]) &&
        titleIsUnder50Chars(request.resource.data);
    }

    match /published/{postID}/comments/{commentID} {
      // `authorUID`: string, required
      // `createdAt`: timestamp, required
      // `editedAt`: timestamp, optional
      // `comment`: string, < 500 characters, required

      // Must have permanent account to read comments
      allow read: if !(request.auth.token.firebase.sign_in_provider == "anonymous");

      allow create: if
        // User has verified email
        request.auth.token.email_verified == true &&
        // Comment is under 500 characters
        request.resource.data.comment.size() < 500 &&
        // UID is not on the block list
        !exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
    }
  }
}

Führen Sie die Tests noch einmal aus und prüfen Sie, ob ein weiterer Test bestanden wurde.

10. Kommentare aktualisieren: Zeitbasierte Regeln

Kommentare können vom Verfasser eine Stunde nach dem Erstellen bearbeitet werden. Verwenden Sie dazu den Zeitstempel createdAt.

Prüfen Sie zuerst, ob der Nutzer der Autor ist:

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

Prüfen Sie als Nächstes, ob der Kommentar innerhalb der letzten Stunde erstellt wurde:

(request.time - resource.data.createdAt) < duration.value(1, 'h');

In Kombination mit dem logischen AND-Operator ergibt sich folgende Regel zum Aktualisieren von Kommentaren:

allow update: if
  // is author
  request.auth.uid == resource.data.authorUID &&
  // within an hour of comment creation
  (request.time - resource.data.createdAt) < duration.value(1, 'h');

Wiederholen Sie die Tests und achten Sie darauf, dass mindestens ein Test bestanden wird.

11. Kommentare löschen: Überprüfung der Inhaberschaft des übergeordneten Elements

Kommentare können vom Verfasser, einem Moderator oder dem Autor des Blogposts gelöscht werden.

Erstens: Da die zuvor hinzugefügte Helper-Funktion nach einem authorUID-Feld sucht, das in einem Beitrag oder Kommentar vorhanden sein kann, kannst du die Helper-Funktion wiederverwenden, um zu prüfen, ob der Nutzer der Autor oder Moderator ist:

isAuthorOrModerator(resource.data, request.auth)

Um zu prüfen, ob der Nutzer der Autor des Blogposts ist, kannst du mit einer get den Beitrag in Firestore abrufen:

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

Da jede dieser Bedingungen ausreichend ist, setzen Sie zwischen ihnen einen logischen ODER-Operator:

allow delete: if
  // is comment author or moderator
  isAuthorOrModerator(resource.data, request.auth) ||
  // is blog post author
  request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID;

Führen Sie die Tests noch einmal aus und prüfen Sie, ob ein weiterer Test bestanden wurde.

Und die gesamte Regeldatei ist:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Returns true if user is post author or a moderator
    function isAuthorOrModerator(post, auth) {
      let isAuthor = auth.uid == post.authorUID;
      let isModerator = auth.token.isModerator == true;
      return isAuthor || isModerator;
    }

    function titleIsUnder50Chars(post) {
      return post.title.size() < 50;
    }

    // Draft blog posts
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User is author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and createdAt fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        titleIsUnder50Chars(request.resource.data);

      allow update: if
        // User is author
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
          ]) &&
        titleIsUnder50Chars(request.resource.data);

      // Can be read or deleted by author or moderator
      allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
    }

    // Published blog posts are denormalized from drafts
    match /published/{postID} {
      // `authorUID`: string, required
      // `content`: string, required
      // `publishedAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, required
      // `visible`: boolean, required

      // Can be read by everyone
      allow read: if true;

      // Published posts are created only via functions, never by users
      // No hard deletes; soft deletes update `visible` field.
      allow create, delete: if false;

      allow update: if
        isAuthorOrModerator(resource.data, request.auth) &&
        // Immutable fields are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "publishedAt",
          "url"
        ]) &&
        // Required fields are present
        request.resource.data.keys().hasAll([
          "content",
          "title",
          "visible"
        ]) &&
        titleIsUnder50Chars(request.resource.data);
    }

    match /published/{postID}/comments/{commentID} {
      // `authorUID`: string, required
      // `createdAt`: timestamp, required
      // `editedAt`: timestamp, optional
      // `comment`: string, < 500 characters, required

      // Must have permanent account to read comments
      allow read: if !(request.auth.token.firebase.sign_in_provider == "anonymous");

      allow create: if
        // User has verified email
        request.auth.token.email_verified == true &&
        // Comment is under 500 characters
        request.resource.data.comment.size() < 500 &&
        // UID is not on the block list
        !exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));

      allow update: if
        // is author
        request.auth.uid == resource.data.authorUID &&
        // within an hour of comment creation
        (request.time - resource.data.createdAt) < duration.value(1, 'h');

      allow delete: if
        // is comment author or moderator
        isAuthorOrModerator(resource.data, request.auth) ||
        // is blog post author
        request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID;
    }
  }
}

12. Nächste Schritte

Glückwunsch! Sie haben die Sicherheitsregeln geschrieben, die alle Tests bestanden haben und die Anwendung geschützt haben.

Hier sind ein paar verwandte Themen, mit denen Sie sich als Nächstes beschäftigen können:

  • Blogpost: Sicherheitsregeln zur Codeüberprüfung
  • Codelab: Erste lokale Entwicklung mit den Emulatoren
  • Video: So richten Sie mit GitHub Actions eine CI für emulatorbasierte Tests ein