Bezpieczne zapytania o dane

Na tej stronie, na podstawie informacji z artykułów Tworzenie reguł zabezpieczeń i Tworzenie warunków reguł zabezpieczeń, wyjaśniamy, jak Cloud Firestore Security Rules współdziała z zapytaniami. W tym artykule omawiamy szczegółowo, jak reguły zabezpieczeń wpływają na zapytania, które możesz tworzyć, oraz jak zapewnić, aby zapytania te stosowały te same ograniczenia co reguły zabezpieczeń. Na tej stronie znajdziesz też opis sposobu tworzenia reguł zabezpieczeń, które zezwalają na zapytania lub je blokują na podstawie właściwości zapytań, takich jak limitorderBy.

Reguły nie są filtrami

Podczas tworzenia zapytań służących do pobierania dokumentów pamiętaj, że reguły bezpieczeństwa to nie filtry – zapytania działają na zasadzie wszystko albo nic. Aby zaoszczędzić czas i zasoby, Cloud Firestore ocenia zapytanie na podstawie potencjalnego zbioru wyników zamiast rzeczywistych wartości pól wszystkich dokumentów. Jeśli zapytanie może zwrócić dokumenty, do których klient nie ma uprawnień do odczytu, cała prośba kończy się niepowodzeniem.

Zapytania i reguły zabezpieczeń

Jak widać na przykładach poniżej, musisz tworzyć zapytania, które będą pasować do ograniczeń określonych w regułach zabezpieczeń.

Zabezpieczanie dokumentów i wysyłanie zapytań do nich na podstawie auth.uid

Ten przykład pokazuje, jak napisać zapytanie, aby pobrać dokumenty chronione przez regułę bezpieczeństwa. Weź pod uwagę bazę danych zawierającą kolekcję dokumentów story:

/stories/{storyid}

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

Oprócz pól title i content każdy dokument zawiera pola author i published, które służą do kontroli dostępu. W tych przykładach zakładamy, że aplikacja używa Uwierzytelniania Firebase do ustawiania pola author na identyfikator UID użytkownika, który utworzył dokument. Firebase Authentication wypełnia też zmienną request.auth w regułach zabezpieczeń.

Poniższa reguła zabezpieczeń używa zmiennych request.auth i resource.data, aby ograniczyć dostęp do odczytu i zapisu dla każdego elementu story dla jego 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 użytkownik widzi listę storydokumentów, których jest autorem. Możesz oczekiwać, że do wypełnienia tej strony możesz użyć tego zapytania. To zapytanie się nie powiedzie, ponieważ nie zawiera tych samych ograniczeń co reguły zabezpieczeń:

Nieprawidłowe: ograniczenia zapytań nie pasują do ograniczeń reguł bezpieczeństwa

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

Zapytanie kończy się niepowodzeniem nawet wtedy, gdy bieżący użytkownik jest autorem wszystkich dokumentów story. Dzieje się tak, ponieważ Cloud Firestore stosuje reguły zabezpieczeń, oceniając zapytanie na podstawie potencjalnego zbioru wyników, a nie rzeczywistych właściwości dokumentów w bazie danych. Jeśli zapytanie może zawierać dokumenty, które naruszają Twoje reguły zabezpieczeń, nie zostanie ono zrealizowane.

Natomiast to zapytanie zostanie wykonane, ponieważ zawiera ono takie samo ograniczenie pola author jak reguły zabezpieczeń:

Prawidłowy: ograniczenia zapytania pasują do ograniczeń reguł bezpieczeństwa

var user = firebase.auth().currentUser;

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

Bezpieczne przechowywanie dokumentów i wysyłanie zapytań na podstawie pola

Aby lepiej zobrazować interakcję zapytań i reguł, poniżej podajemy reguły zabezpieczeń, które rozszerzają uprawnienia odczytu dla kolekcji stories, aby każdy użytkownik mógł odczytywać dokumenty story, w których polu published ustawiono 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 bezpieczeństwa:

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

Ograniczenie zapytania .where("published", "==", true) gwarantuje, że resource.data.published będzie równe true w przypadku każdego wyniku. Dlatego to zapytanie spełnia reguły zabezpieczeń i ma uprawnienia do odczytu danych.

Zapytania: OR

Podczas sprawdzania zapytania logicznego OR (or, in lub array-contains-any) pod kątem zgodności z regułami Cloud Firestore sprawdza każdą wartość porównawczą osobno. Każda wartość porównawcza musi spełniać ograniczenia reguły bezpieczeństwa. Na przykład w przypadku tej reguły:

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

Nieprawidłowe: zapytanie nie gwarantuje, że x > 5 dla wszystkich możliwych 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])
    )

Właściwe: zapytanie gwarantuje, że x > 5 dla 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])
    )

Ocenianie ograniczeń zapytań

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

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

Podana niżej reguła pokazuje, jak pisać reguły zabezpieczeń, które sprawdzają ograniczenia nałożone na zapytania. W tym przykładzie rozszerzamy poprzedni zbiór reguł stories o te zmiany:

  • Zestaw reguł dzieli regułę dotyczącą odczytu na reguły dotyczące getlist.
  • Reguła get ogranicza wyszukiwanie pojedynczych dokumentów do dokumentów publicznych lub dokumentów utworzonych przez użytkownika.
  • Reguła list stosuje te same ograniczenia co reguła get, ale w przypadku zapytań. Sprawdza też limit zapytań, a następnie odrzuca każde zapytanie bez limitu lub z limitem większym niż 10.
  • Zestaw reguł definiuje funkcję authorOrPublished(), aby 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 grupy zbiorów i reguły zabezpieczeń

Domyślnie zapytania są ograniczone do jednej kolekcji i wyświetlają wyniki tylko z tej kolekcji. Za pomocą zapytań dotyczących grupy kolekcji możesz pobierać wyniki z grupy kolekcji zawierającej wszystkie kolekcje o tym samym identyfikatorze. W tej sekcji opisujemy, jak zabezpieczyć zapytania dotyczące grupy zbiorów za pomocą reguł bezpieczeństwa.

Bezpieczne przechowywanie dokumentów i wysyłanie zapytań do nich na podstawie grup kolekcji

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

  1. Upewnij się, że rules_version = '2'; jest pierwszym wierszem reguł. Zapytania dotyczące grup kolekcji wymagają nowego rekursywnego działania za pomocą symbolu wieloznacznego {name=**} w ramach reguł bezpieczeństwa w wersji 2.
  2. Utwórz regułę dla grupy kolekcji za pomocą match /{path=**}/[COLLECTION_ID]/{doc}.

Weźmy na przykład forum podzielone na forum dokumenty 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 wpisy mogą być edytowane przez ich właścicieli i czytane przez uwierzytelnionych użytkowników:

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 pobrać posty z dowolnego forum:

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

Co jednak, jeśli chcesz wyświetlić 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 swoich regułach zabezpieczeń musisz zezwolić na to zapytanie, tworząc regułę odczytu lub regułę 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 te reguły będą stosowane do wszystkich kolekcji o identyfikatorze posts, niezależnie od hierarchii. Te reguły mają zastosowanie do wszystkich tych kolekcji:posts

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

Bezpieczne zapytania dotyczące zbioru grup na podstawie pola

Podobnie jak zapytania dotyczące pojedynczej kolekcji, zapytania dotyczące grupy kolekcji muszą też spełniać ograniczenia określone przez Twoje reguły zabezpieczeń. Możemy na przykład dodać pole published do każdego wpisu na forum, tak jak w przypadku pola stories w przykładzie powyżej:

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

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

Następnie możemy napisać reguły dla grupy kolekcji posts na podstawie stanu published i postu 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;
    }
  }
}

Dzięki tym regułom klienci internetowi, klienci Apple i klienci Androida 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ć opublikowane przez autora posty na wszystkich forach:

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

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

Bezpieczne przechowywanie dokumentów i wysyłanie zapytań do nich na podstawie grupy kolekcji i ścieżki dokumentu

W niektórych przypadkach możesz ograniczyć zapytania do grupy kolekcji na podstawie ścieżki dokumentu. Aby utworzyć te ograniczenia, możesz użyć tych samych technik zabezpieczania i wyszukiwania dokumentów na podstawie pola.

Weź pod uwagę aplikację, która śledzi transakcje każdego użytkownika na kilku giełdach papierów wartościowych i giełdach kryptowalut:

/users/{userid}/exchange/{exchangeid}/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, który użytkownik jest właścicielem dokumentu transaction na podstawie jego ścieżki, powielamy te informacje w każdym dokumencie transaction, ponieważ pozwala nam to na:

  • Pisać zapytania dotyczące grupy kolekcji, które są ograniczone do dokumentów zawierających określoną ścieżkę dokumentu /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)
    
  • Zastosuj to ograniczenie do wszystkich zapytań dotyczących grupy kolekcji transactions, aby jeden użytkownik nie mógł pobierać dokumentów transactions należących do innego użytkownika.transaction

Wprowadzamy to ograniczenie w regułach bezpieczeństwa i włączamy weryfikację danych dla pola 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