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 osobom na odczyt i edycję 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 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ć „opowiadania” i „komentarze” z tymi wymaganiami dotyczącymi bezpieczeństwa:

  • Każda historia ma jednego właściciela i można ją udostępnić „autorom”, „komentatorom” i „czytelnikom”.
  • Czytelnicy mogą zobaczyć tylko historie i komentarze. Nie mogą edytować żadnych elementów.
  • 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 treść:

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

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

Reguły

Teraz, gdy masz już w bazie danych role użytkowników, musisz napisać reguły bezpieczeństwa, aby je zweryfikować. 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 opowieściami. 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 umożliwią użytkownikom o dowolnej roli czytanie artykułów i komentarzy. Korzystanie z 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. Pozwól autorom, komentatorom i właścicielom publikować komentarze. 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 dotyczącej artykułów 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

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

  • Dokładność: w przykładzie powyżej wiele ról (autor i właściciel) ma dostęp do zapisu do 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ż użycie systemu, w którym role są przechowywane w ich własnej kolekcji, a nie jako pole w dokumencie docelowym.