Bezpieczne zapytania dotyczące danych

Na tej stronie omawiamy zagadnienia omawiane w sekcjach Tworzenie struktury reguł zabezpieczeń i Zapisywanie warunków reguł zabezpieczeń, gdzie wyjaśniamy, jak reguły zabezpieczeń Cloud Firestore współdziałają z zapytaniami. W tym artykule opisujemy, jak reguły zabezpieczeń wpływają na zapytania, które można tworzyć, i przedstawiamy, jak sprawdzić, czy w zapytaniach obowiązują te same ograniczenia co reguły zabezpieczeń. Na tej stronie opisujemy też, jak pisać reguły zabezpieczeń, które zezwalają na zapytania lub je odrzucają na podstawie właściwości zapytań, takich jak limit i orderBy.

Reguły nie są filtrami

Podczas tworzenia zapytań służących do pobierania dokumentów pamiętaj, że reguły zabezpieczeń nie są filtrami – zapytania to wszystkie albo nic. Aby zaoszczędzić Twój czas i zasoby, Cloud Firestore ocenia zapytanie pod kątem potencjalnego zbioru wyników zamiast rzeczywistych wartości pól dla wszystkich dokumentów. Jeśli zapytanie może potencjalnie zwrócić dokumenty, których klient nie ma uprawnień do odczytu, całe żądanie zakończy się niepowodzeniem.

Zapytania i reguły zabezpieczeń

Jak widać w poniższych przykładach, musisz tworzyć zapytania tak, aby spełniały ograniczenia reguł zabezpieczeń.

Zabezpieczanie dokumentów i wykonywanie na nich zapytań na podstawie zasad auth.uid

Poniższy przykład pokazuje, jak napisać zapytanie pobierające dokumenty chronione przez regułę zabezpieczeń. Weźmy za przykład bazę danych zawierającą zbiór story dokumentów:

/stories/{storyid}

{
  title: "A Great Story",
  content: "Once upon a time...",
  author: "some_auth_id",
  published: false
}

Oprócz pól title i content w każdym dokumencie znajdują się pola author i published, które służą do kontroli dostępu. W tych przykładach zakładamy, że aplikacja korzysta z uwierzytelniania Firebase do ustawienia w polu author identyfikatora UID użytkownika, który utworzył dokument. Uwierzytelnianie Firebase wypełnia też zmienną request.auth w regułach zabezpieczeń.

Ta reguła zabezpieczeń używa zmiennych request.auth i resource.data, aby ograniczyć uprawnienia do odczytu i zapisu dla poszczególnych story do autora:

service cloud.firestore {
  match /databases/{database}/documents {
    match /stories/{storyid} {
      // Only the authenticated user who authored the document can read or write
      allow read, write: if request.auth != null && request.auth.uid == resource.data.author;
    }
  }
}

Załóżmy, że Twoja aplikacja zawiera stronę, na której wyświetla się lista story dokumentów stworzonych przez tego użytkownika. To zapytanie może wypełniać tę stronę. To zapytanie zakończy się jednak niepowodzeniem, ponieważ nie obejmuje tych samych ograniczeń co reguły zabezpieczeń:

Błąd: ograniczenia zapytania nie odpowiadają ograniczeniom reguł zabezpieczeń.

// This query will fail
db.collection("stories").get()

Zapytanie nie zostaje wykonane, nawet jeśli bieżący użytkownik jest autorem każdego dokumentu story. Dzieje się tak, ponieważ gdy Cloud Firestore stosuje reguły zabezpieczeń, ocenia zapytanie pod kątem potencjalnego zbioru wyników, a nie rzeczywistych właściwości dokumentów w bazie danych. Jeśli zapytanie może potencjalnie obejmować dokumenty, które naruszają Twoje reguły zabezpieczeń, zakończy się niepowodzeniem.

W przeciwieństwie do tego poniższe zapytanie jest spełnione, ponieważ zawiera to samo ograniczenie w polu author co reguły zabezpieczeń:

Prawidłowe: ograniczenia zapytań pasują do ograniczeń reguł zabezpieczeń

var user = firebase.auth().currentUser;

db.collection("stories").where("author", "==", user.uid).get()

Zabezpieczanie dokumentów i wykonywanie na nich zapytań na podstawie pól

Aby lepiej zademonstrować interakcję między zapytaniami a regułami, poniższe reguły zabezpieczeń rozszerzają uprawnienia do odczytu kolekcji stories, umożliwiając każdemu użytkownikowi odczytywanie dokumentów story, w których pole published ma wartość true.

service cloud.firestore {
  match /databases/{database}/documents {
    match /stories/{storyid} {
      // Anyone can read a published story; only story authors can read unpublished stories
      allow read: if resource.data.published == true || (request.auth != null && request.auth.uid == resource.data.author);
      // Only story authors can write
      allow write: if request.auth != null && request.auth.uid == resource.data.author;
    }
  }
}

Zapytanie o opublikowane strony musi zawierać te same ograniczenia co reguły zabezpieczeń:

db.collection("stories").where("published", "==", true).get()

Ograniczenie zapytania .where("published", "==", true) gwarantuje, że parametr resource.data.published ma wartość true w przypadku dowolnego wyniku. Dlatego to zapytanie jest zgodne z regułami zabezpieczeń i może odczytywać dane.

Zapytania: OR

Podczas oceniania logicznego zapytania OR (or, in lub array-contains-any) względem zbioru reguł Cloud Firestore ocenia każdą wartość porównania oddzielnie. Każda wartość porównania musi spełniać ograniczenia reguły zabezpieczeń. Na przykład dla tej reguły:

match /mydocuments/{doc} {
  allow read: if resource.data.x > 5;
}

Nieprawidłowy: zapytanie nie gwarantuje, że x > 5 w przypadku wszystkich potencjalnych dokumentów

// These queries will fail
query(db.collection("mydocuments"),
      or(where("x", "==", 1),
         where("x", "==", 6)
      )
    )

query(db.collection("mydocuments"),
      where("x", "in", [1, 3, 6, 42, 99])
    )

Prawidłowy: zapytanie gwarantuje, że x > 5 w przypadku wszystkich potencjalnych dokumentów

query(db.collection("mydocuments"),
      or(where("x", "==", 6),
         where("x", "==", 42)
      )
    )

query(db.collection("mydocuments"),
      where("x", "in", [6, 42, 99, 105, 200])
    )

Ocena ograniczeń w zapytaniach

Twoje reguły zabezpieczeń mogą też akceptować lub odrzucać zapytania na podstawie swoich ograniczeń. Zmienna request.query zawiera właściwości limit, offset i orderBy zapytania. Reguły zabezpieczeń mogą na przykład odrzucać zapytania, które nie ograniczają maksymalnej liczby dokumentów pobieranych do określonego zakresu:

allow list: if request.query.limit <= 10;

Poniższy zestaw reguł pokazuje, jak pisać reguły zabezpieczeń oceniające ograniczenia nałożone w zapytaniach. Ten przykład rozszerza poprzedni zestaw reguł stories o te zmiany:

  • Zestaw reguł dzieli regułę odczytu na reguły dla get i list.
  • Reguła get ogranicza pobieranie pojedynczych dokumentów do dokumentów publicznych lub dokumentów utworzonych przez użytkownika.
  • Reguła list stosuje te same ograniczenia co get, ale do zapytań. Sprawdza też limit zapytań, a potem odrzuca zapytania bez limitu lub z limitem większym niż 10.
  • Zestaw reguł definiuje funkcję authorOrPublished(), która pozwala uniknąć duplikowania kodu.
service cloud.firestore {

  match /databases/{database}/documents {

    match /stories/{storyid} {

      // Returns `true` if the requested story is 'published'
      // or the user authored the story
      function authorOrPublished() {
        return resource.data.published == true || request.auth.uid == resource.data.author;
      }

      // Deny any query not limited to 10 or fewer documents
      // Anyone can query published stories
      // Authors can query their unpublished stories
      allow list: if request.query.limit <= 10 &&
                     authorOrPublished();

      // Anyone can retrieve a published story
      // Only a story's author can retrieve an unpublished story
      allow get: if authorOrPublished();

      // Only a story's author can write to a story
      allow write: if request.auth.uid == resource.data.author;
    }

  }
}

Zapytania dotyczące grup kolekcji i reguły zabezpieczeń

Domyślnie zapytania są ograniczone do 1 kolekcji i pobierają wyniki tylko z tej kolekcji. Za pomocą zapytań dotyczących grup kolekcji możesz uzyskać wyniki z grupy kolekcji złożonej ze wszystkich kolekcji o tym samym identyfikatorze. W tej sekcji dowiesz się, jak zabezpieczyć zapytania dotyczące grupy kolekcji za pomocą reguł zabezpieczeń.

Zabezpieczanie dokumentów i wykonywanie na nich zapytań na podstawie grup kolekcji

W regułach zabezpieczeń musisz wyraźnie zezwolić na zapytania dotyczące grup kolekcji, tworząc regułę dla tej grupy:

  1. Upewnij się, że rules_version = '2'; jest pierwszym wierszem zestawu reguł. Zapytania dotyczące grup kolekcji wymagają nowego rekurencyjnego zachowania symbolu wieloznacznego {name=**} reguł zabezpieczeń w wersji 2.
  2. Napisz regułę dla swojej grupy kolekcji za pomocą atrybutu match /{path=**}/[COLLECTION_ID]/{doc}.

Weźmy na przykład forum podzielone na dokumenty forum zawierające posts podkolekcji:

/forums/{forumid}/posts/{postid}

{
  author: "some_auth_id",
  authorname: "some_username",
  content: "I just read a great story.",
}

W tej aplikacji ich właściciele mogą edytować posty, a uwierzytelnieni użytkownicy mogą je odczytać:

service cloud.firestore {
  match /databases/{database}/documents {
    match /forums/{forumid}/posts/{post} {
      // Only authenticated users can read
      allow read: if request.auth != null;
      // Only the post author can write
      allow write: if request.auth != null && request.auth.uid == resource.data.author;
    }
  }
}

Każdy uwierzytelniony użytkownik może pobierać posty z dowolnego forum:

db.collection("forums/technology/posts").get()

Co jednak w sytuacji, gdy chcesz pokazywać bieżącemu użytkownikowi jego posty na wszystkich forach? Za pomocą zapytania dotyczącego grupy kolekcji możesz pobrać wyniki ze wszystkich kolekcji posts:

var user = firebase.auth().currentUser;

db.collectionGroup("posts").where("author", "==", user.uid).get()

W regułach zabezpieczeń musisz zezwolić na to zapytanie, tworząc regułę odczytu lub listy dla grupy kolekcji posts:

rules_version = '2';
service cloud.firestore {

  match /databases/{database}/documents {
    // Authenticated users can query the posts collection group
    // Applies to collection queries, collection group queries, and
    // single document retrievals
    match /{path=**}/posts/{post} {
      allow read: if request.auth != null;
    }
    match /forums/{forumid}/posts/{postid} {
      // Only a post's author can write to a post
      allow write: if request.auth != null && request.auth.uid == resource.data.author;

    }
  }
}

Pamiętaj jednak, że reguły te będą miały zastosowanie do wszystkich kolekcji o identyfikatorze posts niezależnie od hierarchii. Na przykład te reguły dotyczą wszystkich tych kolekcji posts:

  • /posts/{postid}
  • /forums/{forumid}/posts/{postid}
  • /forums/{forumid}/subforum/{subforumid}/posts/{postid}

Zabezpiecz zapytania dotyczące grup kolekcji na podstawie pola

Podobnie jak w przypadku zapytań w ramach pojedynczego zbioru zapytania dotyczące grup kolekcji muszą też spełniać ograniczenia ustawione przez reguły zabezpieczeń. Na przykład do każdego posta na forum możemy dodać pole published tak jak w przykładzie stories powyżej:

/forums/{forumid}/posts/{postid}

{
  author: "some_auth_id",
  authorname: "some_username",
  content: "I just read a great story.",
  published: false
}

Możemy wtedy utworzyć reguły dla grupy kolekcji posts oparte na stanie published i posta author:

rules_version = '2';
service cloud.firestore {

  match /databases/{database}/documents {

    // Returns `true` if the requested post is 'published'
    // or the user authored the post
    function authorOrPublished() {
      return resource.data.published == true || request.auth.uid == resource.data.author;
    }

    match /{path=**}/posts/{post} {

      // Anyone can query published posts
      // Authors can query their unpublished posts
      allow list: if authorOrPublished();

      // Anyone can retrieve a published post
      // Authors can retrieve an unpublished post
      allow get: if authorOrPublished();
    }

    match /forums/{forumid}/posts/{postid} {
      // Only a post's author can write to a post
      allow write: if request.auth.uid == resource.data.author;
    }
  }
}

Przy użyciu tych reguł klienty internetowe, Apple i Android mogą wykonywać te zapytania:

  • Każdy może pobrać opublikowane posty na forum:

    db.collection("forums/technology/posts").where('published', '==', true).get()
    
  • Każdy może pobrać posty autora opublikowane na wszystkich forach:

    db.collectionGroup("posts").where("author", "==", "some_auth_id").where('published', '==', true).get()
    
  • Autorzy mogą pobierać wszystkie swoje opublikowane i nieopublikowane posty ze wszystkich forów:

    var user = firebase.auth().currentUser;
    
    db.collectionGroup("posts").where("author", "==", user.uid).get()
    

Zabezpiecz dokumenty i wyszukuj dokumenty na podstawie grupy kolekcji i ścieżki dokumentu

W niektórych przypadkach możesz chcieć ograniczyć zapytania dotyczące grup kolekcji na podstawie ścieżki dokumentu. Aby utworzyć te ograniczenia, możesz użyć tych samych technik zabezpieczania dokumentów i wysyłania do nich zapytań na podstawie pól.

Rozważmy aplikację, która śledzi transakcje poszczególnych użytkowników na kilku giełdach i giełdach kryptowalut:

/users/{identyfikator użytkownika}/Wymiana/{identyfikator-usługi}/transactions/{transaction}

{
  amount: 100,
  exchange: 'some_exchange_name',
  timestamp: April 1, 2019 at 12:00:00 PM UTC-7,
  user: "some_auth_id",
}

Zwróć uwagę na pole user. Chociaż wiemy, do którego użytkownika należy dokument transaction ze ścieżki dokumentu, duplikujemy te informacje w każdym dokumencie transaction, bo pozwala nam to robić 2 rzeczy:

  • Twórz zapytania dotyczące grup kolekcji, które są ograniczone do dokumentów, których ścieżka do dokumentu zawiera określony /users/{userid}. Przykład:

    var user = firebase.auth().currentUser;
    // Return current user's last five transactions across all exchanges
    db.collectionGroup("transactions").where("user", "==", user).orderBy('timestamp').limit(5)
    
  • Egzekwuj to ograniczenie do wszystkich zapytań w grupie kolekcji transactions, aby jeden użytkownik nie mógł pobierać dokumentów transaction innego użytkownika.

To ograniczenie egzekwujemy w naszych regułach zabezpieczeń i wprowadzamy weryfikację danych w polu user:

rules_version = '2';
service cloud.firestore {

  match /databases/{database}/documents {

    match /{path=**}/transactions/{transaction} {
      // Authenticated users can retrieve only their own transactions
      allow read: if resource.data.user == request.auth.uid;
    }

    match /users/{userid}/exchange/{exchangeid}/transactions/{transaction} {
      // Authenticated users can write to their own transactions subcollections
      // Writes must populate the user field with the correct auth id
      allow write: if userid == request.auth.uid && request.data.user == request.auth.uid
    }
  }
}

Dalsze kroki