Schützen Sie Ihre Daten mit Firestore-Sicherheitsregeln

1. Bevor Sie beginnen

Cloud Firestore, Cloud Storage for Firebase und die Echtzeitdatenbank verlassen sich 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 fungieren. Dies ist einer der wichtigsten Teile bei der Entwicklung Ihrer Anwendung. Und dieses Codelab wird Sie dabei begleiten.

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 )

Was du tun wirst

In diesem Codelab sichern Sie eine einfache Blog-Plattform, die auf Firestore basiert. Sie verwenden den Firestore-Emulator, um Einheitentests für die Sicherheitsregeln auszuführen und sicherzustellen, dass die Regeln den erwarteten Zugriff zulassen oder verbieten.

Sie lernen, wie Sie:

  • Gewähren Sie granulare 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 Ablehnungsliste und vorläufige Löschungen
  • Verstehen Sie, wann Daten denormalisiert werden müssen, um mehrere Zugriffsmuster zu erfüllen

2. Einrichten

Dies ist eine Blogging-Anwendung. Hier ist eine Zusammenfassung der Anwendungsfunktionalität auf hoher Ebene:

Blogbeiträge entwerfen:

  • Benutzer können drafts von Blogbeiträgen erstellen, die in der Entwurfssammlung enthalten sind.
  • Der Autor kann einen Entwurf weiterhin 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.

Bemerkungen

  • Veröffentlichte Posts erlauben Kommentare, die eine Untersammlung zu jedem veröffentlichten Post darstellen.
  • Um Missbrauch zu reduzieren, müssen Benutzer eine verifizierte E-Mail-Adresse haben und dürfen nicht auf einem Leugner sein, um einen Kommentar zu hinterlassen.
  • 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, aber minimale Sicherheitsregeln selbst, also müssen Sie als erstes die Quelle klonen, um die Tests auszuführen:

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

Wechseln Sie dann in das Ausgangsverzeichnis, 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 verwenden, 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, aber spätere Versionen enthalten mehr Fehlerbehebungen.

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

Starten Sie die Emulatoren

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

Zu Beginn ist Ihre Datenbank gesperrt: Lese- und Schreibzugriffe auf die Datenbank werden allgemein verweigert, und alle Tests schlagen fehl. Während Sie Sicherheitsregeln schreiben, werden die Tests bestanden. Um die Tests anzuzeigen, öffnen 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 sind 9 Fehler vorhanden. Während Sie die Regeldatei erstellen, können Sie den Fortschritt messen, indem Sie beobachten, wie mehr Tests bestanden werden.

4. Erstellen Sie Blogpost-Entwürfe.

Da sich der Zugriff auf Blogpostentwürfe so sehr vom Zugriff auf veröffentlichte Blogposts unterscheidet, speichert diese Blogging-App Blogpostentwürfe in einer separaten Sammlung, /drafts . Auf Entwürfe kann nur vom Autor oder einem Moderator zugegriffen werden, und es gibt 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 weil es sich auf der obersten Ebene befindet, gilt im Moment dieselbe pauschale Regel für alle Anfragen, unabhängig davon, wer die Anfrage stellt oder welche Daten er zu lesen oder zu schreiben versucht.

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 dieser Anwendung 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 stellt, dieselbe UID ist, die im Dokument aufgeführt ist.

Die erste Bedingung für die Erstellung wird sein:

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, request.resource all hat 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 diese 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 bestanden wurde.

5. Blogpost-Entwürfe aktualisieren.

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

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

Die zweite Voraussetzung für ein Update ist, dass sich zwei Attribute, authorUID und createdAt , nicht ändern sollten:

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 werden 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;
    }
  }
}

Führen Sie Ihre Tests erneut aus und bestätigen Sie, dass ein weiterer Test bestanden wird.

6. Entwürfe löschen und lesen: Attribute Based Access Control

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

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

Außerdem dürfen Autoren mit einem isModerator -Attribut auf 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, damit 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 aus und bestätigen Sie, dass ein weiterer Test nun bestanden wurde.

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

Da die Zugriffsmuster für veröffentlichte Posts und Postentwürfe so unterschiedlich sind, denormalisiert diese App die Posts in separate Sammlungen von draft und published Posts. Beispielsweise können veröffentlichte Beiträge von jedem gelesen, aber nicht hart gelöscht werden, während Entwürfe gelöscht, aber nur vom Autor und 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 Posts. Die einfachsten Regeln zum 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 bestanden wird.

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

Die Bedingungen zum Aktualisieren eines veröffentlichten Beitrags sind:

  • es kann nur vom Autor oder Moderator durchgeführt werden, und
  • 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 das Lesen und Pflegen schwierig werden. Stattdessen erstellen Sie eine benutzerdefinierte Funktion, die die Logik kapselt, um entweder Autor oder Moderator zu sein. Dann rufen Sie es aus 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 Beitragsdokument (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 . 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, wobei Sie darauf achten, 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 für die Aktualisierung veröffentlichter Beiträge schreiben, die ebenfalls 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 publishedAt Felder url , authorUID und publishAt sind unveränderlich. Die beiden anderen Felder title und content sowie visible müssen auch nach einem Update noch 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

Fügen Sie schließlich eine Bedingung hinzu, dass der Titel weniger als 50 Zeichen lang ist. Da es sich um wiederverwendete Logik handelt, könnten Sie dies tun, indem Sie eine neue Funktion titleIsUnder50Chars . Mit der neuen Funktion wird die Bedingung für die Aktualisierung eines veröffentlichten Beitrags zu:

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 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 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. An diesem Punkt sollten Sie 5 bestandene Tests und 4 nicht bestandene Tests haben.

9. Kommentare: Untersammlungen und Anmeldeanbieterberechtigungen

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

Lesen von Kommentaren: Kann nicht anonym sein

Für diese App können nur Benutzer, die ein permanentes Konto erstellt haben, kein anonymes Konto, die Kommentare lesen. Um diese Regel durchzusetzen, suchen Sie das sign_in_provider -Attribut, das sich auf jedem auth.token -Objekt befindet:

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

Führen Sie Ihre Tests erneut aus und bestätigen Sie, dass ein weiterer Test bestanden wurde.

Kommentare erstellen: Überprüfung einer Ablehnungsliste

Es gibt drei Bedingungen für das Erstellen eines Kommentars:

  • Ein Benutzer muss über eine verifizierte E-Mail-Adresse verfügen
  • der Kommentar muss weniger als 500 Zeichen lang sein, und
  • Sie können nicht auf einer Liste gesperrter Benutzer stehen, die in Firestore in der Sammlung bannedUsers gespeichert ist. Nehmen Sie 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 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 ist 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 charachters
        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 bestanden wird.

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

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, wird die Regel zum Aktualisieren von Kommentaren zu:

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 bestanden wird.

11. Löschen von Kommentaren: Prüfung auf übergeordneten Besitz

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 authorUID -Feld 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 Blogposts ist, suchen Sie den Post in Firestore mit get nach:

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

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

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 charachters
        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ückwünsche! 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 können:

  • Blog-Beitrag : Wie man Sicherheitsregeln überprüft
  • Codelab : Gehen Sie mit den Emulatoren durch die lokale Erstentwicklung
  • Video : Verwendung von Einrichtungs-CI für emulatorbasierte Tests mit GitHub-Aktionen