Chroń swoje dane Firestore za pomocą reguł bezpieczeństwa Firebase

1. Zanim zaczniesz

Cloud Firestore, Cloud Storage for Firebase i Realtime Database korzystają z plików konfiguracyjnych, które piszesz, aby zapewnić dostęp do odczytu i zapisu. Ta konfiguracja, zwana regułami bezpieczeństwa, może również działać jako rodzaj schematu dla Twojej aplikacji. Jest to jedna z najważniejszych części tworzenia aplikacji. Te ćwiczenia z kodowania przeprowadzą Cię przez to.

Warunki wstępne

  • Prosty edytor, taki jak Visual Studio Code, Atom lub Sublime Text
  • Node.js 8.6.0 lub nowszy (aby zainstalować Node.js, użyj nvm ; aby sprawdzić swoją wersję, uruchom node --version )
  • Java 7 lub nowsza (aby zainstalować Javę, skorzystaj z tych instrukcji ; aby sprawdzić swoją wersję, uruchom java -version )

Co zrobisz

Podczas tych zajęć z programowania zabezpieczysz prostą platformę blogową zbudowaną na Firestore. Będziesz używać emulatora Firestore do przeprowadzania testów jednostkowych pod kątem Reguł bezpieczeństwa i upewniania się, że reguły zezwalają i uniemożliwiają oczekiwany dostęp.

Dowiesz się jak:

  • Przyznaj szczegółowe uprawnienia
  • Wymuszaj sprawdzanie poprawności danych i typów
  • Wdrażaj kontrolę dostępu opartą na atrybutach
  • Przyznaj dostęp w oparciu o metodę uwierzytelniania
  • Twórz niestandardowe funkcje
  • Utwórz reguły bezpieczeństwa oparte na czasie
  • Zaimplementuj listę odrzuconych i trwałe usuwanie
  • Dowiedz się, kiedy zdenormalizować dane, aby spełnić wiele wzorców dostępu

2. Skonfiguruj

To jest aplikacja do blogowania. Oto ogólne podsumowanie funkcjonalności aplikacji:

Projekty postów na blogu:

  • Użytkownicy mogą tworzyć wersje robocze postów na blogu, które znajdują się w kolekcji drafts .
  • Autor może nadal aktualizować wersję roboczą, dopóki nie będzie gotowa do publikacji.
  • Kiedy dokument jest gotowy do publikacji, uruchamiana jest funkcja Firebase, która tworzy nowy dokument w published kolekcji.
  • Wersje robocze mogą zostać usunięte przez autora lub moderatorów serwisu

Opublikowane wpisy na blogu:

  • Opublikowane posty nie mogą być tworzone przez użytkowników, jedynie poprzez funkcję.
  • Można je usunąć jedynie w sposób nietrwały, co powoduje aktualizację visible atrybutu do wartości fałszywej.

Uwagi

  • Opublikowane posty umożliwiają komentowanie, które stanowi podkolekcję każdego opublikowanego postu.
  • Aby ograniczyć nadużycia, użytkownicy muszą mieć zweryfikowany adres e-mail i nie znajdować się na liście odmów, aby móc zostawić komentarz.
  • Komentarze można aktualizować jedynie w ciągu godziny od ich opublikowania.
  • Komentarze mogą być usuwane przez autora komentarza, autora oryginalnego wpisu lub przez moderatorów.

Oprócz reguł dostępu utworzysz reguły zabezpieczeń, które wymuszają wymagane pola i sprawdzanie poprawności danych.

Wszystko będzie się działo lokalnie, przy użyciu pakietu Firebase Emulator Suite.

Zdobądź kod źródłowy

W tych ćwiczeniach z programowania zaczniesz od testów Reguł bezpieczeństwa, ale samych reguł bezpieczeństwa, więc pierwszą rzeczą, którą musisz zrobić, to sklonować źródło, aby uruchomić testy:

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

Następnie przejdź do katalogu stanu początkowego, gdzie będziesz pracować przez resztę ćwiczeń z programowania:

$ cd codelab-rules/initial-state

Teraz zainstaluj zależności, aby móc uruchomić testy. Jeśli masz wolniejsze połączenie internetowe, może to zająć minutę lub dwie:

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

Pobierz interfejs wiersza polecenia Firebase

Pakiet emulatorów, którego będziesz używać do uruchamiania testów, jest częścią Firebase CLI (interfejs wiersza poleceń), który można zainstalować na komputerze za pomocą następującego polecenia:

$ npm install -g firebase-tools

Następnie potwierdź, że masz najnowszą wersję interfejsu CLI. To ćwiczenie z programowania powinno działać z wersją 8.4.0 lub wyższą, ale nowsze wersje zawierają więcej poprawek błędów.

$ firebase --version
9.10.2

3. Uruchom testy

W tej sekcji uruchomisz testy lokalnie. Oznacza to, że nadszedł czas, aby uruchomić pakiet emulatorów.

Uruchom emulatory

Aplikacja, z którą będziesz pracować, ma trzy główne kolekcje Firestore: drafts zawierają posty na blogu, które są w toku, kolekcja published zawiera posty na blogu, które zostały opublikowane, a comments stanowią podkolekcję opublikowanych postów. Repozytorium zawiera testy jednostkowe dla Reguł bezpieczeństwa, które definiują atrybuty użytkownika i inne warunki wymagane przez użytkownika do tworzenia, odczytywania, aktualizowania i usuwania dokumentów w kolekcjach drafts , published i comments . Napiszesz reguły bezpieczeństwa, aby te testy przebiegły pomyślnie.

Na początek Twoja baza danych jest zablokowana: odczyty i zapisy w bazie danych są powszechnie blokowane, a wszystkie testy kończą się niepowodzeniem. Podczas pisania reguł bezpieczeństwa testy zakończą się pomyślnie. Aby zobaczyć testy, otwórz w swoim edytorze functions/test.js .

W wierszu poleceń uruchom emulatory za pomocą emulators:exec i uruchom testy:

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

Przewiń na górę wyników:

$ 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

...

W tej chwili jest 9 porażek. Tworząc plik reguł, możesz mierzyć postęp, obserwując przebieg większej liczby testów.

4. Utwórz wersje robocze postów na blogu.

Ponieważ dostęp do wersji roboczych postów na blogu różni się od dostępu do opublikowanych postów na blogu, ta aplikacja do blogowania przechowuje wersje robocze postów na blogu w osobnej kolekcji /drafts . Dostęp do wersji roboczych ma wyłącznie autor lub moderator. Wersje robocze posiadają walidację wymaganych i niezmiennych pól.

Otwierając plik firestore.rules , znajdziesz domyślny plik reguł:

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

Instrukcja dopasowania match /{document=**} używa składni ** do rekursywnego stosowania do wszystkich dokumentów w podkolekcjach. A ponieważ jest to na najwyższym poziomie, obecnie ta sama ogólna zasada dotyczy wszystkich żądań, niezależnie od tego, kto składa żądanie i jakie dane próbuje odczytać lub zapisać.

Zacznij od usunięcia najbardziej wewnętrznej instrukcji dopasowania i zastąpienia jej instrukcją match /drafts/{draftID} . (Komentarze dotyczące struktury dokumentów mogą być pomocne przy tworzeniu reguł i zostaną uwzględnione w tym ćwiczeniu z programowania; są one zawsze opcjonalne).

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

Pierwsza reguła, którą napiszesz dla wersji roboczych, będzie kontrolować, kto może tworzyć dokumenty. W tej aplikacji wersje robocze mogą być tworzone wyłącznie przez osobę wymienioną jako autor. Sprawdź, czy UID osoby składającej wniosek jest taki sam jak UID podany w dokumencie.

Pierwszym warunkiem utworzenia będzie:

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

Następnie dokumenty można tworzyć tylko wtedy, gdy zawierają trzy wymagane pola: authorUID , createdAt i title . (Użytkownik nie podaje pola createdAt ; wymusza to, aby aplikacja musiała je dodać przed próbą utworzenia dokumentu). Ponieważ wystarczy tylko sprawdzić, czy atrybuty są tworzone, możesz sprawdzić, czy request.resource ma wszystkie te klucze:

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

Ostatnim wymogiem tworzenia wpisu na blogu jest to, że tytuł nie może mieć więcej niż 50 znaków:

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

Ponieważ wszystkie te warunki muszą być spełnione, połącz je razem za pomocą operatora logicznego AND oraz && . Pierwsza zasada brzmi:

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

W terminalu uruchom ponownie testy i potwierdź, że pierwszy test zakończył się pomyślnie.

5. Zaktualizuj wersje robocze postów na blogu.

Następnie, gdy autorzy udoskonalą wersje robocze postów na blogu, będą edytować wersje robocze dokumentów. Utwórz regułę określającą warunki, w których można zaktualizować post. Po pierwsze, tylko autor może aktualizować swoje wersje robocze. Pamiętaj, że tutaj sprawdzasz już zapisany UID, resource.data.authorUID :

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

Drugim wymaganiem aktualizacji jest to, że dwa atrybuty, authorUID i createdAt , nie powinny się zmieniać:

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

I wreszcie tytuł powinien mieć maksymalnie 50 znaków:

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

Ponieważ wszystkie te warunki muszą być spełnione, połącz je razem z && :

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;

Pełne zasady stają się:

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

Uruchom ponownie testy i potwierdź, że inny test zakończył się pomyślnie.

6. Usuń i przeczytaj wersje robocze: Kontrola dostępu oparta na atrybutach

Tak jak autorzy mogą tworzyć i aktualizować wersje robocze, mogą także usuwać wersje robocze.

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

Dodatkowo autorzy posiadający atrybut isModerator na swoim tokenie autoryzacji mogą usuwać wersje robocze:

request.auth.token.isModerator == true

Ponieważ którykolwiek z tych warunków jest wystarczający do usunięcia, połącz je za pomocą logicznego operatora OR, || :

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

Te same warunki dotyczą odczytów, więc do reguły można dodać uprawnienia:

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

Pełne zasady są teraz:

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

Uruchom ponownie testy i potwierdź, że kolejny test został pomyślnie zakończony.

7. Odczytuje, tworzy i usuwa opublikowane posty: denormalizacja dla różnych wzorców dostępu

Ponieważ wzorce dostępu do opublikowanych postów i wersji roboczych postów są tak różne, ta aplikacja denormalizuje posty w osobne kolekcje draft i published . Na przykład opublikowane posty mogą być czytane przez każdego, ale nie można ich trwale usunąć, natomiast wersje robocze można usuwać, ale mogą je czytać tylko autor i moderatorzy. W tej aplikacji, gdy użytkownik chce opublikować wersję roboczą wpisu na blogu, uruchamiana jest funkcja, która utworzy nowy opublikowany post.

Następnie napiszesz zasady dotyczące publikowanych postów. Najprostsza zasada pisania jest taka, że ​​opublikowane posty może przeczytać każdy i nikt nie może ich tworzyć ani usuwać. Dodaj te reguły:

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

Dodając je do istniejących reguł, cały plik reguł stanie się:

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

Uruchom ponownie testy i potwierdź, że inny test zakończył się pomyślnie.

8. Aktualizacja opublikowanych postów: Funkcje niestandardowe i zmienne lokalne

Warunki aktualizacji opublikowanego postu są następujące:

  • może to zrobić wyłącznie autor lub moderator, oraz
  • musi zawierać wszystkie wymagane pola.

Ponieważ masz już napisane warunki bycia autorem lub moderatorem, możesz je skopiować i wkleić, ale z czasem mogą one stać się trudne do odczytania i utrzymania. Zamiast tego utworzysz niestandardową funkcję, która będzie zawierać logikę bycia autorem lub moderatorem. Następnie wywołasz to z wielu warunków.

Utwórz funkcję niestandardową

Nad instrukcją dopasowania dla wersji roboczych utwórz nową funkcję o nazwie isAuthorOrModerator , która przyjmuje jako argumenty dokument pocztowy (będzie to działać zarówno w przypadku wersji roboczych, jak i opublikowanych postów) oraz obiekt autoryzacji użytkownika:

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: ...
    }
  }
}

Użyj zmiennych lokalnych

Wewnątrz funkcji użyj słowa kluczowego let , aby ustawić zmienne isAuthor i isModerator . Wszystkie funkcje muszą kończyć się instrukcją return, a nasza zwróci wartość logiczną wskazującą, czy którakolwiek ze zmiennych jest prawdziwa:

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

Wywołaj funkcję

Teraz zaktualizujesz regułę dla wersji roboczych, aby wywoływała tę funkcję, uważając, aby przekazać resource.data jako pierwszy argument:

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

Teraz możesz napisać warunek aktualizacji opublikowanych postów, który również korzysta z nowej funkcji:

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

Dodaj walidacje

Niektórych pól opublikowanego posta nie należy zmieniać, w szczególności url , authorUID i publishedAt są niezmienne. Pozostałe dwa pola, title i content oraz visible , muszą być nadal obecne po aktualizacji. Dodaj warunki, aby wymusić te wymagania dotyczące aktualizacji opublikowanych postów:

// 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"
])

Utwórz własną funkcję niestandardową

Na koniec dodaj warunek, aby tytuł miał mniej niż 50 znaków. Ponieważ jest to logika ponownie wykorzystana, można to zrobić, tworząc nową funkcję titleIsUnder50Chars . Dzięki nowej funkcji warunkiem aktualizacji opublikowanego wpisu staje się:

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

A pełny plik reguł to:

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

Uruchom ponownie testy. W tym momencie powinieneś mieć 5 pozytywnych testów i 4 niezaliczone.

9. Komentarze: Podkolekcje i uprawnienia dostawcy logowania

Opublikowane posty umożliwiają komentowanie, a komentarze są przechowywane w podkolekcji opublikowanego postu ( /published/{postID}/comments/{commentID} ). Domyślnie reguły kolekcji nie mają zastosowania do podkolekcji. Nie chcesz, aby te same zasady, które mają zastosowanie do dokumentu nadrzędnego opublikowanego postu, miały zastosowanie do komentarzy; stworzysz różne.

Aby napisać reguły dostępu do komentarzy, zacznij od instrukcji match:

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

Czytanie komentarzy: Nie mogę być anonimowy

W przypadku tej aplikacji komentarze mogą czytać tylko użytkownicy, którzy utworzyli konto stałe, a nie konto anonimowe. Aby wymusić tę regułę, sprawdź atrybut sign_in_provider znajdujący się w każdym obiekcie auth.token :

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

Uruchom ponownie testy i potwierdź, że jeszcze jeden test zakończył się pomyślnie.

Tworzenie komentarzy: Sprawdzanie listy odrzuconych

Istnieją trzy warunki utworzenia komentarza:

  • użytkownik musi mieć zweryfikowany adres e-mail
  • komentarz musi mieć mniej niż 500 znaków oraz
  • nie mogą znajdować się na liście zbanowanych użytkowników, która jest przechowywana w Firestore w kolekcji bannedUsers . Biorąc te warunki pojedynczo:
request.auth.token.email_verified == true
request.resource.data.comment.size() < 500
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));

Ostatnia zasada tworzenia komentarzy jest następująca:

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

Cały plik reguł ma teraz postać:

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

Uruchom ponownie testy i upewnij się, że jeszcze jeden test przebiegł pomyślnie.

10. Aktualizacja komentarzy: Reguły czasowe

Logika biznesowa komentarzy jest taka, że ​​autor komentarza może je edytować przez godzinę po utworzeniu. Aby to zaimplementować, użyj znacznika czasu createdAt .

Po pierwsze, aby ustalić, że użytkownik jest autorem:

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

Następnie, że komentarz powstał w ciągu ostatniej godziny:

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

Łącząc je z operatorem logicznym AND, reguła aktualizacji komentarzy wygląda następująco:

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

Uruchom ponownie testy i upewnij się, że jeszcze jeden test przebiegł pomyślnie.

11. Usuwanie komentarzy: sprawdzanie własności nadrzędnej

Komentarze mogą być usuwane przez autora komentarza, moderatora lub autora wpisu na blogu.

Po pierwsze, ponieważ dodana wcześniej funkcja pomocnicza sprawdza, czy pole authorUID może istnieć w poście lub komentarzu, możesz ponownie użyć funkcji pomocniczej, aby sprawdzić, czy użytkownik jest autorem lub moderatorem:

isAuthorOrModerator(resource.data, request.auth)

Aby sprawdzić, czy użytkownik jest autorem wpisu na blogu, użyj polecenia get , aby wyszukać post w Firestore:

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

Ponieważ którykolwiek z tych warunków jest wystarczający, użyj między nimi logicznego operatora OR:

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;

Uruchom ponownie testy i upewnij się, że jeszcze jeden test przebiegł pomyślnie.

A cały plik reguł to:

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. Kolejne kroki

Gratulacje! Napisałeś Zasady Bezpieczeństwa, które przeszły pomyślnie wszystkie testy i zabezpieczyły aplikację!

Oto kilka powiązanych tematów, którymi warto się zająć w następnej kolejności:

  • Post na blogu : Jak przeglądać kod Reguły bezpieczeństwa
  • Laboratorium kodowania : spacer po pierwszym lokalnym rozwoju z emulatorami
  • Wideo : jak używać konfiguracji CI do testów opartych na emulatorze przy użyciu akcji GitHub