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