Sicherer Datenzugriff für Nutzer und Gruppen

Viele Anwendungen für die Zusammenarbeit ermöglichen Nutzern das Lesen und Schreiben verschiedener Daten basierend auf einer Reihe von Berechtigungen. In einer Anwendung zur Bearbeitung von Dokumenten möchten Nutzer z. B. einigen Nutzern erlauben, ihre Dokumente zu lesen und zu schreiben, und gleichzeitig unerwünschte Zugriffe blockieren.

Lösung: Rollenbasierte Zugriffssteuerung

Sie können das Cloud Firestore-Datenmodell sowie benutzerdefinierte Sicherheitsregeln nutzen, um die rollenbasierte Zugriffssteuerung in Ihrer Anwendung zu implementieren.

Angenommen, Sie erstellen eine Anwendung zum gemeinsamen Schreiben, in der Nutzer "Geschichten" und "Kommentare" mit den folgenden Sicherheitsanforderungen erstellen können:

  • Jede Geschichte hat einen Inhaber und kann für "Autoren", "Kommentatoren" und "Leser" freigegeben werden.
  • Leser können Geschichten und Kommentare nur ansehen. Sie können nichts bearbeiten.
  • Kommentatoren haben die gleichen Zugriffsberechtigungen wie Leser und können zusätzlich Kommentare zu einer Geschichte hinzufügen.
  • Autoren haben die gleichen Zugriffsberechtigungen wie Kommentatoren und können zusätzlich den Inhalt einer Geschichte bearbeiten.
  • Inhaber können alle Teile einer Geschichte bearbeiten sowie den Zugriff anderer Nutzer steuern.

Datenstruktur

Angenommen, Ihre Anwendung enthält eine Sammlung stories, in der jedes Dokument eine Geschichte darstellt. Jede Geschichte hat auch die untergeordnete Sammlung comments, in der jedes Dokument ein Kommentar zu dieser Geschichte ist.

Fügen Sie das Feld roles hinzu, das eine Zuordnung von Nutzer-IDs zu Rollen darstellt, um einen Überblick über die Zugriffsrollen zu behalten:

/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 Nutzer-ID des Autors und ein bestimmter Inhalt:

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

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

Regeln

Nachdem Sie die Rollen der Nutzer in der Datenbank gespeichert haben, müssen Sie Sicherheitsregeln schreiben, um diese zu validieren. Bei diesen Regeln wird davon ausgegangen, dass die Anwendung Firebase Auth verwendet, sodass die Variable request.auth.uid die Nutzer-ID ist.

Schritt 1: Beginnen Sie mit einer einfachen Regeldatei, 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-Regel hinzu, mit der Inhaber die vollständige Kontrolle über die Geschichten erhalten. Die definierten Funktionen bestimmen die Rollen eines Nutzers und 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 Nutzer mit einer beliebigen Rolle ermöglichen, Geschichten und Kommentare zu lesen. Wenn Sie die im vorherigen Schritt definierten Funktionen nutzen, bleiben die Regeln übersichtlich 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 Autoren, Kommentatoren und Inhabern, Kommentare zu posten. Beachten Sie, dass durch diese Regel auch validiert wird, dass der owner des Kommentars mit dem anfragenden Nutzer übereinstimmt, wodurch verhindert wird, dass Nutzer die Kommentare des jeweils anderen ü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: Erlauben Sie Autoren, Inhalte von Geschichten zu bearbeiten, wobei das Bearbeiten von Rollen der Geschichte oder Ändern anderer Attribute des Dokuments nicht zulässig ist. Dazu muss die write-Regel der Geschichten in separate Regeln für create, update und delete aufgeteilt werden, da Autoren Geschichten nur 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;
        }
     }
   }
}

Beschränkungen

Die oben gezeigte Lösung veranschaulicht das Sichern von Nutzerdaten mithilfe von Sicherheitsregeln. Beachten Sie jedoch die folgenden Einschränkungen:

  • Granularität: Im obigen Beispiel haben mehrere Rollen (Autor und Inhaber) Schreibzugriff auf das gleiche Dokument, allerdings mit unterschiedlichen Einschränkungen. Dies kann bei komplexeren Dokumenten schwierig zu verwalten sein und es ist möglicherweise besser, einzelne Dokumente in mehrere Dokumente aufzuteilen, die jeweils einer einzelnen Rolle zugeordnet sind.
  • Große Gruppen: Wenn Sie eine Freigabe für sehr große oder komplexe Gruppen benötigen, sollten Sie ein System verwenden, in dem Rollen in ihrer eigenen Sammlung und nicht als Feld im Zieldokument gespeichert werden.