На этой странице рассматриваются концепции, изложенные в разделах «Структурирование правил безопасности» и «Написание условий для правил безопасности», чтобы объяснить, как можно использовать 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/финансы
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
не поддерживают generics или аргументы type. Другими словами, вы можете использовать правила безопасности, чтобы обеспечить, чтобы определенное поле содержало список или 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 заключается в том, что они либо позволяют клиенту вносить изменения в документ, либо отклоняют все редактирование. Вы не можете создать правила безопасности, которые принимают записи в некоторые поля документа, отклоняя другие в той же операции.