Ta strona rozwija koncepcje przedstawione w artykułach Strukturyzowanie reguł zabezpieczeń i Pisanie warunków dla reguł zabezpieczeń, aby wyjaśnić, jak za pomocą Cloud Firestore Security Rules tworzyć reguły, które umożliwiają klientom wykonywanie operacji na niektórych polach w dokumencie, ale nie na innych.
Czasami możesz chcieć kontrolować zmiany w dokumencie nie na poziomie dokumentu, ale na poziomie pola.
Możesz na przykład zezwolić klientowi na tworzenie i zmienianie dokumentu, ale nie zezwalać mu na edytowanie niektórych pól w tym dokumencie. Możesz też wymagać, aby każdy dokument tworzony przez klienta zawsze zawierał określony zestaw pól. Z tego przewodnika dowiesz się, jak wykonać niektóre z tych zadań za pomocą Cloud Firestore Security Rules.
Zezwalanie na dostęp do odczytu tylko w przypadku określonych pól
Odczyty w Cloud Firestore są wykonywane na poziomie dokumentu. Możesz pobrać cały dokument lub nie pobrać nic. Nie można pobrać części dokumentu. Nie można używać samych reguł zabezpieczeń, aby uniemożliwić użytkownikom odczytywanie określonych pól w dokumencie.
Jeśli w dokumencie są pola, które chcesz ukryć przed niektórymi użytkownikami, najlepiej umieścić je w osobnym dokumencie. Możesz na przykład utworzyć dokument w private
podkolekcji w ten sposób:
/employees/{emp_id}
name: "Alice Hamilton",
department: 461,
start_date: <timestamp>
/employees/{emp_id}/private/finances
salary: 80000,
bonus_mult: 1.25,
perf_review: 4.2
Następnie możesz dodać reguły bezpieczeństwa, które będą miały różne poziomy dostępu dla tych dwóch kolekcji. W tym przykładzie używamy niestandardowych roszczeń autoryzacyjnych, aby określić, że tylko użytkownicy z niestandardowym roszczeniem autoryzacyjnym role
równym Finance
mogą wyświetlać informacje finansowe pracownika.
service cloud.firestore {
match /databases/{database}/documents {
// Allow any logged in user to view the public employee data
match /employees/{emp_id} {
allow read: if request.resource.auth != null
// Allow only users with the custom auth claim of "Finance" to view
// the employee's financial data
match /private/finances {
allow read: if request.resource.auth &&
request.resource.auth.token.role == 'Finance'
}
}
}
}
Ograniczanie pól podczas tworzenia dokumentu
Cloud Firestore nie ma schematu, co oznacza, że na poziomie bazy danych nie ma ograniczeń dotyczących pól, które może zawierać dokument. Ta elastyczność może ułatwiać tworzenie aplikacji, ale czasami warto zadbać o to, aby klienci mogli tworzyć tylko dokumenty zawierające określone pola lub nie mogli tworzyć dokumentów zawierających inne pola.
Możesz je utworzyć, sprawdzając metodę keys
obiektu request.resource.data
. To lista wszystkich pól, w których klient próbuje zapisać dane w tym nowym dokumencie. Łącząc ten zestaw pól z funkcjami takimi jak hasOnly()
lub hasAny()
, możesz dodać logikę, która ogranicza typy dokumentów, jakie użytkownik może dodawać do Cloud Firestore.
Wymaganie określonych pól w nowych dokumentach
Załóżmy, że chcesz mieć pewność, że wszystkie dokumenty utworzone w kolekcji restaurant
zawierają co najmniej pola name
, location
i city
. Możesz to zrobić, wywołując hasAll()
na liście kluczy w nowym dokumencie.
service cloud.firestore {
match /databases/{database}/documents {
// Allow the user to create a document only if that document contains a name
// location, and city field
match /restaurant/{restId} {
allow create: if request.resource.data.keys().hasAll(['name', 'location', 'city']);
}
}
}
Umożliwia to tworzenie restauracji z użyciem innych pól, ale zapewnia, że wszystkie dokumenty utworzone przez klienta zawierają co najmniej te 3 pola.
Blokowanie określonych pól w nowych dokumentach
Podobnie możesz uniemożliwić klientom tworzenie dokumentów zawierających określone pola, używając tagu hasAny()
w odniesieniu do listy zabronionych pól. Ta metoda zwraca wartość „true”, jeśli dokument zawiera którekolwiek z tych pól, więc prawdopodobnie będziesz chcieć zanegować wynik, aby zabronić używania określonych pól.
Na przykład w poniższym przykładzie klienci nie mogą tworzyć dokumentu zawierającego pole average_score
ani rating_count
, ponieważ te pola zostaną dodane przez wywołanie serwera w późniejszym czasie.
service cloud.firestore {
match /databases/{database}/documents {
// Allow the user to create a document only if that document does *not*
// contain an average_score or rating_count field.
match /restaurant/{restId} {
allow create: if (!request.resource.data.keys().hasAny(
['average_score', 'rating_count']));
}
}
}
Tworzenie listy dozwolonych pól w przypadku nowych dokumentów
Zamiast zabraniać używania określonych pól w nowych dokumentach, możesz utworzyć listę tylko tych pól, które są w nich wyraźnie dozwolone. Następnie możesz użyć funkcji hasOnly()
, aby mieć pewność, że wszystkie nowe dokumenty będą zawierać tylko te pola (lub ich podzbiór), a nie inne.
service cloud.firestore {
match /databases/{database}/documents {
// Allow the user to create a document only if that document doesn't contain
// any fields besides the ones listed below.
match /restaurant/{restId} {
allow create: if (request.resource.data.keys().hasOnly(
['name', 'location', 'city', 'address', 'hours', 'cuisine']));
}
}
}
Łączenie pól wymaganych i opcjonalnych
Możesz łączyć operacje hasAll
i hasOnly
w regułach bezpieczeństwa, aby wymagać niektórych pól i zezwalając na inne. Na przykład ten przykład wymaga, aby wszystkie nowe dokumenty zawierały pola name
, location
i city
, a opcjonalnie dopuszcza pola address
, hours
i cuisine
.
service cloud.firestore {
match /databases/{database}/documents {
// Allow the user to create a document only if that document has a name,
// location, and city field, and optionally address, hours, or cuisine field
match /restaurant/{restId} {
allow create: if (request.resource.data.keys().hasAll(['name', 'location', 'city'])) &&
(request.resource.data.keys().hasOnly(
['name', 'location', 'city', 'address', 'hours', 'cuisine']));
}
}
}
W rzeczywistości możesz przenieść tę logikę do funkcji pomocniczej, aby uniknąć duplikowania kodu i łatwiej łączyć pola opcjonalne i wymagane w jedną listę, jak w tym przykładzie:
service cloud.firestore {
match /databases/{database}/documents {
function verifyFields(required, optional) {
let allAllowedFields = required.concat(optional);
return request.resource.data.keys().hasAll(required) &&
request.resource.data.keys().hasOnly(allAllowedFields);
}
match /restaurant/{restId} {
allow create: if verifyFields(['name', 'location', 'city'],
['address', 'hours', 'cuisine']);
}
}
}
Ograniczanie pól podczas aktualizacji
Powszechną praktyką w zakresie bezpieczeństwa jest zezwalanie klientom na edytowanie tylko niektórych pól. Nie możesz tego zrobić, patrząc tylko na listę request.resource.data.keys()
opisaną w poprzedniej sekcji, ponieważ ta lista przedstawia cały dokument po aktualizacji i zawiera pola, których klient nie zmienił.
Jeśli jednak użyjesz funkcji diff()
, możesz porównać request.resource.data
z obiektem resource.data
, który reprezentuje dokument w bazie danych przed aktualizacją. Spowoduje to utworzenie obiektu mapDiff
, który zawiera wszystkie zmiany między dwiema różnymi mapami.
Wywołując metodę affectedKeys()
na tym obiekcie mapDiff, możesz uzyskać zbiór pól, które zostały zmienione podczas edycji. Następnie możesz użyć funkcji takich jak hasOnly()
lub hasAny()
, aby sprawdzić, czy ten zbiór zawiera określone elementy.
Zapobieganie zmianom w niektórych polach
Używając metody hasAny()
na zbiorze wygenerowanym przez affectedKeys()
, a następnie negując wynik, możesz odrzucić każde żądanie klienta, które próbuje zmienić pola, których nie chcesz zmieniać.
Możesz na przykład zezwolić klientom na aktualizowanie informacji o restauracji, ale nie na zmianę średniej oceny ani liczby opinii.
service cloud.firestore {
match /databases/{database}/documents {
match /restaurant/{restId} {
// Allow the client to update a document only if that document doesn't
// change the average_score or rating_count fields
allow update: if (!request.resource.data.diff(resource.data).affectedKeys()
.hasAny(['average_score', 'rating_count']));
}
}
}
umożliwiać zmianę tylko niektórych pól,
Zamiast określać pola, których nie chcesz zmieniać, możesz też użyć funkcji
hasOnly()
do określenia listy pól, które chcesz zmienić. Jest to ogólnie uważane za bezpieczniejsze, ponieważ zapisywanie w nowych polach dokumentu jest domyślnie zabronione, dopóki nie zezwolisz na to w regułach bezpieczeństwa.
Zamiast na przykład blokować pola average_score
i rating_count
, możesz utworzyć reguły zabezpieczeń, które pozwolą klientom zmieniać tylko pola name
, location
, city
, address
, hours
i cuisine
.
service cloud.firestore {
match /databases/{database}/documents {
match /restaurant/{restId} {
// Allow a client to update only these 6 fields in a document
allow update: if (request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['name', 'location', 'city', 'address', 'hours', 'cuisine']));
}
}
}
Oznacza to, że jeśli w przyszłej wersji aplikacji dokumenty restauracji będą zawierać pole telephone
, próby edytowania tego pola będą kończyć się niepowodzeniem, dopóki nie wrócisz i nie dodasz tego pola do listy hasOnly()
w regułach bezpieczeństwa.
Wymuszanie typów pól
Kolejnym efektem braku schematu w Cloud Firestore jest to, że na poziomie bazy danych nie ma wymogu dotyczącego typów danych, które można przechowywać w określonych polach. Możesz to wymusić w regułach zabezpieczeń za pomocą operatora is
.
Na przykład ta reguła zabezpieczeń wymusza, aby pole score
w opinii było liczbą całkowitą, pola headline
, content
i author_name
były ciągami znaków, a pole review_date
było sygnaturą czasową.
service cloud.firestore {
match /databases/{database}/documents {
match /restaurant/{restId} {
// Restaurant rules go here...
match /review/{reviewId} {
allow create: if (request.resource.data.score is int &&
request.resource.data.headline is string &&
request.resource.data.content is string &&
request.resource.data.author_name is string &&
request.resource.data.review_date is timestamp
);
}
}
}
}
Prawidłowe typy danych dla operatora is
to bool
, bytes
, float
, int
, list
, latlng
, number
, path
, map
, string
i timestamp
. Operator is
obsługuje też typy danych constraint
, duration
, set
i map_diff
, ale ponieważ są one generowane przez sam język reguł zabezpieczeń, a nie przez klientów, rzadko używa się ich w większości praktycznych zastosowań.
Typy danych list
i map
nie obsługują typów ogólnych ani argumentów typu.
Innymi słowy, możesz używać reguł zabezpieczeń, aby wymusić, aby określone pole zawierało listę lub mapę, ale nie możesz wymusić, aby pole zawierało listę wszystkich liczb całkowitych lub wszystkich ciągów znaków.
Podobnie możesz używać reguł bezpieczeństwa, aby wymuszać wartości typów dla określonych wpisów na liście lub w mapie (używając odpowiednio notacji nawiasowej lub nazw kluczy), ale nie ma skrótu, który umożliwiałby wymuszenie typów danych wszystkich elementów w mapie lub na liście jednocześnie.
Na przykład te reguły zapewniają, że pole tags
w dokumencie zawiera listę, a pierwszy wpis jest ciągiem tekstowym. Zapewnia też, że pole product
zawiera mapę, która z kolei zawiera nazwę produktu (ciąg tekstowy) i ilość (liczbę całkowitą).
service cloud.firestore {
match /databases/{database}/documents {
match /orders/{orderId} {
allow create: if request.resource.data.tags is list &&
request.resource.data.tags[0] is string &&
request.resource.data.product is map &&
request.resource.data.product.name is string &&
request.resource.data.product.quantity is int
}
}
}
}
Typy pól muszą być wymuszane podczas tworzenia i aktualizowania dokumentu. Dlatego warto utworzyć funkcję pomocniczą, którą można wywoływać zarówno w sekcjach tworzenia, jak i aktualizowania reguł zabezpieczeń.
service cloud.firestore {
match /databases/{database}/documents {
function reviewFieldsAreValidTypes(docData) {
return docData.score is int &&
docData.headline is string &&
docData.content is string &&
docData.author_name is string &&
docData.review_date is timestamp;
}
match /restaurant/{restId} {
// Restaurant rules go here...
match /review/{reviewId} {
allow create: if reviewFieldsAreValidTypes(request.resource.data) &&
// Other rules may go here
allow update: if reviewFieldsAreValidTypes(request.resource.data) &&
// Other rules may go here
}
}
}
}
Wymuszanie typów w przypadku pól opcjonalnych
Pamiętaj, że wywołanie funkcji request.resource.data.foo
w dokumencie, w którym nie ma funkcji foo
, powoduje błąd, a zatem każda reguła bezpieczeństwa, która wywołuje tę funkcję, odrzuci żądanie. Możesz sobie z tym poradzić, używając metody get
na stronie request.resource.data
. Metoda get
umożliwia podanie argumentu domyślnego dla pola pobieranego z mapy, jeśli to pole nie istnieje.
Jeśli na przykład dokumenty do sprawdzenia zawierają też opcjonalne pole photo_url
i opcjonalne pole tags
, które mają być odpowiednio ciągami znaków i listami, możesz to osiągnąć, przepisując funkcję reviewFieldsAreValidTypes
w następujący sposób:
function reviewFieldsAreValidTypes(docData) {
return docData.score is int &&
docData.headline is string &&
docData.content is string &&
docData.author_name is string &&
docData.review_date is timestamp &&
docData.get('photo_url', '') is string &&
docData.get('tags', []) is list;
}
Odrzuca dokumenty, w których występuje pole tags
, ale nie jest ono listą, a jednocześnie zezwala na dokumenty, które nie zawierają pola tags
(ani photo_url
).
Częściowe zapisywanie jest niedozwolone
Ostatnia uwaga dotycząca Cloud Firestore Security Rules: albo zezwalają one klientowi na wprowadzenie zmiany w dokumencie, albo odrzucają całą edycję. Nie możesz tworzyć reguł zabezpieczeń, które akceptują zapisywanie w niektórych polach dokumentu, a odrzucają zapisywanie w innych polach w ramach tej samej operacji.