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

Wiele aplikacji do współpracy pozwala użytkownikom odczytywać i zapisywać różne dane w zależności od 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

Możesz wykorzystać model danych Cloud Firestore oraz niestandardowe reguły zabezpieczeń, aby wdrożyć w swojej 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” 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 widzą tylko relacje i komentarze. Nie mogą niczego edytować.
  • Komentujący mają pełny dostęp do czytelników i mogą dodawać komentarze do relacji.
  • Autorzy mają pełny dostęp do komentujących i mogą edytować treść relacji.
  • Właściciele mogą edytować dowolną część artykułu 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 opowieść. 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ć. W regułach tych założono, że aplikacja korzysta z uwierzytelniania Firebase, więc zmienna request.auth.uid jest identyfikatorem użytkownika.

Krok 1. Zacznij od podstawowego pliku z regułami, który zawiera puste reguły dotyczące relacji 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. Napisz reguły, które umożliwią użytkownikom o dowolnej roli czytanie artykułów i komentarzy. 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 pole owner komentarza jest zgodne z użytkownikiem wysyłającym prośbę. Uniemożliwia to użytkownikom pisanie nad komentarzami 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 treści i właściciela) ma uprawnienia do zapisu tego samego dokumentu, ale z różnymi ograniczeniami. Zarządzanie w przypadku bardziej złożonych dokumentów może być trudne. Lepiej jest podzielić pojedyncze dokumenty na kilka dokumentów, z których każdy ma przypisaną 1 rolę.
  • Duże grupy: jeśli chcesz udostępniać treści bardzo dużym lub złożonym grupom, rozważ system, w którym role są przechowywane w osobnych kolekcjach, a nie jako pola w dokumencie docelowym.