Ta strona opiera się na koncepcjach opisanych w artykułach Struktura reguł zabezpieczeń i Pisanie warunków reguł zabezpieczeń. Wyjaśnia, jak za pomocą Cloud Firestore Security Rules tworzyć reguły, które umożliwiają klientom wykonywanie operacji na niektórych polach dokumentu, 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 utworzenie lub zmianę dokumentu, ale nie na edytowanie niektórych pól w tym dokumencie. Możesz też wymagać, aby każdy dokument utworzony przez klienta 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 nic. Nie ma możliwości pobrania części dokumentu. Nie można za pomocą samych reguł zabezpieczeń uniemożliwić użytkownikom odczytywania określonych pól w dokumencie.
Jeśli w dokumencie znajdują się 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 podkolekcji private 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 zabezpieczeń, które mają różne poziomy dostępu do tych 2 kolekcji. W tym przykładzie używamy niestandardowych deklaracji uwierzytelniania
aby określić, że tylko użytkownicy z niestandardową deklaracją uwierzytelniania role o wartości 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łatwić tworzenie aplikacji, ale czasami możesz chcieć mieć pewność, że klienci mogą tworzyć tylko dokumenty zawierające określone pola lub nie zawierające innych pól.
Te reguły możesz utworzyć, sprawdzając metodę keys obiektu
request.resource.data. Jest to lista wszystkich pól, które klient próbuje zapisać 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 metodę 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 innymi polami, ale zapewnia, że wszystkie dokumenty utworzone przez klienta zawierają co najmniej te 3 pola.
Zabranianie określonych pól w nowych dokumentach
Podobnie możesz uniemożliwić klientom tworzenie dokumentów zawierających
określone pola, używając metody 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 musiał zanegować wynik, aby zabronić używania niektórych pól.
Na przykład w tym przykładzie klienci nie mogą utworzyć dokumentu zawierającego pole average_score ani rating_count, ponieważ te pola zostaną dodane później przez wywołanie serwera.
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 dla nowych dokumentów
Zamiast zabraniać używania niektórych pól w nowych dokumentach, możesz utworzyć listę tylko tych pól, które są wyraźnie dozwolone w nowych dokumentach. Następnie
możesz użyć funkcji hasOnly()
, aby mieć pewność, że wszystkie nowe dokumenty zawierają tylko te pola
(lub ich podzbiór) i żadne 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
W regułach zabezpieczeń możesz łączyć operacje hasAll i hasOnly, aby wymagać niektórych pól i zezwalać na inne. Na przykład ten przykład wymaga, aby wszystkie nowe dokumenty zawierały pola name, location i city, a opcjonalnie zezwala na 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 rzeczywistym scenariuszu możesz przenieść tę logikę do funkcji pomocniczej, aby uniknąć duplikowania kodu i łatwiej połączyć pola opcjonalne i wymagane w jedną listę, tak 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żna tego zrobić, sprawdzając tylko listę request.resource.data.keys() opisaną w poprzedniej sekcji, ponieważ ta lista reprezentuje cały dokument po aktualizacji i dlatego będzie 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 2 różnymi
mapami.
Wywołując metodę affectedKeys()
na tym obiekcie mapDiff, możesz uzyskać zestaw pól, które zostały zmienione
podczas edycji. Następnie możesz użyć funkcji takich jak
hasOnly()
lub hasAny()
aby mieć pewność, że ten zestaw zawiera (lub nie zawiera) określone elementy.
Uniemożliwianie zmiany niektórych pól
Używając metody hasAny()
w odniesieniu do zestawu wygenerowanego przez affectedKeys()
i 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']));
}
}
}
Zezwalanie na zmianę tylko niektórych pól
Zamiast określać pola, których nie chcesz zmieniać, możesz też użyć funkcji
hasOnly()
i określić listę 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 nie wyraźnie w regułach zabezpieczeń.
Na przykład zamiast zabraniać używania pól average_score i rating_count, możesz utworzyć reguły zabezpieczeń, które zezwalają klientom na zmianę tylko pól 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 edycji tego pola nie powiodą się, dopóki nie wrócisz i nie dodasz tego pola do listy hasOnly() w regułach zabezpieczeń.
Wymuszanie typów pól
Kolejnym skutkiem tego, że Cloud Firestore nie ma schematu, 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 jednak wymusić to w regułach zabezpieczeń za pomocą operatora is.
Na przykład ta reguła zabezpieczeń wymusza, aby pole score 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ł zabezpieczeń, aby wymusić typy wartości dla określonych wpisów na liście lub mapie (odpowiednio za pomocą notacji nawiasów lub nazw kluczy), ale nie ma skrótu, który umożliwiałby wymuszenie typów danych wszystkich elementów na mapie lub liście naraz.
Na przykład te reguły zapewniają, że pole tags w dokumencie
zawiera listę, a pierwszy wpis jest ciągiem znaków. Zapewniają też, że pole product zawiera mapę, która z kolei zawiera nazwę produktu będącą ciągiem znaków i ilość będącą 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 zarówno podczas tworzenia, jak i aktualizowania dokumentu. Dlatego warto utworzyć funkcję pomocniczą, którą można wywoływać zarówno w sekcji tworzenia, jak i aktualizacji 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 pól opcjonalnych
Pamiętaj, że wywołanie request.resource.data.foo w
dokumencie, w którym nie ma pola foo, powoduje błąd, a tym samym każda
reguła zabezpieczeń, która wywołuje to pole, odrzuci żądanie. Możesz sobie z tym poradzić,
używając get
metody w 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 opinii zawierają też opcjonalne pole photo_url i opcjonalne pole tags, które mają być odpowiednio ciągami znaków i listami, możesz to zrobić, przepisując funkcję reviewFieldsAreValidTypes na coś takiego:
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;
}
Spowoduje to odrzucenie dokumentów, w których pole tags istnieje, ale nie jest listą, a jednocześnie zezwoli na dokumenty, które nie zawierają pola tags (ani photo_url).
Zapisywanie częściowe jest niedozwolone
Ostatnia uwaga dotycząca Cloud Firestore Security Rules jest taka, że zezwalają one klientowi na wprowadzenie zmiany w dokumencie lub odrzucają całą edycję. Nie możesz tworzyć reguł zabezpieczeń, które akceptują zapisywanie w niektórych polach dokumentu, a odrzucają inne w tej samej operacji.