Bezpieczny dostęp do danych dla użytkowników i grup

Wiele aplikacji do współpracy umożliwia użytkownikom odczytywanie i zapisywanie różnych danych na podstawie zestawu uprawnień. W aplikacji do edycji dokumentów użytkownicy mogą na przykład zezwolić kilku użytkownikom na czytanie i pisanie ich dokumentów, a jednocześnie zablokować im niechciany dostęp.

Rozwiązanie: kontrola dostępu oparta na rolach

Aby wdrożyć w aplikacji kontrolę dostępu na podstawie ról, możesz skorzystać z modelu danych Cloud Firestore oraz niestandardowych reguł zabezpieczeń.

Załóżmy, że tworzysz aplikację do wspólnego pisania, w której użytkownicy mogą tworzyć „historie” i „komentarze” spełniające następujące wymagania dotyczące bezpieczeństwa:

  • Każda relacja ma jednego właściciela i można ją udostępniać „autorom”, „komentatorom” i „czytelnikom”.
  • Czytelnicy mogą zobaczyć tylko historie i komentarze. Nie mogą niczego edytować.
  • Komentujący mają wszystkie uprawnienia czytelników, a dodatkowo mogą dodawać komentarze do historii.
  • Autorzy mają wszystkie uprawnienia komentujących, a dodatkowo mogą edytować treść relacji.
  • Właściciele mogą edytować dowolną część relacji, 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ż kolekcję comments, w której każdy dokument jest komentarzem na dany temat.

Aby śledzić role dostępu, dodaj pole roles, które jest mapą identyfikatorów użytkowników na role:

/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 pewną treść:

/stories/{storyid}/comments/{commentid}

{
  user: "alice",
  content: "I think this is a great story!"
}

Reguły

Po zarejestrowaniu ról użytkowników w bazie danych musisz napisać reguły zabezpieczeń, aby je sprawdzić. Te reguły zakładają, że aplikacja korzysta z 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 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 artykułami. Zdefiniowane funkcje pomagają w określaniu ról użytkownika oraz tego, 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 pozwolą użytkownikowi o dowolnej roli czytać historie i komentarze. Użycie funkcji zdefiniowanych w poprzednim kroku pozwala zachować zwięzłość i czytelność reguł:

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 artykułów, komentującym i właścicielom na publikowanie komentarzy. Pamiętaj, że ta reguła sprawdza też, czy owner komentarza pasuje do użytkownika, który wysłał żądanie. Zapobiega to nadpisywaniu komentarzy przez innych 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. Daj twórcom możliwość edytowania treści artykułu, ale nie mogą edytować ról artykułu ani zmieniać innych właściwości dokumentu. Wymaga to podzielenia reguły write na osobne reguły dla kategorii create, update i delete, ponieważ autorzy mogą aktualizować tylko 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

Powyższe rozwiązanie demonstruje zabezpieczanie danych użytkowników za pomocą reguł zabezpieczeń, ale pamiętaj o tych ograniczeniach:

  • Szczegółowość: w tym przykładzie kilka ról (autora i właściciela) ma uprawnienia do zapisu tego samego dokumentu, ale z różnymi ograniczeniami. W przypadku bardziej złożonych dokumentów może być trudno zarządzać uprawnieniami, dlatego lepiej jest podzielić pojedyncze dokumenty na wiele dokumentów, z których każdy będzie należeć do jednej roli.
  • Duże grupy: jeśli chcesz udostępniać treści bardzo dużym lub złożonym grupom, rozważ użycie systemu, w którym role są przechowywane w ich własnej kolekcji, a nie jako pole w dokumencie docelowym.