Kontrolowanie dostępu do określonych pól

Ta strona rozwija koncepcje przedstawione w artykułach Strukturyzowanie reguł zabezpieczeńPisanie warunków dla reguł zabezpieczeń, aby wyjaśnić, jak za pomocą Cloud Firestore Security Rules tworzyć reguły, które umożliwiają klientom wykonywanie operacji na niektórych polach w dokumencie, ale nie na innych.

Czasami możesz chcieć kontrolować zmiany w dokumencie nie na poziomie dokumentu, ale na poziomie pola.

Możesz na przykład zezwolić klientowi na tworzenie i zmienianie dokumentu, ale nie zezwalać mu na edytowanie niektórych pól w tym dokumencie. Możesz też wymagać, aby każdy dokument tworzony przez klienta zawsze zawierał określony zestaw pól. Z tego przewodnika dowiesz się, jak wykonać niektóre z tych zadań za pomocą Cloud Firestore Security Rules.

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

Odczyty w Cloud Firestore są wykonywane na poziomie dokumentu. Możesz pobrać cały dokument lub nie pobrać nic. Nie można pobrać części dokumentu. Nie można używać samych reguł zabezpieczeń, aby uniemożliwić użytkownikom odczytywanie określonych pól w dokumencie.

Jeśli w dokumencie są pola, które chcesz ukryć przed niektórymi użytkownikami, najlepiej umieścić je w osobnym dokumencie. Możesz na przykład utworzyć dokument w privatepodkolekcji w ten sposób:

/employees/{emp_id}

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

/employees/{emp_id}/private/finances

    salary: 80000,
    bonus_mult: 1.25,
    perf_review: 4.2

Następnie możesz dodać reguły bezpieczeństwa, które będą miały różne poziomy dostępu dla tych dwóch kolekcji. W tym przykładzie używamy niestandardowych roszczeń autoryzacyjnych, aby określić, że tylko użytkownicy z niestandardowym roszczeniem autoryzacyjnym role równym Finance mogą wyświetlać 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 nie ma schematu, co oznacza, że na poziomie bazy danych nie ma ograniczeń dotyczących pól, które może zawierać dokument. Ta elastyczność może ułatwiać tworzenie aplikacji, ale czasami warto zadbać o to, aby klienci mogli tworzyć tylko dokumenty zawierające określone pola lub nie mogli tworzyć dokumentów zawierających inne pola.

Możesz je utworzyć, sprawdzając metodę keys obiektu request.resource.data. To lista wszystkich pól, w których klient próbuje zapisać dane w tym nowym dokumencie. Łącząc ten zestaw pól z funkcjami takimi jak hasOnly() lub hasAny(), możesz dodać logikę, która ogranicza typy dokumentów, jakie użytkownik może dodawać do Cloud Firestore.

Wymaganie określonych pól w nowych dokumentach

Załóżmy, że chcesz mieć pewność, że wszystkie dokumenty utworzone w kolekcji restaurant zawierają co najmniej pola name, locationcity. Możesz to zrobić, wywołując 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']);
    }
  }
}

Umożliwia to tworzenie restauracji z użyciem innych pól, ale zapewnia, że wszystkie dokumenty utworzone przez klienta zawierają co najmniej te 3 pola.

Blokowanie określonych pól w nowych dokumentach

Podobnie możesz uniemożliwić klientom tworzenie dokumentów zawierających określone pola, używając tagu hasAny() w odniesieniu do listy zabronionych pól. Ta metoda zwraca wartość „true”, jeśli dokument zawiera którekolwiek z tych pól, więc prawdopodobnie będziesz chcieć zanegować wynik, aby zabronić używania określonych pól.

Na przykład w poniższym przykładzie klienci nie mogą tworzyć dokumentu zawierającego pole average_score ani rating_count, ponieważ te pola zostaną dodane przez wywołanie serwera w późniejszym czasie.

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 w przypadku nowych dokumentów

Zamiast zabraniać używania określonych pól w nowych dokumentach, możesz utworzyć listę tylko tych pól, które są w nich wyraźnie dozwolone. Następnie możesz użyć funkcji hasOnly(), aby mieć pewność, że wszystkie nowe dokumenty będą zawierać tylko te pola (lub ich podzbiór), 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 łączyć operacje hasAllhasOnly w regułach bezpieczeństwa, aby wymagać niektórych pól i zezwalając na inne. Na przykład ten przykład wymaga, aby wszystkie nowe dokumenty zawierały pola name, locationcity, a opcjonalnie dopuszcza pola address, hourscuisine.

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 rzeczywistości możesz przenieść tę logikę do funkcji pomocniczej, aby uniknąć duplikowania kodu i łatwiej łączyć pola opcjonalne i wymagane w jedną listę, jak w tym przykładzie:

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ą w zakresie bezpieczeństwa jest zezwalanie klientom na edytowanie tylko niektórych pól. Nie możesz tego zrobić, patrząc tylko na listę request.resource.data.keys() opisaną w poprzedniej sekcji, ponieważ ta lista przedstawia cały dokument po aktualizacji i zawiera pola, których klient nie zmienił.

Jeśli jednak użyjesz 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 zawiera wszystkie zmiany między dwiema różnymi mapami.

Wywołując metodę affectedKeys() na tym obiekcie mapDiff, możesz uzyskać zbiór pól, które zostały zmienione podczas edycji. Następnie możesz użyć funkcji takich jak hasOnly() lub hasAny(), aby sprawdzić, czy ten zbiór zawiera określone elementy.

Zapobieganie zmianom w niektórych polach

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

Możesz na przykład zezwolić klientom na aktualizowanie informacji o restauracji, ale nie na zmianę średniej oceny ani liczby opinii.

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']));
    }
  }
}

umożliwiać zmianę tylko niektórych pól,

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

Zamiast na przykład blokować pola average_scorerating_count, możesz utworzyć reguły zabezpieczeń, które pozwolą klientom zmieniać tylko pola name, location, city, address, hourscuisine.

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 przyszłej wersji aplikacji dokumenty restauracji będą zawierać pole telephone, próby edytowania tego pola będą kończyć się niepowodzeniem, dopóki nie wrócisz i nie dodasz tego pola do listy hasOnly() w regułach bezpieczeństwa.

Wymuszanie typów pól

Kolejnym efektem braku schematu w Cloud Firestore jest to, że na poziomie bazy danych nie ma wymogu dotyczącego typów danych, które można przechowywać w określonych polach. Możesz to wymusić w regułach zabezpieczeń za pomocą operatora is.

Na przykład ta reguła zabezpieczeń wymusza, aby pole scorew opinii było liczbą całkowitą, pola headline, contentauthor_name były ciągami znaków, a pole review_date było 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 operatora is to bool, bytes, float, int, list, latlng, number, path, map, stringtimestamp. Operator is obsługuje też typy danych constraint, duration, set i map_diff, ale ponieważ są one generowane przez sam język reguł zabezpieczeń, a nie przez klientów, rzadko używa się ich w większości praktycznych zastosowań.

Typy danych listmap nie obsługują typów ogólnych ani argumentów typu. Innymi słowy, możesz używać reguł zabezpieczeń, 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żywać reguł bezpieczeństwa, aby wymuszać wartości typów dla określonych wpisów na liście lub w mapie (używając odpowiednio notacji nawiasowej lub nazw kluczy), ale nie ma skrótu, który umożliwiałby wymuszenie typów danych wszystkich elementów w mapie lub na liście jednocześnie.

Na przykład te reguły zapewniają, że pole tags w dokumencie zawiera listę, a pierwszy wpis jest ciągiem tekstowym. Zapewnia też, że pole product zawiera mapę, która z kolei zawiera nazwę produktu (ciąg tekstowy) i ilość (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 podczas tworzenia i aktualizowania dokumentu. Dlatego warto utworzyć funkcję pomocniczą, którą można wywoływać zarówno w sekcjach tworzenia, jak i aktualizowania reguł zabezpieczeń.

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 w przypadku pól opcjonalnych

Pamiętaj, że wywołanie funkcji request.resource.data.foo w dokumencie, w którym nie ma funkcji foo, powoduje błąd, a zatem każda reguła bezpieczeństwa, która wywołuje tę funkcję, odrzuci żądanie. Możesz sobie z tym poradzić, używając metody get na stronie request.resource.data. Metoda get umożliwia podanie argumentu domyślnego dla pola pobieranego z mapy, jeśli to pole nie istnieje.

Jeśli na przykład dokumenty do sprawdzenia zawierają też opcjonalne pole photo_url i opcjonalne pole tags, które mają być odpowiednio ciągami znaków i listami, możesz to osiągnąć, przepisując funkcję reviewFieldsAreValidTypes w następujący sposób:

  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;
  }

Odrzuca dokumenty, w których występuje pole tags, ale nie jest ono listą, a jednocześnie zezwala na dokumenty, które nie zawierają pola tags (ani photo_url).

Częściowe zapisywanie jest niedozwolone

Ostatnia uwaga dotycząca Cloud Firestore Security Rules: albo zezwalają one klientowi na wprowadzenie zmiany w dokumencie, albo odrzucają całą edycję. Nie możesz tworzyć reguł zabezpieczeń, które akceptują zapisywanie w niektórych polach dokumentu, a odrzucają zapisywanie w innych polach w ramach tej samej operacji.