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 na podstawie zestawu uprawnień. Na przykład w aplikacji do edytowania dokumentów użytkownicy mogą chcieć zezwolić kilku użytkownikom na czytanie i zapisywanie ich dokumentów, blokowania niechcianego dostępu.

Rozwiązanie: kontrola dostępu oparta na rolach

Możesz wykorzystać model danych Cloud Firestore oraz reguły zabezpieczeń pozwalające wdrożyć dostęp oparty na rolach w aplikacji.

Załóżmy, że tworzysz aplikację do wspólnego pisania, w której użytkownicy mogą tworzyć „relacje”, i „komentarze” z tymi wymaganiami dotyczącymi zabezpieczeń:

  • 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ść artykułu.
  • 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żdy artykuł ma też kolekcję comments, w której każdy dokument jest komentarz 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 pewną treść:

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

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

Reguły

Gdy masz już użytkowników zarejestrowanych w bazie danych, musisz napisać Reguły zabezpieczeń pozwalające je zweryfikować. Te reguły zakładają, że aplikacja używa Uwierzytelnianie Firebase, aby request.auth.uid to identyfikator użytkownika.

Krok 1. Zacznij od pliku z podstawowymi regułami, który zawiera puste reguły dotyczące artykułów i komentarze:

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 opowiadania historii. Zdefiniowane funkcje pomagają określić role użytkownika oraz prawidłowe dokumenty:

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 na dowolnej roli czytać artykuły i komentarzy. Użycie funkcji zdefiniowanych w poprzednim kroku pozwala zachować reguły 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 artykułów, komentującym i właścicielom na publikowanie komentarzy. Zwróć uwagę, że ta reguła sprawdza również, czy owner komentarza pasuje do żądającego użytkownika, który zapobiega nadpisywaniu komentarzy 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 opcji jego edycji role lub inne właściwości dokumentu. Wymaga podziału reguły write są tworzone w osobnych regułach dla tych kategorii: create, update 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 przykładzie powyżej wiele ról (autor i właściciel) mieć uprawnienia do zapisu w tym samym dokumencie, ale z innymi ograniczeniami. Przy bardziej złożonych dokumentach zarządzanie tym może być trudne, lepiej podzielić pojedynczy dokument na kilka należące do jednej roli.
  • Duże grupy: jeśli chcesz udostępnić treści bardzo dużym lub złożonym grupom, rozważmy system, w którym role są przechowywane w osobnych zbiorach, niż jako pole na dokumencie docelowym.