Wiele aplikacji do współpracy umożliwia użytkownikom odczytywanie i zapisywanie różnych 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
Możesz skorzystać z modelu danych Cloud Firestore oraz niestandardowych reguł zabezpieczeń, aby wdrożyć w aplikacji kontrolę dostępu opartą na rolach.
Załóżmy, że tworzysz aplikację do wspólnego pisania, w której użytkownicy mogą tworzyć „historie” i „komentarze” z następującymi wymaganiami dotyczącymi bezpieczeństwa:
- Każde opowiadanie ma jednego właściciela i może być udostępniane „autorom”, „komentatorom” i „czytelnikom”.
- Czytelnicy mogą tylko wyświetlać artykuły i komentarze. Nie mogą nic edytować.
- Komentujący mają takie same uprawnienia jak czytelnicy, ale mogą też dodawać komentarze do opowiadania.
- Autorzy mają takie same uprawnienia jak komentujący, a dodatkowo mogą edytować treść artykułu.
- Właściciele mogą edytować dowolną część opowiadania, 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 artykuł. 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 i 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 role użytkowników są zapisane w bazie danych, musisz napisać reguły zabezpieczeń, aby je zweryfikować. Te reguły zakładają, że aplikacja używa Firebase Auth, więc zmienna request.auth.uid jest identyfikatorem użytkownika.
Krok 1. Zacznij od podstawowego pliku reguł, który zawiera puste reguły dotyczące artykułów 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 relacjami. Zdefiniowane funkcje pomagają określić role użytkownika i to, 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. Napisz reguły, które pozwolą użytkownikowi o dowolnej roli czytać artykuły i komentarze. Użycie funkcji zdefiniowanych w poprzednim kroku sprawia, że 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 opowiadań na publikowanie komentarzy.
Zwróć uwagę, że ta reguła sprawdza też, czy owner komentarza odpowiada użytkownikowi, który wysłał żądanie, co zapobiega nadpisywaniu komentarzy przez użytkowników:
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. Przyznaj autorom możliwość edytowania treści artykułu, ale nie ról ani innych właściwości dokumentu. Wymaga to podzielenia reguły write na osobne reguły dla create, update i delete, ponieważ autorzy mogą tylko aktualizować artykuły:
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 pokazane powyżej demonstruje zabezpieczanie danych użytkownika za pomocą reguł zabezpieczeń, ale należy pamiętać o tych ograniczeniach:
- Szczegółowość: w przykładzie powyżej wiele ról (zapisujący i właściciel) ma uprawnienia 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 i lepiej jest podzielić pojedyncze dokumenty na wiele dokumentów, z których każdy jest przypisany do jednej roli.
- Duże grupy: jeśli chcesz udostępniać dane bardzo dużym lub złożonym grupom, rozważ system, w którym role są przechowywane we własnej kolekcji, a nie jako pole w dokumencie docelowym.