Kontroluj dostęp do określonych pól

Ta strona opiera się na koncepcjach z Strukturyzacji reguł bezpieczeństwa i Pisania warunków dla reguł bezpieczeństwa , aby wyjaśnić, w jaki sposób można używać reguł bezpieczeństwa Cloud Firestore do tworzenia reguł, które pozwalają klientom wykonywać operacje na niektórych polach dokumentu, ale nie na innych.

Może się zdarzyć, że będziesz chciał kontrolować zmiany w dokumencie nie na poziomie dokumentu, ale na poziomie pola.

Na przykład możesz zezwolić klientowi na utworzenie lub zmianę dokumentu, ale nie pozwolić mu na edycję niektórych pól w tym dokumencie. Możesz też wymusić, aby każdy dokument zawsze tworzony przez klienta zawierał określony zestaw pól. W tym przewodniku opisano, jak wykonać niektóre z tych zadań za pomocą reguł bezpieczeństwa Cloud Firestore.

Zezwalanie na dostęp do odczytu tylko dla określonych pól

Odczyty w Cloud Firestore wykonywane są na poziomie dokumentu. Albo pobierasz cały dokument, albo nie pobierasz niczego. Nie ma możliwości odzyskania częściowego dokumentu. Niemożliwe jest użycie samych reguł bezpieczeństwa, aby uniemożliwić użytkownikom czytanie określonych pól w dokumencie.

Jeśli w dokumencie znajdują się pewne pola, które chcesz ukryć przed niektórymi użytkownikami, najlepszym sposobem byłoby umieszczenie ich w osobnym dokumencie. Możesz na przykład rozważyć utworzenie dokumentu w private podkolekcji w następujący sposób:

/pracownicy/{emp_id}

  name: "Alice Hamilton",
  department: 461,
  start_date: <timestamp>

/employees/{emp_id}/private/finanse

    salary: 80000,
    bonus_mult: 1.25,
    perf_review: 4.2

Następnie możesz dodać reguły bezpieczeństwa, które mają różne poziomy dostępu dla obu kolekcji. W tym przykładzie używamy niestandardowych oświadczeń autoryzacyjnych , aby powiedzieć, że tylko użytkownicy z role niestandardowych oświadczeń autoryzacyjnych równą Finance mogą przeglądać informacje finansowe pracownika.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow any logged in user to view the public employee data
    match /employees/{emp_id} {
      allow read: if request.resource.auth != null
      // Allow only users with the custom auth claim of "Finance" to view
      // the employee's financial data
      match /private/finances {
        allow read: if request.resource.auth &&
          request.resource.auth.token.role == 'Finance'
      }
    }
  }
}

Ograniczanie pól podczas tworzenia dokumentu

Cloud Firestore jest bezschematowy, co oznacza, że ​​na poziomie bazy danych nie ma żadnych ograniczeń dotyczących pól, które zawiera dokument. Chociaż ta elastyczność może ułatwić programowanie, będą chwile, kiedy będziesz chciał mieć pewność, że klienci będą mogli tworzyć tylko dokumenty zawierające określone pola lub nie zawierające innych pól.

Możesz utworzyć te reguły, sprawdzając metodę keys obiektu request.resource.data . To jest lista wszystkich pól, które klient próbuje zapisać w nowym dokumencie. Łącząc ten zestaw pól z funkcjami takimi jak hasOnly() lub hasAny() , możesz dodać logikę ograniczającą typy dokumentów, które użytkownik może dodać do Cloud Firestore.

Wymaganie określonych pól w nowych dokumentach

Załóżmy, że chcesz się upewnić, że wszystkie dokumenty utworzone w kolekcji restaurant zawierają co najmniej pole name , location i city . Możesz to zrobić, wywołując funkcję hasAll() na liście kluczy w nowym dokumencie.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document contains a name
    // location, and city field
    match /restaurant/{restId} {
      allow create: if request.resource.data.keys().hasAll(['name', 'location', 'city']);
    }
  }
}

Pozwala to na tworzenie restauracji również z innymi polami, ale gwarantuje, że wszystkie dokumenty utworzone przez klienta będą zawierały przynajmniej te trzy pola.

Zabranianie określonych pól w nowych dokumentach

Podobnie możesz uniemożliwić klientom tworzenie dokumentów zawierających określone pola, używając hasAny() w odniesieniu do listy zabronionych pól. Ta metoda ma wartość true, jeśli dokument zawiera którekolwiek z tych pól, więc prawdopodobnie chcesz zanegować wynik, aby zabronić niektórych pól.

Na przykład w poniższym przykładzie klientom nie wolno tworzyć dokumentu zawierającego pole average_score lub rating_count , ponieważ pola te zostaną dodane później przez wywołanie serwera.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document does *not*
    // contain an average_score or rating_count field.
    match /restaurant/{restId} {
      allow create: if (!request.resource.data.keys().hasAny(
        ['average_score', 'rating_count']));
    }
  }
}

Tworzenie listy dozwolonych pól dla nowych dokumentów

Zamiast zabraniać niektórych pól w nowych dokumentach, możesz utworzyć listę tylko tych pól, które są wyraźnie dozwolone w nowych dokumentach. Następnie możesz użyć funkcji hasOnly() , aby upewnić się, że wszystkie nowe utworzone dokumenty zawierają tylko te pola (lub podzbiór tych pól), a nie inne.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document doesn't contain
    // any fields besides the ones listed below.
    match /restaurant/{restId} {
      allow create: if (request.resource.data.keys().hasOnly(
        ['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

Łączenie pól wymaganych i opcjonalnych

Możesz połączyć operacje hasAll i hasOnly w regułach bezpieczeństwa, aby wymagać niektórych pól i zezwalać na inne. Na przykład ten przykład wymaga, aby wszystkie nowe dokumenty zawierały pola name , location i city , a opcjonalnie dopuszcza pola address , hours i cuisine .

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document has a name,
    // location, and city field, and optionally address, hours, or cuisine field
    match /restaurant/{restId} {
      allow create: if (request.resource.data.keys().hasAll(['name', 'location', 'city'])) &&
       (request.resource.data.keys().hasOnly(
           ['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

W prawdziwym scenariuszu możesz chcieć przenieść tę logikę do funkcji pomocniczej, aby uniknąć duplikowania kodu i łatwiej połączyć pola opcjonalne i wymagane w jedną listę, na przykład:

service cloud.firestore {
  match /databases/{database}/documents {
    function verifyFields(required, optional) {
      let allAllowedFields = required.concat(optional);
      return request.resource.data.keys().hasAll(required) &&
        request.resource.data.keys().hasOnly(allAllowedFields);
    }
    match /restaurant/{restId} {
      allow create: if verifyFields(['name', 'location', 'city'],
        ['address', 'hours', 'cuisine']);
    }
  }
}

Ograniczanie pól podczas aktualizacji

Powszechną praktyką bezpieczeństwa jest zezwalanie klientom na edycję tylko niektórych pól, a innych nie. Nie można tego osiągnąć wyłącznie poprzez przeglądanie listy request.resource.data.keys() opisanej w poprzedniej sekcji, ponieważ ta lista reprezentuje kompletny dokument, tak jak wyglądałby po aktualizacji, a zatem zawierałby pola, których nie zrobił klient zmiana.

Jeśli jednak chcesz użyć funkcji diff() , możesz porównać request.resource.data z obiektem resource.data , który reprezentuje dokument w bazie danych przed aktualizacją. Spowoduje to utworzenie obiektu mapDiff , który jest obiektem zawierającym wszystkie zmiany pomiędzy dwiema różnymi mapami.

Wywołując metodę affectedKeys() w tym mapDiff, możesz uzyskać zestaw pól, które zostały zmienione podczas edycji. Następnie możesz użyć funkcji takich jak hasOnly() lub hasAny() , aby upewnić się, że ten zestaw zawiera (lub nie) określone elementy.

Zapobieganie zmianie niektórych pól

Używając metody hasAny() na zestawie wygenerowanym przez affectedKeys() i następnie negując wynik, możesz odrzucić każde żądanie klienta próbującego zmienić pola, których nie chcesz zmieniać.

Możesz na przykład zezwolić klientom na aktualizowanie informacji o restauracji, ale nie zmieniać ich średniego wyniku ani liczby recenzji.

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
      // Allow the client to update a document only if that document doesn't
      // change the average_score or rating_count fields
      allow update: if (!request.resource.data.diff(resource.data).affectedKeys()
        .hasAny(['average_score', 'rating_count']));
    }
  }
}

Zezwalanie na zmianę tylko niektórych pól

Zamiast określać pola, których nie chcesz zmieniać, możesz także użyć funkcji hasOnly() w celu określenia listy pól, które chcesz zmienić. Jest to ogólnie uważane za bezpieczniejsze, ponieważ zapisy w nowych polach dokumentu są domyślnie zabronione, dopóki wyraźnie nie zezwolisz na to w regułach bezpieczeństwa.

Na przykład, zamiast blokować pola average_score i rating_count , możesz utworzyć reguły bezpieczeństwa, które pozwolą klientom zmieniać tylko name , location , city , address , hours i cuisine .

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
    // Allow a client to update only these 6 fields in a document
      allow update: if (request.resource.data.diff(resource.data).affectedKeys()
        .hasOnly(['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

Oznacza to, że jeśli w jakiejś przyszłej wersji aplikacji dokumenty restauracji będą zawierać pole telephone , próby edycji tego pola nie powiodą się, dopóki nie wrócisz i nie dodasz tego pola do listy hasOnly() w regułach bezpieczeństwa.

Egzekwowanie typów pól

Innym skutkiem braku schematu Cloud Firestore jest brak egzekwowania na poziomie bazy danych tego, jakie typy danych mogą być przechowywane w określonych polach. Można to jednak wymusić w regułach bezpieczeństwa za pomocą operatora is .

Na przykład poniższa reguła bezpieczeństwa wymusza, aby pole score recenzji było liczbą całkowitą, headline , content i author_name były ciągami znaków, a review_date była sygnaturą czasową.

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
      // Restaurant rules go here...
      match /review/{reviewId} {
        allow create: if (request.resource.data.score is int &&
          request.resource.data.headline is string &&
          request.resource.data.content is string &&
          request.resource.data.author_name is string &&
          request.resource.data.review_date is timestamp
        );
      }
    }
  }
}

Prawidłowe typy danych dla is is to bool , bytes , float , int , list , latlng , number , path , map , string i timestamp . Operator is obsługuje także typy danych constraint , duration , set i map_diff , ale ponieważ są one generowane przez sam język reguł bezpieczeństwa, a nie generowane przez klientów, rzadko używa się ich w większości praktycznych zastosowań.

typy danych list i map nie obsługują argumentów ogólnych ani typów. Innymi słowy, możesz użyć reguł bezpieczeństwa, aby wymusić, aby określone pole zawierało listę lub mapę, ale nie możesz wymusić, aby pole zawierało listę wszystkich liczb całkowitych lub wszystkich ciągów znaków.

Podobnie możesz użyć reguł bezpieczeństwa, aby wymusić wartości typu dla określonych wpisów na liście lub mapie (używając odpowiednio notacji nawiasów lub nazw kluczy), ale nie ma skrótu, aby wymusić typy danych wszystkich elementów na mapie lub liście pod adresem raz.

Na przykład poniższe reguły zapewniają, że pole tags w dokumencie zawiera listę, a pierwszy wpis jest ciągiem znaków. Zapewnia również, że pole product zawiera mapę, która z kolei zawiera nazwę produktu będącą ciągiem znaków oraz ilość będącą liczbą całkowitą.

service cloud.firestore {
  match /databases/{database}/documents {
  match /orders/{orderId} {
    allow create: if request.resource.data.tags is list &&
      request.resource.data.tags[0] is string &&
      request.resource.data.product is map &&
      request.resource.data.product.name is string &&
      request.resource.data.product.quantity is int
      }
    }
  }
}

Typy pól muszą być wymuszane zarówno podczas tworzenia, jak i aktualizowania dokumentu. Dlatego warto rozważyć utworzenie funkcji pomocniczej, którą można wywołać zarówno w sekcjach tworzenia, jak i aktualizacji reguł bezpieczeństwa.

service cloud.firestore {
  match /databases/{database}/documents {

  function reviewFieldsAreValidTypes(docData) {
     return docData.score is int &&
          docData.headline is string &&
          docData.content is string &&
          docData.author_name is string &&
          docData.review_date is timestamp;
  }

   match /restaurant/{restId} {
      // Restaurant rules go here...
      match /review/{reviewId} {
        allow create: if reviewFieldsAreValidTypes(request.resource.data) &&
          // Other rules may go here
        allow update: if reviewFieldsAreValidTypes(request.resource.data) &&
          // Other rules may go here
      }
    }
  }
}

Wymuszanie typów dla pól opcjonalnych

Należy pamiętać, że wywołanie request.resource.data.foo w dokumencie, w którym foo nie istnieje, powoduje błąd i dlatego każda reguła bezpieczeństwa wykonująca to wywołanie odrzuci żądanie. Możesz poradzić sobie z tą sytuacją, używając metody get na request.resource.data . Metoda get umożliwia podanie domyślnego argumentu dla pola, które pobierasz z mapy, jeśli to pole nie istnieje.

Na przykład, jeśli dokumenty recenzji zawierają również opcjonalne pole photo_url i opcjonalne pole tags , dla których chcesz sprawdzić, czy są to odpowiednio ciągi znaków i listy, możesz to osiągnąć, przepisując funkcję reviewFieldsAreValidTypes na mniej więcej następującą:

  function reviewFieldsAreValidTypes(docData) {
     return docData.score is int &&
          docData.headline is string &&
          docData.content is string &&
          docData.author_name is string &&
          docData.review_date is timestamp &&
          docData.get('photo_url', '') is string &&
          docData.get('tags', []) is list;
  }

To odrzuca dokumenty, w których istnieją tags , ale nie są listą, jednocześnie zezwalając na dokumenty, które nie zawierają pola tags (lub photo_url ).

Częściowe zapisy nigdy nie są dozwolone

Ostatnia uwaga na temat reguł bezpieczeństwa Cloud Firestore jest taka, że ​​albo pozwalają klientowi na wprowadzenie zmian w dokumencie, albo odrzucają całą edycję. Nie można utworzyć reguł bezpieczeństwa, które akceptują zapisy w niektórych polach dokumentu i odrzucają inne w tej samej operacji.