Bezpieczne zapytania o dane

Ta strona jest rozwinięciem koncepcji przedstawionych w artykułach Struktura reguł zabezpieczeń i Pisanie warunków reguł zabezpieczeń. Wyjaśniamy tu, jak Cloud Firestore Security Rules współdziałają z zapytaniami. Przyglądamy się bliżej temu, jak reguły zabezpieczeń wpływają na zapytania, które możesz pisać, i opisujemy, jak zapewnić, że Twoje zapytania będą używać tych samych ograniczeń co reguły zabezpieczeń. Na tej stronie opisujemy też, jak pisać reguły zabezpieczeń, aby zezwalać na zapytania lub je odrzucać na podstawie właściwości zapytania, takich jak limit i orderBy.

Reguły nie są filtrami

Podczas pisania zapytań w celu pobrania dokumentów pamiętaj, że reguły zabezpieczeń nie są filtrami – 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 we wszystkich dokumentach. Jeśli zapytanie może potencjalnie zwrócić dokumenty, do których klient nie ma uprawnień do odczytu, całe żądanie zakończy się niepowodzeniem.

Zapytania i reguły zabezpieczeń

Jak pokazują poniższe przykłady, musisz pisać zapytania tak, aby pasowały do ograniczeń reguł zabezpieczeń.

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

Poniższy przykład pokazuje, jak napisać zapytanie, aby pobrać dokumenty chronione przez regułę zabezpieczeń. Załóżmy, że masz bazę danych, która zawiera 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 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 każdego dokumentu story do 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ę, która wyświetla użytkownikowi listę dokumentów story, których jest autorem. Możesz oczekiwać, że do wypełnienia tej strony możesz użyć tego zapytania. To zapytanie zakończy się jednak niepowodzeniem, ponieważ nie zawiera tych samych ograniczeń co reguły zabezpieczeń:

Nieprawidłowe: ograniczenia zapytania nie pasują do ograniczeń reguł zabezpieczeń

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

Zapytanie zakończy się niepowodzeniem 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 na podstawie potencjalnego zbioru wyników, a nie rzeczywistych właściwości dokumentów w bazie danych. Jeśli zapytanie może potencjalnie zawierać dokumenty, które naruszają reguły zabezpieczeń, zakończy się niepowodzeniem.

Natomiast poniższe zapytanie zakończy się powodzeniem, ponieważ zawiera to samo ograniczenie pola author co reguły zabezpieczeń:

Prawidłowe: ograniczenia zapytania 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 pola

Aby jeszcze lepiej zademonstrować interakcję między zapytaniami a regułami, poniższe reguły zabezpieczeń rozszerzają dostęp do odczytu kolekcji stories, aby umożliwić każdemu użytkownikowi odczytywanie dokumentów story, w których pole published jest ustawione na 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 resource.data.published ma wartość true w przypadku każdego wyniku. Dlatego to zapytanie spełnia reguły zabezpieczeń i może odczytywać dane.

Zapytania OR

Podczas oceniania logicznego zapytania OR (or, in, lub array-contains-any) na podstawie zestawu reguł Cloud Firestore ocenia każdą wartość porównania osobno. Każda wartość porównania musi spełniać ograniczenia reguły zabezpieczeń. 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 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łowe: 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

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

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

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

  • Zestaw reguł dzieli regułę odczytu na reguły get i list.
  • Reguła get ogranicza pobieranie pojedynczych dokumentów do dokumentów publicznych lub dokumentów, których autorem jest użytkownik.
  • Reguła list stosuje te same ograniczenia co get, ale w przypadku zapytań. Sprawdza też limit zapytania, a następnie odrzuca wszystkie zapytania 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 kolekcji i reguły zabezpieczeń

Domyślnie zapytania są ograniczone do pojedynczej kolekcji i pobierają wyniki tylko z tej kolekcji. Za pomocą zapytań dotyczących grupy kolekcji możesz pobierać wyniki z grupy kolekcji składającej się ze wszystkich kolekcji o tym samym identyfikatorze. W tej sekcji opisujemy, 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 grupy kolekcji, pisząc regułę dla grupy kolekcji:

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

Załóżmy na przykład, że masz forum zorganizowane w dokumenty forum zawierające kolekcje podrzędne posts:

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

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

W tej aplikacji umożliwiamy właścicielom edytowanie postów, a uwierzytelnionym użytkownikom – ich odczytywanie:

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()

Ale co zrobić, jeśli chcesz pokazać bieżącemu użytkownikowi jego posty ze wszystkich forów? Możesz użyć zapytania dotyczącego grupy kolekcji, aby pobrać wyniki ze wszystkich posts kolekcji:

var user = firebase.auth().currentUser;

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

W regułach zabezpieczeń musisz zezwolić na to zapytanie, pisząc regułę odczytu lub list 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ą obowiązywać w przypadku wszystkich kolekcji o identyfikatorze posts niezależnie od hierarchii. Na przykład te reguły mają zastosowanie do wszystkich tych kolekcji posts:

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

Zabezpieczanie zapytań dotyczących grupy kolekcji na podstawie pola

Podobnie jak zapytania dotyczące pojedynczej kolekcji, zapytania dotyczące grupy kolekcji muszą też spełniać ograniczenia określone w regułach zabezpieczeń. Na przykład możemy dodać pole published do każdego posta na forum, 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
}

Następnie możemy napisać reguły dla grupy kolekcji posts na podstawie stanu published i author posta:

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, Apple i Android mogą wykonywać te zapytania:

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

    db.collection("forums/technology/posts").where('published', '==', true).get()
    
  • Każdy może pobierać opublikowane posty autora ze wszystkich forów:

    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()
    

Zabezpieczanie dokumentów i wykonywanie na nich zapytań na podstawie grupy kolekcji i ścieżki dokumentu

W niektórych przypadkach możesz chcieć ograniczyć zapytania dotyczące grupy kolekcji na podstawie ścieżki dokumentu. Aby utworzyć te ograniczenia, możesz użyć tych samych metod zabezpieczania dokumentów i wykonywania na nich zapytań na podstawie pola.

Załóżmy, że masz aplikację, która śledzi transakcje każdego użytkownika na kilku giełdach akcji i 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. Mimo że wiemy, który użytkownik jest właścicielem dokumentu transaction na podstawie ścieżki dokumentu, powielamy te informacje w każdym dokumencie transaction, ponieważ pozwala nam to na 2 rzeczy:

  • Pisanie zapytań dotyczących grupy kolekcji, które są ograniczone do dokumentów zawierających w ścieżce dokumentu konkretny ciąg /users/{userid}. Na 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)
    
  • Wymuszanie tego ograniczenia w przypadku wszystkich zapytań dotyczących grupy kolekcji transactions , aby jeden użytkownik nie mógł pobierać dokumentów transaction innego użytkownika.

Wymuszamy to ograniczenie w regułach zabezpieczeń i uwzględniamy 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