Sicherer Datenzugriff für Benutzer und Gruppen

Viele kollaborative Apps ermöglichen es Benutzern, basierend auf einer Reihe von Berechtigungen verschiedene Daten zu lesen und zu schreiben. In einer Dokumentenbearbeitungs-App möchten Benutzer beispielsweise einigen Benutzern das Lesen und Schreiben ihrer Dokumente erlauben und gleichzeitig unerwünschten Zugriff blockieren.

Lösung: Rollenbasierte Zugriffskontrolle

Sie können das Datenmodell von Cloud Firestore sowie benutzerdefinierte Sicherheitsregeln nutzen, um eine rollenbasierte Zugriffskontrolle in Ihrer App zu implementieren.

Angenommen, Sie erstellen eine Anwendung zum kollaborativen Schreiben, in der Benutzer „Geschichten“ und „Kommentare“ mit den folgenden Sicherheitsanforderungen erstellen können:

  • Jede Geschichte hat einen Besitzer und kann mit „Autoren“, „Kommentatoren“ und „Lesern“ geteilt werden.
  • Leser können nur Geschichten und Kommentare sehen. Sie können nichts bearbeiten.
  • Kommentatoren haben den gleichen Zugriff wie Leser und können einer Story auch Kommentare hinzufügen.
  • Autoren haben den gleichen Zugriff wie Kommentatoren und können auch den Inhalt der Story bearbeiten.
  • Eigentümer können jeden Teil einer Story bearbeiten und den Zugriff anderer Benutzer steuern.

Datenstruktur

Angenommen, Ihre App verfügt über eine stories Sammlung, in der jedes Dokument eine Story darstellt. Jede Story verfügt außerdem über eine comments Untersammlung, in der jedes Dokument einen Kommentar zu dieser Story darstellt.

Um den Überblick über Zugriffsrollen zu behalten, fügen Sie ein roles hinzu, das eine Zuordnung von Benutzer-IDs zu Rollen darstellt:

/stories/{storyid}

{
  title: "A Great Story",
  content: "Once upon a time ...",
  roles: {
    alice: "owner",
    bob: "reader",
    david: "writer",
    jane: "commenter"
    // ...
  }
}

Kommentare enthalten nur zwei Felder, die Benutzer-ID des Autors und einige Inhalte:

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

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

Regeln

Nachdem Sie nun die Rollen der Benutzer in der Datenbank erfasst haben, müssen Sie Sicherheitsregeln schreiben, um sie zu validieren. Bei diesen Regeln wird davon ausgegangen, dass die App Firebase Auth verwendet, sodass die Variable request.auth.uid “ die Benutzer-ID ist.

Schritt 1 : Beginnen Sie mit einer Grundregeldatei, die leere Regeln für Geschichten und Kommentare enthält:

service cloud.firestore {
   match /databases/{database}/documents {
     match /stories/{story} {
         // TODO: Story rules go here...

         match /comments/{comment} {
            // TODO: Comment rules go here...
         }
     }
   }
}

Schritt 2 : Fügen Sie eine einfache write hinzu, die den Eigentümern die vollständige Kontrolle über die Storys gibt. Die definierten Funktionen helfen dabei, die Rollen eines Benutzers zu bestimmen und festzustellen, ob neue Dokumente gültig sind:

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} {
            // ...
         }
     }
   }
}

Schritt 3 : Schreiben Sie Regeln, die es einem Benutzer jeder Rolle ermöglichen, Geschichten und Kommentare zu lesen. Durch die Verwendung der im vorherigen Schritt definierten Funktionen bleiben die Regeln präzise und lesbar:

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']);
        }
     }
   }
}

Schritt 4 : Erlauben Sie Story-Autoren, Kommentatoren und Eigentümern, Kommentare zu posten. Beachten Sie, dass diese Regel auch überprüft, ob der owner des Kommentars mit dem anfragenden Benutzer übereinstimmt, wodurch verhindert wird, dass Benutzer die Kommentare anderer überschreiben:

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;
        }
     }
   }
}

Schritt 5 : Geben Sie Autoren die Möglichkeit, den Story-Inhalt zu bearbeiten, aber nicht die Story-Rollen zu bearbeiten oder andere Eigenschaften des Dokuments zu ändern. Dies erfordert die Aufteilung der Story- write in separate Regeln für „ create “, update “ und „ delete , da Autoren nur Storys aktualisieren können:

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;
        }
     }
   }
}

Einschränkungen

Die oben gezeigte Lösung veranschaulicht die Sicherung von Benutzerdaten mithilfe von Sicherheitsregeln. Sie sollten sich jedoch der folgenden Einschränkungen bewusst sein:

  • Granularität : Im obigen Beispiel haben mehrere Rollen (Autor und Eigentümer) Schreibzugriff auf dasselbe Dokument, jedoch mit unterschiedlichen Einschränkungen. Dies kann bei komplexeren Dokumenten schwierig zu handhaben sein und es kann besser sein, einzelne Dokumente in mehrere Dokumente aufzuteilen, die jeweils einer einzelnen Rolle gehören.
  • Große Gruppen : Wenn Sie Daten mit sehr großen oder komplexen Gruppen teilen müssen, ziehen Sie ein System in Betracht, bei dem Rollen in einer eigenen Sammlung und nicht als Feld im Zieldokument gespeichert werden.