Kontrolowanie dostępu do określonych pól

Na tej stronie rozwijamy zagadnienia omówione w artykułach Ustrukturyzowanie reguł zabezpieczeńTworzenie warunków reguł zabezpieczeń, aby wyjaśnić, jak za pomocą funkcji Cloud Firestore Security Rules można tworzyć reguły, które umożliwiają klientom wykonywanie operacji na niektórych polach w dokumencie, ale nie na innych.

Czasami może się zdarzyć, że chcesz kontrolować zmiany w dokumencie nie na poziomie dokumentu, ale na poziomie pola.

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

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

Czytanie w Cloud Firestore odbywa się na poziomie dokumentu. Możesz albo pobrać cały dokument, albo nic nie pobrać. Nie można pobrać części dokumentu. Korzystając tylko z reguł zabezpieczeń, nie można uniemożliwić użytkownikom odczytywania 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 podkolekcji private 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 mają różne poziomy dostępu do 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 jest bezschematowy, co oznacza, że na poziomie bazy danych nie ma żadnych ograniczeń dotyczących pól zawartych w dokumencie. Chociaż ta elastyczność może ułatwić rozwój, mogą pojawić się sytuacje, w których będziesz mieć pewność, że klienci mogą tworzyć tylko dokumenty zawierające określone pola lub nie zawierające innych pól.

Możesz je tworzyć, analizując metodę keys obiektu request.resource.data. To lista wszystkich pól, do których klient próbuje zapisać dane w 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, które użytkownik może dodać do pola 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 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']);
    }
  }
}

Dzięki temu restauracje mogą być tworzone z innymi polami, ale jednocześnie zapewnia, że wszystkie dokumenty utworzone przez klienta zawierają co najmniej te 3 pola.

Zabranie możliwości używania 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 pól zakazanych. Ta metoda zwraca wartość true, jeśli dokument zawiera dowolne z tych pól, więc prawdopodobnie chcesz zanegować wynik, aby zabronić używania niektórych pól.

Na przykład w następującym 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 nowych dokumentach

Zamiast zabraniać używania określonych 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 mieć pewność, że nowe dokumenty zawierają tylko te pola (lub ich podzbiór), a nie żadne 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

W swoich regułach bezpieczeństwa możesz łączyć operacje hasAll i hasOnly, aby wymagać niektórych pól i zezwalać na inne. Na przykład w tym przykładzie wymagamy, aby wszystkie nowe dokumenty zawierały pola name, locationcity, a opcjonalnie dopuszczamy 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 praktyce możesz przenieść tę logikę do funkcji pomocniczej, aby uniknąć powielania kodu i łatwiej łączyć pola opcjonalne i wymagane w jednej liście, 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 edytowanie tylko niektórych pól, a nie wszystkich. Nie możesz tego zrobić, patrząc tylko na listę request.resource.data.keys() opisaną w poprzedniej sekcji, ponieważ ta lista przedstawia kompletny dokument w takiej postaci, jaką ma po aktualizacji, a więc zawiera pola, które nie zostały zmienione przez klienta.

Jeśli jednak użyjesz funkcji diff(), możesz porównać obiekt 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 2 mapami.

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

zapobieganie zmianie niektórych pól;

Używając metody hasAny() na zbiorze wygenerowanym przez funkcję affectedKeys(), a następnie negując wynik, możesz odrzucić dowolne żą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']));
    }
  }
}

zezwalać na zmianę tylko niektórych pól;

Zamiast podawania pól, których nie chcesz zmieniać, możesz też użyć funkcji hasOnly(), aby określić listę 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 swoich regułach bezpieczeństwa.

Zamiast zablokować pola average_scorerating_count, możesz utworzyć reguły bezpieczeństwa, 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 zawierają pole telephone, próby jego edycji będą się nieudać, dopóki nie wrócisz do listy reguł zabezpieczeń i nie dodasz tego pola do listy hasOnly().

Wymuszanie typów pól

Kolejnym efektem braku schematu w Cloud Firestore jest to, że na poziomie bazy danych nie ma żadnych zasad dotyczących tego, jakie typy danych można przechowywać w konkretnych polach. Możesz jednak wymusić to w regułach zabezpieczeń za pomocą operatora is.

Na przykład ta reguła bezpieczeństwa nakazuje, aby pole score w opinii było liczbą całkowitą, a pola headline, content i author_name miały być ciągami znaków, a pole review_date ma być 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 isobsługuje też typy danych constraint, duration, setmap_diff, ale ponieważ są one generowane przez sam język reguł zabezpieczeń, a nie przez klientów, rzadko używa się ich w praktycznych zastosowaniach.

Typy danych listmap nie obsługują ogólników ani argumentów typu. Innymi słowy, możesz użyć reguł zabezpieczeń, aby wymusić, aby dane pole zawierało listę lub mapę, ale nie możesz wymusić, aby zawierało ono 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 konkretnych wpisów na liście lub mapie (odpowiednio za pomocą nawiasów lub nazw kluczy), ale nie ma skrótu, który pozwalałby na wymuszenie typów danych wszystkich elementów na liście lub mapie naraz.

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 będącą ciągiem tekstowym i 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 podczas tworzenia i aktualizowania dokumentu. Dlatego warto utworzyć funkcję pomocniczą, którą można wywołać w sekcji tworzenia 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 istnieje element foo, powoduje błąd, dlatego każda reguła zabezpieczeń wykonująca to wywołanie odrzuci żądanie. Możesz rozwiązać tę sytuację, używając metody getrequest.resource.data. Metoda get umożliwia podanie argumentu domyślnego dla pola, które pobierasz z mapy, jeśli to pole nie istnieje.

Jeśli na przykład dokumenty do sprawdzenia zawierają opcjonalne pole photo_url i opcjonalne pole tags, które chcesz zweryfikować jako odpowiednio ciągi tekstowe i listy, możesz to zrobić, przepisując funkcję reviewFieldsAreValidTypes na coś takiego:

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

Ta reguła odrzuca dokumenty, w których występuje element tags, ale nie jest on listą, a jednocześnie zezwala na dokumenty, które nie zawierają pola tags (ani photo_url).

Częściowe zapisy są zawsze niedozwolone.

Ostatnia uwaga dotycząca Cloud Firestore Security Rules: albo pozwalają klientowi wprowadzić zmianę w dokumencie, albo odrzucają całą zmianę. Nie możesz utworzyć reguł zabezpieczeń, które zezwalają na zapisywanie w niektórych polach dokumentu, a w tym samym czasie odrzucają zapisywanie w innych polach.