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”, „komentatorom” i „czytelnikom”.
- Czytelnicy mogą tylko wyświetlać historie i komentarze. Nie mogą niczego edytować.
- Komentatorzy mają taki sam dostęp jak czytelnicy, a dodatkowo mogą dodawać komentarze do historii.
- Autorzy mają taki sam dostęp jak komentatorzy, a dodatkowo mogą edytować treść historii.
- Właściciele mogą edytować dowolną część historii, a także 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 używa
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, komentatorom i właścicielom historii na publikowanie komentarzy.
Pamiętaj, że ta reguła sprawdza też, czy owner komentarza pasuje do użytkownika wysyłającego żą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 zmiany 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.