Контролируйте доступ к определенным полям

Эта страница основана на концепциях структурирования правил безопасности и написания условий для правил безопасности, чтобы объяснить, как вы можете использовать Cloud Firestore Security Rules для создания правил, которые позволяют клиентам выполнять операции с некоторыми полями в документе, но не с другими.

Могут быть случаи, когда вы захотите контролировать изменения в документе не на уровне документа, а на уровне поля.

Например, вы можете разрешить клиенту создавать или изменять документ, но не разрешать ему редактировать определенные поля в этом документе. Или вы можете захотеть обеспечить, чтобы любой документ, создаваемый клиентом, всегда содержал определенный набор полей. В этом руководстве рассказывается, как можно выполнить некоторые из этих задач с помощью Cloud Firestore Security Rules .

Разрешение доступа на чтение только для определенных полей

Чтение в Cloud Firestore выполняется на уровне документа. Вы либо получаете весь документ, либо ничего не получаете. Невозможно получить частичный документ. Невозможно использовать только правила безопасности, чтобы запретить пользователям читать определенные поля в документе.

Если в документе есть определенные поля, которые вы хотите скрыть от некоторых пользователей, лучше всего поместить их в отдельный документ. Например, вы можете рассмотреть возможность создания документа в private подколлекции следующим образом:

/сотрудники/{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

Затем вы можете добавить правила безопасности с разными уровнями доступа для двух коллекций. В этом примере мы используем настраиваемые утверждения проверки подлинности , чтобы сказать, что только пользователи с role настраиваемого утверждения проверки подлинности, равной Finance могут просматривать финансовую информацию сотрудника.

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

Ограничение полей при создании документа

Cloud Firestore не имеет схемы, а это означает, что на уровне базы данных нет ограничений на то, какие поля содержит документ. Хотя такая гибкость может облегчить разработку, бывают случаи, когда вам нужно убедиться, что клиенты могут создавать только документы, содержащие определенные поля, или не содержать других полей.

Вы можете создать эти правила, изучив метод keys объекта request.resource.data . Это список всех полей, которые клиент пытается записать в этом новом документе. Комбинируя этот набор полей с такими функциями, как hasOnly() или hasAny() , вы можете добавить логику, ограничивающую типы документов, которые пользователь может добавлять в Cloud Firestore .

Требование определенных полей в новых документах

Допустим, вы хотите убедиться, что все документы, созданные в коллекции restaurant содержат как минимум поля name , location и city . Вы можете сделать это, вызвав hasAll() для списка ключей в новом документе.

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

Это позволяет создавать рестораны и с другими полями, но гарантирует, что все документы, созданные клиентом, будут содержать как минимум эти три поля.

Запрет определенных полей в новых документах

Аналогичным образом вы можете запретить клиентам создавать документы, содержащие определенные поля, используя hasAny() для списка запрещенных полей. Этот метод оценивается как true, если документ содержит какое-либо из этих полей, поэтому вы, вероятно, захотите инвертировать результат, чтобы запретить определенные поля.

Например, в следующем примере клиентам не разрешено создавать документ, содержащий поле average_score или rating_count поскольку эти поля будут добавлены вызовом сервера позже.

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

Создание белого списка полей для новых документов

Вместо того, чтобы запрещать определенные поля в новых документах, вы можете создать список только тех полей, которые явно разрешены в новых документах. Затем вы можете использовать функцию hasOnly() чтобы убедиться, что все новые созданные документы содержат только эти поля (или подмножество этих полей) и никаких других.

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

Объединение обязательных и необязательных полей

Вы можете объединить операции hasAll и hasOnly в своих правилах безопасности, чтобы требовать одни поля и разрешать другие. Например, в этом примере требуется, чтобы все новые документы содержали поля name , location и city , а также, при необходимости, допускались поля address , hours и 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']));
    }
  }
}

В реальном сценарии вы можете переместить эту логику во вспомогательную функцию, чтобы избежать дублирования кода и упростить объединение необязательных и обязательных полей в один список, например:

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

Ограничение полей при обновлении

Обычной практикой безопасности является разрешение клиентам редактировать только некоторые поля, но не другие. Вы не можете сделать это, просто просматривая список request.resource.data.keys() , описанный в предыдущем разделе, поскольку этот список представляет собой полный документ в том виде, в каком он будет выглядеть после обновления, и, следовательно, будет включать поля, которые клиент не указал. изменять.

Однако если бы вы использовали функцию diff() , вы могли бы сравнить request.resource.data с объектом resource.data , который представляет документ в базе данных перед обновлением. При этом создается объект mapDiff , который содержит все изменения между двумя разными картами.

Вызвав affectedKeys() в этом файлеmapDiff, вы можете получить набор полей, которые были изменены при редактировании. Затем вы можете использовать такие функции, как hasOnly() или hasAny() чтобы убедиться, что этот набор содержит (или не содержит) определенные элементы.

Запретить изменение некоторых полей

Используя метод hasAny() для набора, сгенерированного функцией affectedKeys() , а затем отрицая результат, вы можете отклонить любой клиентский запрос, который пытается изменить поля, которые вы не хотите изменять.

Например, вы можете разрешить клиентам обновлять информацию о ресторане, но не менять свой средний балл или количество отзывов.

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

Разрешение изменять только определенные поля

Вместо указания полей, которые вы не хотите изменять, вы также можете использовать функцию hasOnly() чтобы указать список полей, которые вы хотите изменить. Обычно это считается более безопасным, поскольку запись в любые новые поля документа запрещена по умолчанию, пока вы явно не разрешите ее в своих правилах безопасности.

Например, вместо того, чтобы запрещать поля average_score и rating_count , вы можете создать правила безопасности, которые позволят клиентам изменять только поля name , location , city , address , hours и 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']));
    }
  }
}

Это означает, что если в какой-либо будущей итерации вашего приложения документы ресторана будут включать поле telephone , попытки изменить это поле завершится неудачей, пока вы не вернетесь и не добавите это поле в список hasOnly() в ваших правилах безопасности.

Обеспечение соблюдения типов полей

Еще одним эффектом бессхемности Cloud Firestore является то, что на уровне базы данных не устанавливается, какие типы данных могут храниться в определенных полях. Однако это можно реализовать в правилах безопасности с помощью оператора is .

Например, следующее правило безопасности требует, чтобы поле score отзыва было целым числом, поля headline , content author_name — строками, а review_date — временной меткой.

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

Допустимые типы данных для оператора is : bool , bytes , float , int , list , latlng , number , path , map , string и timestamp . Оператор is также поддерживает типы данных constraint , duration , set и map_diff , но поскольку они генерируются самим языком правил безопасности, а не клиентами, вы редко используете их в большинстве практических приложений.

Типы данных list и map не поддерживают универсальные шаблоны или аргументы типа. Другими словами, вы можете использовать правила безопасности, чтобы обеспечить, чтобы определенное поле содержало список или карту, но вы не можете обеспечить, чтобы поле содержало список всех целых чисел или всех строк.

Аналогичным образом вы можете использовать правила безопасности для принудительного применения значений типов для определенных записей в списке или карте (используя нотацию скобок или имена ключей соответственно), но не существует ярлыка для принудительного применения типов данных всех элементов в карте или списке на странице один раз.

Например, следующие правила гарантируют, что поле tags в документе содержит список и что первая запись является строкой. Это также гарантирует, что поле product содержит карту, которая, в свою очередь, содержит имя продукта, представляющее собой строку, и количество, являющееся целым числом.

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

Типы полей необходимо соблюдать как при создании, так и при обновлении документа. Поэтому вы можете рассмотреть возможность создания вспомогательной функции, которую вы можете вызывать как в разделах создания, так и в разделе обновления правил безопасности.

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

Принудительные типы для необязательных полей

Важно помнить, что вызов request.resource.data.foo для документа, где foo не существует, приводит к ошибке, и поэтому любое правило безопасности, выполняющее этот вызов, отклонит запрос. Вы можете справиться с этой ситуацией, используя метод get в request.resource.data . Метод get позволяет вам предоставить аргумент по умолчанию для поля, которое вы получаете с карты, если это поле не существует.

Например, если документы проверки также содержат необязательное поле photo_url и необязательное поле tags , которые вы хотите проверить как строки и списки соответственно, вы можете добиться этого, переписав функцию reviewFieldsAreValidTypes примерно так:

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

Это отклоняет документы, в которых tags существуют, но не являются списком, но по-прежнему разрешает документы, которые не содержат поле tags (или photo_url ).

Частичная запись никогда не допускается.

Последнее замечание о Cloud Firestore Security Rules : они либо позволяют клиенту вносить изменения в документ, либо отклоняют все изменения. Вы не можете создавать правила безопасности, которые разрешают запись в некоторые поля вашего документа и отклоняют другие в той же операции.