Wiele aplikacji do współpracy umożliwia użytkownikom odczytywanie i zapisywanie różnych fragmentów danych na podstawie zestawu uprawnień. Na przykład w aplikacji do edycji dokumentów użytkownicy mogą chcieć zezwolić kilku osobom na odczytywanie i zapisywanie dokumentów, jednocześnie blokując niechciany dostęp.
Rozwiązanie: kontrola dostępu oparta na rolach
Aby wdrożyć w aplikacji kontrolę dostępu opartą na rolach, możesz skorzystać z modelu danych Cloud Firestore oraz niestandardowych reguł zabezpieczeń do implementacji kontroli dostępu w swojej aplikacji.
Załóżmy, że tworzysz aplikację do wspólnego pisania, w której użytkownicy mogą tworzyć „historie” i „komentarze” zgodnie z tymi wymaganiami dotyczącymi bezpieczeństwa:
- Każda historia ma jednego właściciela i można ją udostępniać „autorom”, „komentującym” i „czytelnikom”.
- Czytelnicy mogą tylko wyświetlać historie i komentarze. Nie mogą niczego edytować.
- Komentujący mają taki sam dostęp jak czytelnicy, a dodatkowo mogą dodawać komentarze do historii.
- Autorzy mają taki sam dostęp jak komentujący, a dodatkowo mogą edytować treść historii.
- Właściciele mogą edytować dowolną część historii oraz kontrolować dostęp innych użytkowników.
Struktura danych
Załóżmy, że Twoja aplikacja ma kolekcję stories, w której każdy dokument reprezentuje historię. Każda historia ma też podkolekcję comments, w której każdy dokument jest komentarzem do tej historii.
Aby śledzić role dostępu, dodaj pole roles, które jest mapą identyfikatorów użytkowników do ról:
/stories/{storyid}
{
title: "A Great Story",
content: "Once upon a time ...",
roles: {
alice: "owner",
bob: "reader",
david: "writer",
jane: "commenter"
// ...
}
}
Komentarze zawierają tylko 2 pola – identyfikator użytkownika autora i treść:
/stories/{storyid}/comments/{commentid}
{
user: "alice",
content: "I think this is a great story!"
}
Reguły
Teraz, gdy masz role użytkowników zapisane w bazie danych, musisz utworzyć reguły zabezpieczeń, aby je zweryfikować. Te reguły zakładają, że aplikacja korzysta z
Firebase Auth, dzięki czemu request.auth.uid
zmienna jest identyfikatorem użytkownika.
Krok 1. Zacznij od podstawowego pliku reguł, który zawiera puste reguły dotyczące historii i komentarzy:
service cloud.firestore {
match /databases/{database}/documents {
match /stories/{story} {
// TODO: Story rules go here...
match /comments/{comment} {
// TODO: Comment rules go here...
}
}
}
}
Krok 2. Dodaj prostą regułę write, która daje właścicielom pełną kontrolę nad
historiami. Zdefiniowane funkcje pomagają określić role użytkownika i sprawdzić, czy nowe dokumenty są prawidłowe:
service cloud.firestore {
match /databases/{database}/documents {
match /stories/{story} {
function isSignedIn() {
return request.auth != null;
}
function getRole(rsc) {
// Read from the "roles" map in the resource (rsc).
return rsc.data.roles[request.auth.uid];
}
function isOneOfRoles(rsc, array) {
// Determine if the user is one of an array of roles
return isSignedIn() && (getRole(rsc) in array);
}
function isValidNewStory() {
// Valid if story does not exist and the new story has the correct owner.
return resource == null && isOneOfRoles(request.resource, ['owner']);
}
// Owners can read, write, and delete stories
allow write: if isValidNewStory() || isOneOfRoles(resource, ['owner']);
match /comments/{comment} {
// ...
}
}
}
}
Krok 3. Utwórz reguły, które umożliwiają użytkownikowi z dowolną rolą odczytywanie historii i komentarzy. Dzięki funkcjom zdefiniowanym w poprzednim kroku reguły są zwięzłe i czytelne:
service cloud.firestore {
match /databases/{database}/documents {
match /stories/{story} {
function isSignedIn() {
return request.auth != null;
}
function getRole(rsc) {
return rsc.data.roles[request.auth.uid];
}
function isOneOfRoles(rsc, array) {
return isSignedIn() && (getRole(rsc) in array);
}
function isValidNewStory() {
return resource == null
&& request.resource.data.roles[request.auth.uid] == 'owner';
}
allow write: if isValidNewStory() || isOneOfRoles(resource, ['owner']);
// Any role can read stories.
allow read: if isOneOfRoles(resource, ['owner', 'writer', 'commenter', 'reader']);
match /comments/{comment} {
// Any role can read comments.
allow read: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
['owner', 'writer', 'commenter', 'reader']);
}
}
}
}
Krok 4. Zezwól autorom, komentującym i właścicielom historii na publikowanie komentarzy.
Pamiętaj, że ta reguła sprawdza też, czy owner komentarza jest zgodny z użytkownikiem wysyłającym żądanie, co uniemożliwia użytkownikom nadpisywanie komentarzy innych osób:
service cloud.firestore {
match /databases/{database}/documents {
match /stories/{story} {
function isSignedIn() {
return request.auth != null;
}
function getRole(rsc) {
return rsc.data.roles[request.auth.uid];
}
function isOneOfRoles(rsc, array) {
return isSignedIn() && (getRole(rsc) in array);
}
function isValidNewStory() {
return resource == null
&& request.resource.data.roles[request.auth.uid] == 'owner';
}
allow write: if isValidNewStory() || isOneOfRoles(resource, ['owner'])
allow read: if isOneOfRoles(resource, ['owner', 'writer', 'commenter', 'reader']);
match /comments/{comment} {
allow read: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
['owner', 'writer', 'commenter', 'reader']);
// Owners, writers, and commenters can create comments. The
// user id in the comment document must match the requesting
// user's id.
//
// Note: we have to use get() here to retrieve the story
// document so that we can check the user's role.
allow create: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
['owner', 'writer', 'commenter'])
&& request.resource.data.user == request.auth.uid;
}
}
}
}
Krok 5. Daj autorom możliwość edytowania treści historii, ale nie edytowania ról historii
ani zmieniania innych właściwości dokumentu. Wymaga to podzielenia reguły write historii na osobne reguły create, update i delete, ponieważ autorzy mogą tylko aktualizować historie:
service cloud.firestore {
match /databases/{database}/documents {
match /stories/{story} {
function isSignedIn() {
return request.auth != null;
}
function getRole(rsc) {
return rsc.data.roles[request.auth.uid];
}
function isOneOfRoles(rsc, array) {
return isSignedIn() && (getRole(rsc) in array);
}
function isValidNewStory() {
return request.resource.data.roles[request.auth.uid] == 'owner';
}
function onlyContentChanged() {
// Ensure that title and roles are unchanged and that no new
// fields are added to the document.
return request.resource.data.title == resource.data.title
&& request.resource.data.roles == resource.data.roles
&& request.resource.data.keys() == resource.data.keys();
}
// Split writing into creation, deletion, and updating. Only an
// owner can create or delete a story but a writer can update
// story content.
allow create: if isValidNewStory();
allow delete: if isOneOfRoles(resource, ['owner']);
allow update: if isOneOfRoles(resource, ['owner'])
|| (isOneOfRoles(resource, ['writer']) && onlyContentChanged());
allow read: if isOneOfRoles(resource, ['owner', 'writer', 'commenter', 'reader']);
match /comments/{comment} {
allow read: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
['owner', 'writer', 'commenter', 'reader']);
allow create: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
['owner', 'writer', 'commenter'])
&& request.resource.data.user == request.auth.uid;
}
}
}
}
Ograniczenia
Rozwiązanie przedstawione powyżej pokazuje, jak zabezpieczyć dane użytkownika za pomocą reguł zabezpieczeń, ale musisz pamiętać o tych ograniczeniach:
- Szczegółowość: w powyższym przykładzie wiele ról (autor i właściciel) ma dostęp do zapisu w tym samym dokumencie, ale z różnymi ograniczeniami. W przypadku bardziej złożonych dokumentów może to być trudne do zarządzania. Lepiej jest podzielić pojedyncze dokumenty na wiele dokumentów, z których każdy należy do jednej roli.
- Duże grupy: jeśli musisz udostępniać treści bardzo dużym lub złożonym grupom, rozważ użycie systemu, w którym role są przechowywane w osobnej kolekcji, a nie jako pole w dokumencie docelowym.