1. Zanim zaczniesz
Cloud Firestore, Cloud Storage dla Firebase i Realtime Database opierają się na plikach konfiguracyjnych, które piszesz, aby przyznać 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. To jedna z najważniejszych części tworzenia aplikacji. A to laboratorium kodów przeprowadzi Cię przez to.
Wymagania 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ę, użyj tych instrukcji ; aby sprawdzić swoją wersję, uruchom
java -version
)
Co zrobisz
W tym laboratorium kodowania zabezpieczysz prostą platformę blogową opartą 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 nie zezwalają na oczekiwany dostęp.
Dowiesz się, jak:
- Przyznaj szczegółowe uprawnienia
- Wymuszaj walidacje danych i typów
- Zaimplementuj kontrolę dostępu opartą na atrybutach
- Udziel dostępu na podstawie metody uwierzytelniania
- Twórz niestandardowe funkcje
- Twórz reguły bezpieczeństwa oparte na czasie
- Zaimplementuj listę odmów i miękkie 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:
Wersje robocze postów na blogu:
- Użytkownicy mogą tworzyć wersje robocze postów na blogu, które znajdują się w kolekcji
drafts
. - Autor może aktualizować wersję roboczą, dopóki nie będzie gotowa do opublikowania.
- Gdy jest gotowy do opublikowania, uruchamiana jest funkcja Firebase, która tworzy nowy dokument w
published
kolekcji. - Wersje robocze mogą być usuwane przez autora lub przez moderatorów serwisu
Opublikowane wpisy na blogu:
- Opublikowane posty nie mogą być tworzone przez użytkowników, tylko za pomocą funkcji.
- Można je tylko usunąć nietrwałie, co powoduje zaktualizowanie
visible
atrybutu na wartość false.
Uwagi
- Opublikowane posty umożliwiają komentarze, które są zbiorem podrzędnym każdego opublikowanego posta.
- Aby ograniczyć nadużycia, użytkownicy muszą mieć zweryfikowany adres e-mail i nie być na liście odmów, aby móc zostawić komentarz.
- Komentarze można aktualizować tylko w ciągu godziny od ich opublikowania.
- Komentarze mogą być usuwane przez autora komentarza, autora oryginalnego postu lub przez moderatorów.
Oprócz reguł dostępu utworzysz reguły bezpieczeństwa, które wymuszają wymagane pola i sprawdzanie poprawności danych.
Wszystko będzie się działo lokalnie, przy użyciu Firebase Emulator Suite.
Pobierz kod źródłowy
W tym laboratorium kodowania 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, w którym będziesz pracować przez pozostałą część tego modułu:
$ cd codelab-rules/initial-state
Teraz zainstaluj zależności, aby móc uruchomić testy. Jeśli korzystasz z wolniejszego połączenia internetowego, może to potrwać 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 (interfejsu 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 laboratorium kodów powinno działać z wersją 8.4.0 lub nowszą, 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 opublikowane posty na blogu, a comments
to podzbiór opublikowanych postów. Repozytorium jest dostarczane z testami jednostkowymi reguł bezpieczeństwa, które definiują atrybuty użytkownika i inne warunki wymagane, aby użytkownik mógł tworzyć, czytać, aktualizować i usuwać dokumenty w kolekcjach drafts
, published
i comments
. Napiszesz zasady bezpieczeństwa, aby te testy przeszły pomyślnie.
Na początek twoja baza danych jest zablokowana: odczyty i zapisy w bazie danych są powszechnie odrzucane, a wszystkie testy kończą się niepowodzeniem. Gdy napiszesz zasady bezpieczeństwa, testy przejdą pomyślnie. Aby zobaczyć testy, otwórz functions/test.js
w swoim edytorze.
W wierszu poleceń uruchom emulatory za pomocą emulators:exec
i uruchom testy:
$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test"
Przewiń do góry danych wyjściowych:
$ 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 awarii. Tworząc plik reguł, możesz mierzyć postępy, obserwując, jak przechodzi kolejne testy.
4. Twórz wersje robocze postów na blogu.
Ponieważ dostęp do wersji roboczych postów na blogu różni się znacznie od dostępu do opublikowanych postów na blogu, ta aplikacja do blogowania przechowuje wersje robocze postów na blogu w osobnej kolekcji /drafts
. Wersje robocze są dostępne tylko dla autora lub moderatora i mają walidacje 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 **
w celu rekurencyjnego zastosowania do wszystkich dokumentów w podkolekcjach. A ponieważ jest na najwyższym poziomie, w tej chwili ta sama ogólna zasada dotyczy wszystkich żądań, bez względu na to, kto składa żądanie lub jakie dane próbuje odczytać lub zapisać.
Zacznij od usunięcia najbardziej wewnętrznej instrukcji match i zastąpienia jej match /drafts/{draftID}
. (Komentarze dotyczące struktury dokumentów mogą być pomocne w regułach i zostaną uwzględnione w tym laboratorium kodowania; są 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 określać, kto może tworzyć dokumenty. W tej aplikacji wersje robocze mogą być tworzone wyłącznie przez osobę wymienioną jako autor. Sprawdź, czy identyfikator UID osoby składającej wniosek jest taki sam, jak w dokumencie.
Pierwszym warunkiem utworzenia będzie:
request.resource.data.authorUID == request.auth.uid
Następnie dokumenty mogą być tworzone tylko wtedy, gdy zawierają trzy wymagane pola, authorUID
, createdAt
i title
. (Użytkownik nie podaje pola createdAt
; oznacza to, że aplikacja musi je dodać przed próbą utworzenia dokumentu). Ponieważ wystarczy sprawdzić, czy tworzone są atrybuty, można sprawdzić, czy request.resource
zawiera wszystkie te klucze:
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
])
Ostatnim wymogiem tworzenia posta 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 z operatorem logicznym AND, &&
. Pierwsza reguła staje 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;
}
}
}
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ą swoje wersje robocze postów na blogu, dokonają edycji wersji roboczych dokumentów. Utwórz regułę dla warunków, w których post może zostać zaktualizowany. Po pierwsze, tylko autor może aktualizować swoje wersje robocze. Zauważ, że tutaj sprawdzasz UID, który został już napisany, 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 na koniec 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 kolejny 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ą również usuwać wersje robocze.
resource.data.authorUID == request.auth.uid
Ponadto autorzy z atrybutem isModerator
na swoim tokenie autoryzacji mogą usuwać wersje robocze:
request.auth.token.isModerator == true
Ponieważ każdy z tych warunków jest wystarczający do usunięcia, połącz je operatorem logicznym OR, ||
:
allow delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true
Te same warunki dotyczą odczytów, więc można dodać uprawnienie do reguły:
allow read, delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true
Pełne zasady są teraz następujące:
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;
}
}
}
Ponownie uruchom testy i potwierdź, że kolejny test zakończył się pomyślnie.
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 na oddzielne draft
i published
kolekcje. Na przykład opublikowane posty mogą być czytane przez wszystkich, ale nie można ich trwale usunąć, podczas gdy wersje robocze można usuwać, ale tylko autor i moderatorzy mogą je czytać. W tej aplikacji, gdy użytkownik chce opublikować wersję roboczą posta na blogu, uruchamiana jest funkcja, która utworzy nowy opublikowany post.
Następnie napiszesz zasady dotyczące publikowanych postów. Najprostszą zasadą pisania jest to, że opublikowane posty mogą być czytane przez każdego 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ł staje 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 kolejny test zakończył się pomyślnie.
8. Aktualizacja opublikowanych postów: Funkcje niestandardowe i zmienne lokalne
Warunkiem aktualizacji opublikowanego postu jest:
- może to zrobić tylko 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 może to stać się trudne do odczytania i utrzymania. Zamiast tego utworzysz niestandardową funkcję, która zawiera logikę bycia autorem lub moderatorem. Następnie wywołasz to z wielu warunków.
Utwórz niestandardową funkcję
Nad instrukcją dopasowania dla wersji roboczych utwórz nową funkcję o nazwie isAuthorOrModerator
, która przyjmuje jako argumenty dokument postu (to zadziała zarówno w przypadku wersji roboczych, jak i opublikowanych postów) oraz obiekt auth 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óraś 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 wyegzekwować 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ą
I na koniec dodaj warunek, aby tytuł miał mniej niż 50 znaków. Ponieważ jest to ponownie używana logika, możesz to zrobić, tworząc nową funkcję, titleIsUnder50Chars
. Dzięki nowej funkcji warunkiem aktualizacji opublikowanego posta jest:
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);
}
}
}
Ponownie uruchom testy. W tym momencie powinieneś mieć 5 zdanych testów i 4 nieudane.
9. Komentarze: podzbiory i uprawnienia dostawcy logowania
Opublikowane posty umożliwiają komentowanie, a komentarze są przechowywane w podzbiorze opublikowanego posta ( /published/{postID}/comments/{commentID}
). Domyślnie reguły kolekcji nie mają zastosowania do podkolekcji. Nie chcesz, aby te same zasady, które dotyczą dokumentu nadrzędnego opublikowanego postu, dotyczyły komentarzy; będziesz tworzyć różne.
Aby napisać zasady dostępu do komentarzy, zacznij od wyrażenia match:
match /published/{postID}/comments/{commentID} {
// `authorUID`: string, required
// `comment`: string, < 500 characters, required
// `createdAt`: timestamp, required
// `editedAt`: timestamp, optional
Czytanie komentarzy: Nie może 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łę, wyszukaj atrybut sign_in_provider
znajdujący się w każdym obiekcie auth.token
:
allow read: if request.auth.token.firebase.sign_in_provider != "anonymous";
Ponownie uruchom 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 jeden po drugim:
request.auth.token.email_verified == true
request.resource.data.comment.size() < 500
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
Ostateczna zasada tworzenia komentarzy to:
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ł to teraz:
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));
}
}
}
Uruchom ponownie testy i upewnij się, że jeszcze jeden test zakończył się pomyślnie.
10. Aktualizacja komentarzy: Reguły czasowe
Logika biznesowa komentarzy polega na tym, że mogą one być edytowane przez autora komentarza 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 został utworzony 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 zakończył się pomyślnie.
11. Usuwanie komentarzy: sprawdzanie własności rodzica
Komentarze może usuwać autor komentarza, moderator lub autor wpisu na blogu.
Po pierwsze, ponieważ dodana wcześniej funkcja pomocnicza sprawdza pole authorUID
, które może istnieć w poście lub komentarzu, możesz ponownie użyć funkcji pomocniczej, aby sprawdzić, czy użytkownik jest autorem czy moderatorem:
isAuthorOrModerator(resource.data, request.auth)
Aby sprawdzić, czy użytkownik jest autorem posta na blogu, użyj opcji get
, aby wyszukać post w Firestore:
request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID
Ponieważ dowolny z tych warunków jest wystarczający, użyj między nimi operatora logicznego 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 zakończył się 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 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. Kolejne kroki
Gratulacje! Napisałeś zasady bezpieczeństwa, dzięki którym wszystkie testy przeszły pomyślnie i zabezpieczyłeś aplikację!
Oto kilka powiązanych tematów, które warto omówić w następnej kolejności:
- Wpis na blogu : Jak dokonać przeglądu kodu Zasady bezpieczeństwa
- Codelab : przechodzenie przez lokalny pierwszy rozwój z emulatorami
- Wideo : jak używać konfigurowania CI do testów opartych na emulatorach przy użyciu akcji usługi GitHub